Merge goose2 repo into ui/goose2 with full history
@@ -0,0 +1,196 @@
|
||||
---
|
||||
name: code-review
|
||||
description: >-
|
||||
Senior engineer code review focused on catching issues before they become PR
|
||||
comments. Reviews only changed lines, categorizes issues by priority, and fixes
|
||||
them one by one. Use when the user says "code review", "review my code",
|
||||
"review this branch", or wants pre-PR feedback.
|
||||
---
|
||||
|
||||
# Pre-PR Code Review
|
||||
|
||||
You are a senior engineer conducting a thorough code review. Review **only the lines that changed** in this branch (via `git diff main...HEAD`) and provide actionable feedback on code quality. Do not flag issues in unchanged code.
|
||||
|
||||
## Determine Files to Review
|
||||
|
||||
**Before starting the review**, identify which files to review by checking:
|
||||
|
||||
1. **Run git commands** to check both:
|
||||
- Committed changes: `git diff --name-only main...HEAD`
|
||||
- Unstaged/staged changes: `git status --short`
|
||||
|
||||
2. **Ask the user which set to review** if both exist:
|
||||
- If there are both committed changes AND unstaged/staged changes, ask: "I see you have both committed changes and unstaged/staged changes. Which would you like me to review?"
|
||||
- **Option A**: Committed changes in this branch (compare against main)
|
||||
- **Option B**: Current unstaged/staged changes
|
||||
- **Option C**: Both
|
||||
|
||||
3. **Proceed automatically** if only one set exists:
|
||||
- If only committed changes exist → review those
|
||||
- If only unstaged/staged changes exist → review those
|
||||
- If neither exist → inform the user there are no changes to review
|
||||
|
||||
4. **Get the file list** based on the user's choice:
|
||||
- For committed changes: Use `git diff --name-only main...HEAD`
|
||||
- For unstaged/staged: Use `git diff --name-only` and `git diff --cached --name-only`
|
||||
- Filter to only include files that exist (some may be deleted)
|
||||
|
||||
**Only proceed with the review once you have the specific list of files to review.**
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### React Best Practices
|
||||
- **Components**: Are functional components with hooks used consistently?
|
||||
- **State Management**: Is `useState` and `useEffect` used properly? Any unnecessary re-renders?
|
||||
- **Props**: Are prop types properly defined with TypeScript interfaces?
|
||||
- **Keys**: Are list items using proper unique keys (not array indices)?
|
||||
- **Hooks Rules**: Are hooks called at the top level and in the correct order?
|
||||
- **Custom Hooks**: Could any logic be extracted into reusable custom hooks?
|
||||
|
||||
### TypeScript Best Practices
|
||||
- **const vs let vs var**: Is `const` used by default? Is `let` only used when reassignment is needed? Is `var` avoided entirely?
|
||||
- **Type Safety**: Are types explicit and avoiding `any`? Are proper interfaces/types defined?
|
||||
- **Type Assertions**: Are type assertions (`as`) used sparingly and only when necessary?
|
||||
- **Non-null Assertions**: Are non-null assertions (`!`) avoided? They bypass TypeScript's null safety and hide bugs. Use proper null checks or optional chaining instead.
|
||||
- **React Ref Types**: Are React refs properly typed as nullable (`useRef<T>(null)` with `RefObject<T | null>`)? Refs are null on first render and during unmount.
|
||||
- **Optional Chaining**: Is optional chaining (`?.`) used appropriately for potentially undefined values?
|
||||
- **Enums vs Union Types**: Are union types preferred over enums where appropriate?
|
||||
|
||||
### Design System & Styling
|
||||
- **Component Usage**: Are design system components used instead of raw HTML elements (`<Button>` not `<button>`, `<Input>` not `<input>`)?
|
||||
- **No Custom Styling**: Is custom inline styling or CSS avoided in favor of design system utilities?
|
||||
- **Tailwind Classes**: Are Tailwind utility classes used properly and consistently?
|
||||
- **Tailwind JIT Compilation**: Are Tailwind classes using static strings? JavaScript variables in template literals (e.g., `` `max-w-[${variable}]` ``) break JIT compilation. Use static strings or conditional logic instead (e.g., `condition ? 'max-w-[100px]' : 'max-w-[200px]'`).
|
||||
- **Theme Tokens**: Are theme tokens used for colors that adapt to light/dark mode (e.g., `text-foreground`, `bg-card`, `text-muted-foreground`) instead of hardcoded colors (e.g., `text-black`, `bg-white`)?
|
||||
- **Variants**: Could any components benefit from additional variants or properties in the design system?
|
||||
- **Light and Dark Mode Support**: Are colors working properly in both light and dark modes? No broken colors?
|
||||
- **Responsive Layout**: Does the layout work correctly at all breakpoints? No broken layout on mobile, tablet, or desktop?
|
||||
|
||||
### Localization (i18n)
|
||||
- **New Keys**: When new translation keys are added to one locale (e.g., `en`), are all other supported locales updated too? i18next falls back gracefully, but incomplete locales should be flagged.
|
||||
- **Removed Keys**: When UI text is removed, are the corresponding translation keys removed from all locale files?
|
||||
- **Raw Strings**: Are user-facing strings wrapped in `t()` calls instead of hardcoded in JSX? Non-translatable symbols (icons, punctuation, HTML entities) are acceptable with an `i18n-check-ignore` annotation.
|
||||
|
||||
### Code Simplicity (DRY Principle)
|
||||
- **Duplication**: Is there any repeated code that could be extracted into functions or components?
|
||||
- **Complexity**: Are there overly complex functions that could be broken down?
|
||||
- **Logic**: Is the logic straightforward and easy to follow?
|
||||
- **Abstractions**: Are abstractions appropriate (not too early, not too late)?
|
||||
- **Guard Clauses**: Are early-return guards used to keep code shallow and readable?
|
||||
|
||||
### Code Cleanliness
|
||||
- **Comments**: Are there unnecessary comments explaining obvious code? (Remove them)
|
||||
- **Console Logs**: Are there leftover `console.log` statements? (Remove them)
|
||||
- **Dead Code**: Is there unused code, commented-out code, or unused imports?
|
||||
- **Cross-Boundary Dead Data**: Are there struct/interface fields computed on one side of a boundary (e.g., Rust backend) but never consumed on the other (e.g., TypeScript frontend)? This wastes computation and adds noise to data contracts.
|
||||
- **Naming**: Are variable and function names clear and descriptive?
|
||||
- **Magic Numbers**: Are there magic numbers without explanation? Should they be named constants?
|
||||
|
||||
### Animation & UI Polish
|
||||
- **Race Conditions**: Are there any animation race conditions or timing issues?
|
||||
- **Single Source of Truth**: Is state managed in one place to avoid conflicts?
|
||||
- **AnimatePresence**: Is it used properly with unique keys for dialog/modal transitions?
|
||||
- **Reduced Motion**: Is `useReducedMotion()` respected for accessibility?
|
||||
|
||||
### General Code Quality
|
||||
- **Error Handling**: Are errors handled gracefully with user-friendly messages?
|
||||
- **Loading States**: Are loading states shown during async operations?
|
||||
- **Accessibility**: Are ARIA labels, keyboard navigation, and screen reader support considered?
|
||||
- **Performance**: Are there any obvious performance issues (unnecessary re-renders, heavy computations)?
|
||||
- **Git Hygiene**: Are there any files that shouldn't be committed (env files, etc.)?
|
||||
- **Unrelated Changes**: Are there any stray files or changes that don't relate to the branch's main purpose? (Accidental commits, unrelated fixes)
|
||||
|
||||
## Review & Fix Process
|
||||
|
||||
### Step 0: Run Quality Checks
|
||||
|
||||
Before reading any code, run the project's CI gate to establish a baseline:
|
||||
|
||||
```bash
|
||||
just ci
|
||||
```
|
||||
|
||||
This runs: `pnpm check` (Biome lint/format + file sizes), `pnpm typecheck`, `just clippy` (Rust linting), `pnpm test`, `pnpm build`, and `just tauri-check` (Rust type checking).
|
||||
|
||||
Report the results as pass/fail. Any failures are automatically **P0** issues and should appear at the top of the findings list. Do not skip this step even if the user only wants a quick review.
|
||||
|
||||
### Step 1: Conduct Review
|
||||
|
||||
For each file in the list:
|
||||
|
||||
1. Run `git diff main...HEAD -- <file>` to get the exact lines that changed
|
||||
2. Review **only those changed lines** against the Review Checklist — do not flag issues in unchanged code
|
||||
3. Note the file path and line numbers from the diff output for each issue found
|
||||
|
||||
### Step 2: Categorize Issues
|
||||
|
||||
Assign each issue a priority level:
|
||||
- **P0**: Breaks functionality, TypeScript errors, security issues
|
||||
- **P1–P2**: Performance problems, accessibility issues, code quality, unnecessary complexity, poor practices, design system violations
|
||||
- **P3**: Style inconsistencies, minor improvements, missing type safety, animation issues, theme token usage
|
||||
- **P4**: Cleanup — console logs, unused imports, dead code, unnecessary comments, unrelated changes
|
||||
|
||||
If many high-severity issues exist in a file, assess whether a full refactor would be simpler than individual fixes.
|
||||
|
||||
### Step 3: Present Findings
|
||||
|
||||
After reviewing all files, provide:
|
||||
- **Summary**: Total files reviewed, overall quality rating (1-5 stars)
|
||||
- **Issues**: A single numbered list ordered by priority (P0 first, P4 last). Each issue must follow this exact format:
|
||||
|
||||
```
|
||||
1. Short Issue Title (P0) [Must Fix]
|
||||
- Description of the issue and why it matters
|
||||
- Recommended fix
|
||||
|
||||
2. Short Issue Title (P3) [Your Call]
|
||||
- Description of the issue and why it may or may not need addressing
|
||||
- Recommended fix if the user chooses to act on it
|
||||
```
|
||||
|
||||
Use a short, descriptive title (3–6 words max) so issues can be referenced by number (e.g. "fix issue 3").
|
||||
|
||||
### Step 3b: Self-Check
|
||||
|
||||
Before presenting findings to the user, silently review the issue list two more times:
|
||||
|
||||
1. **Pass 1**: For each issue, ask — is this genuinely a problem, or could it be intentional/acceptable? Remove false positives.
|
||||
2. **Pass 2**: For each remaining issue, ask — does the recommended fix actually improve the code, or is it a matter of preference?
|
||||
|
||||
After both passes, tag each surviving issue as one of:
|
||||
- **[Must Fix]** — clear violation, will likely get flagged in PR review
|
||||
- **[Your Call]** — valid concern but may be intentional or a reasonable tradeoff (e.g. stepping outside the design system for a specific reason). Present it but let the user decide.
|
||||
|
||||
Only present issues that survived both passes.
|
||||
|
||||
### Step 4: Fix Issues
|
||||
|
||||
**Before fixing**, ask: "Would you like me to fix these issues in order? Or do you have questions about any of them first? I will fix each issue one by one and ask for approval before moving to the next one."
|
||||
|
||||
**When approved**, work through issues one at a time in numbered order (P0 → P4). After each fix:
|
||||
1. Explain what was changed and why
|
||||
2. Ask: "Does that look good? Ready to move on to issue [N]?"
|
||||
3. Wait for confirmation before proceeding to the next issue
|
||||
|
||||
**Important**: When adding documentation comments:
|
||||
- Only add comments for non-obvious things: magic numbers, complex logic, design decisions, or workarounds
|
||||
- If you call out something as confusing or hard-coded in your review and suggest adding documentation, it's acceptable to add a comment when approved
|
||||
- Don't add comments that just restate what the code does
|
||||
|
||||
Explain each change as you make it. If an issue is too subjective or minor, skip it and note why.
|
||||
|
||||
**Remember**: Cleanup tasks like removing comments should always be done LAST, because earlier fixes might introduce new comments that also need removal.
|
||||
|
||||
### Step 5: Ready to Ship
|
||||
|
||||
Once all issues are fixed, display:
|
||||
|
||||
---
|
||||
|
||||
**✅ Code review complete! All issues have been addressed.**
|
||||
|
||||
Your code is ready to commit and push. Lefthook will run the full CI gate (`just ci`) automatically when you push.
|
||||
|
||||
Next steps: generate a PR summary that explains the intent of this change, what files were modified and why, and how to verify the changes work.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: create-app-e2e-test
|
||||
description: Create app e2e tests for the Goose desktop app using the Tauri app test driver. Use when the user wants to generate, write, or verify UI tests that run against the live app.
|
||||
---
|
||||
|
||||
# Create App E2E Test
|
||||
|
||||
You are an AI agent that creates app e2e tests for the Goose desktop app using the Tauri app test driver.
|
||||
|
||||
## Goal
|
||||
|
||||
Given a test scenario in natural language, you will:
|
||||
|
||||
1. Explore the app using the test driver CLI to discover what's on screen
|
||||
2. Write a Vitest test file that verifies the scenario using stable selectors
|
||||
|
||||
**Do NOT read source code to understand the UI.** Do not read `.tsx`, `.ts`, or `.css` files to find elements. Use `snapshot` to discover what is on the page — that is your only method. The one exception: read source code only when you need to add a `data-testid` attribute.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The Tauri app must already be running in dev mode with the app test driver enabled.
|
||||
|
||||
## Test Driver CLI
|
||||
|
||||
All exploration commands use the test driver client CLI:
|
||||
|
||||
```bash
|
||||
pnpm test-driver <action> [selector] [value]
|
||||
```
|
||||
|
||||
Available commands:
|
||||
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `snapshot` | Get a text DOM of visible elements | `pnpm test-driver snapshot` |
|
||||
| `getText <selector>` | Get inner text of an element | `pnpm test-driver getText "h1"` |
|
||||
| `count <selector>` | Count matching elements | `pnpm test-driver count "button"` |
|
||||
| `click <selector>` | Click an element | `pnpm test-driver click "button"` |
|
||||
| `fill <selector> <value>` | Fill an input/textarea | `pnpm test-driver fill "textarea" "hello"` |
|
||||
| `keypress <selector> <key>` | Dispatch a keyboard event | `pnpm test-driver keypress "textarea" Enter` |
|
||||
| `waitForText <text>` | Wait for text to appear in body (30s default) | `pnpm test-driver waitForText "Success"` |
|
||||
| `scroll <direction>` | Scroll the page (up/down/top/bottom) | `pnpm test-driver scroll down` |
|
||||
| `screenshot [path]` | Take a screenshot | `pnpm test-driver screenshot test.png` |
|
||||
|
||||
### Snapshot Format
|
||||
|
||||
The `snapshot` command returns a simplified text DOM:
|
||||
|
||||
```
|
||||
[e1] input type="text" placeholder="Ask anything..."
|
||||
[t1] label "Name:"
|
||||
[e2] button "Send"
|
||||
[t2] span "Click me"
|
||||
[t3] h1 "Good afternoon"
|
||||
```
|
||||
|
||||
- `[eN]` — interactive elements (input, button, select, textarea, a) with auto-assigned `data-tid`
|
||||
- `[tN]` — visible text nodes
|
||||
- Hidden elements are excluded
|
||||
- Indentation shows DOM hierarchy
|
||||
|
||||
`data-tid` attributes (e.g., `[data-tid='e3']`) are assigned dynamically during each snapshot and **must not** be used in test files — they change between runs.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Explore
|
||||
|
||||
1. Navigate to home first, then `snapshot` to see the current page state.
|
||||
```bash
|
||||
pnpm test-driver click '[data-testid="nav-home"]'
|
||||
pnpm test-driver snapshot
|
||||
```
|
||||
Tests always start from the home screen (`useTestDriver()` navigates home in `beforeEach`), so exploration should too.
|
||||
|
||||
2. For each element you need to interact with or verify:
|
||||
- Identify it from the snapshot (e.g., `[e3] button "Send"`)
|
||||
- Pick a stable selector using the **Element Locating Strategy** below — never use `data-tid`
|
||||
- Use `count` with that stable selector to verify it matches exactly one element
|
||||
- Use `getText` to verify text content
|
||||
- Use `click`/`fill` to perform actions during exploration
|
||||
|
||||
3. After each action, run `snapshot` again — the DOM may have changed.
|
||||
|
||||
Example exploration session:
|
||||
```bash
|
||||
# 1. See what's on the page
|
||||
pnpm test-driver snapshot
|
||||
|
||||
# 2. Pick a selector for the element you need, verify it matches exactly 1
|
||||
pnpm test-driver count 'textarea[placeholder="Ask anything..."]'
|
||||
|
||||
# 3. Interact
|
||||
pnpm test-driver fill 'textarea[placeholder="Ask anything..."]' "hello world"
|
||||
|
||||
# 4. Snapshot again to see the result
|
||||
pnpm test-driver snapshot
|
||||
```
|
||||
|
||||
### Phase 2: Write the Test
|
||||
|
||||
Create a Vitest test file at `tests/app-e2e/<name>.test.ts`.
|
||||
|
||||
Use `useTestDriver()` from `tests/app-e2e/lib/setup.ts` to get a shared test driver connection with automatic teardown and screenshot-on-failure. See `tests/app-e2e/chat.test.ts` as a reference:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { useTestDriver } from "./lib/setup";
|
||||
|
||||
describe("<Feature>", () => {
|
||||
const testDriver = useTestDriver();
|
||||
|
||||
it("does something", async () => {
|
||||
const text = await testDriver.getText('[data-testid="greeting"]');
|
||||
expect(text).toContain("Good");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Test driver API methods available in tests:
|
||||
|
||||
- `testDriver.snapshot()` → text DOM string
|
||||
- `testDriver.getText(selector, { timeout? })` → inner text string
|
||||
- `testDriver.count(selector)` → number of matching elements
|
||||
- `testDriver.click(selector, { timeout? })` → clicks element
|
||||
- `testDriver.fill(selector, value, { timeout? })` → fills input/textarea
|
||||
- `testDriver.keypress(selector, key, { timeout? })` → dispatches keyboard event
|
||||
- `testDriver.waitForText(text, { selector?, timeout? })` → waits for text to appear (default: body, 30s)
|
||||
- `testDriver.scroll(direction)` → scrolls page ("up", "down", "top", "bottom")
|
||||
- `testDriver.screenshot(path)` → saves screenshot
|
||||
|
||||
All `timeout` options default to 5 seconds. `waitForText` defaults to 30 seconds.
|
||||
|
||||
### Phase 3: Verify the Test
|
||||
|
||||
Run the test to make sure it passes:
|
||||
|
||||
```bash
|
||||
pnpm test:app-e2e
|
||||
```
|
||||
|
||||
If it fails, use the test driver CLI to re-explore. The issue could be a wrong selector, an incorrect assertion, or a bug in the app implementation.
|
||||
|
||||
## Element Locating Strategy
|
||||
|
||||
**Always** verify uniqueness with `count` before using any selector in a test. If count > 1, the test will be flaky.
|
||||
|
||||
For each element, find a stable selector using this priority:
|
||||
|
||||
1. **`data-testid` (preferred)**: if the element already has a `data-testid`, use `'[data-testid="my-element"]'`.
|
||||
- Verify with `count` that it's unique
|
||||
|
||||
2. **Semantic selector**: combine attributes, hierarchy, or roles to target a specific element.
|
||||
- `'input[placeholder="Ask anything..."]'` — narrow by attribute value
|
||||
- `'.sidebar [role="navigation"] a'` — narrow by parent context
|
||||
- Always verify with `count` that the selector matches exactly 1 element
|
||||
|
||||
3. **Add a `data-testid` (last resort)**: if no stable selector exists, add a `data-testid` to the source code.
|
||||
- Names must be descriptive and include context (e.g., `home-greeting` not `greeting`, `sidebar-new-chat-button` not `button`)
|
||||
- Only add the `data-testid` attribute — do not change any other source code
|
||||
- Note the code change so it can be committed alongside the test
|
||||
|
||||
**Never** use `data-tid` attributes (`[data-tid='e3']`) in test files — they are assigned dynamically by `snapshot` and change between runs.
|
||||
|
||||
## Rules
|
||||
|
||||
- One test file per feature area (e.g., `home.test.ts`, `sidebar.test.ts`, `settings.test.ts`)
|
||||
- Keep test descriptions specific: "shows time-based greeting on home screen", not "home works"
|
||||
- Always check `count` === 1 for selectors before using them in assertions
|
||||
- Do not use `snapshot` in test assertions — it's for exploration only
|
||||
- The test driver automatically waits up to 5 seconds (configurable via `{ timeout }`) for elements to appear before `click`, `fill`, `getText`, and `keypress`
|
||||
- Use `waitForText` to wait for specific text content to appear (e.g., after submitting a form or waiting for an LLM response)
|
||||
- If the DOM updates after an action, run `snapshot` again to see the new state before writing assertions
|
||||
- Do not add comments in test files — the test descriptions and code should be self-explanatory
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: create-pr
|
||||
description: >-
|
||||
Create a GitHub PR from the current branch: handle uncommitted changes, generate
|
||||
a summary, and submit via gh CLI. Use when the user says "create PR", "open PR",
|
||||
"submit PR", "push PR", or wants to create a pull request.
|
||||
---
|
||||
|
||||
# Create PR
|
||||
|
||||
Create a GitHub PR from the current branch: handle uncommitted changes, generate a summary, and submit.
|
||||
|
||||
## Step 1: Rebase Reminder
|
||||
|
||||
Before doing anything else, remind the user to rebase onto main if they haven't already. Ask if they'd like to proceed or rebase first.
|
||||
|
||||
## Step 2: Check for Uncommitted Changes
|
||||
|
||||
Run `git status` to check for staged, unstaged, or untracked changes.
|
||||
|
||||
- If there are uncommitted changes, show the user what's outstanding and ask if they'd like to commit them before creating the PR.
|
||||
- If the user says yes, stage the relevant files, draft a concise commit message based on the changes, and commit.
|
||||
- If there are no uncommitted changes, move on.
|
||||
|
||||
## Step 3: Gather Branch Context
|
||||
|
||||
Run these commands in parallel to understand the branch:
|
||||
|
||||
1. `git log main..HEAD --oneline` to see all commits on this branch.
|
||||
2. `git diff main..HEAD --stat` to get the list of changed files.
|
||||
3. `git diff main..HEAD` to understand what changed in each file.
|
||||
4. `git rev-parse --abbrev-ref HEAD` to get the current branch name.
|
||||
5. `git status` to check if the branch has been pushed to remote.
|
||||
|
||||
## Step 4: Generate PR Title and Summary
|
||||
|
||||
**Title:** Generate a concise PR title (under 72 characters) that captures the intent of the change. Use conventional style: lowercase, imperative mood (e.g., "prevent chat list from reordering when renaming sessions").
|
||||
|
||||
**Body:** Generate a PR summary with these four sections:
|
||||
|
||||
### Section 1: Overview
|
||||
|
||||
Start with metadata tags, then a Problem/Solution block:
|
||||
|
||||
- `**Category:**` — one of: `new-feature`, `improvement`, `fix`, `infrastructure`
|
||||
- `**User Impact:**` — one sentence describing what changed from the user's perspective. Write this as a standalone sentence a non-technical stakeholder would understand (e.g., "Users can now create and schedule repeatable tasks directly from the desktop app."). This line is used for project changelogs.
|
||||
- `**Problem:**` — describe the user-facing confusion, mismatch, or friction this PR addresses.
|
||||
- `**Solution:**` — explain how the change resolves that UX problem and, if applicable, why the approach was chosen.
|
||||
|
||||
Keep Problem + Solution to 2-4 sentences total. Prioritize intent and expected user experience, but include brief high-level implementation rationale when it explains reliability, maintainability, or code quality.
|
||||
|
||||
### Section 2: Changes
|
||||
|
||||
Wrap this section in a collapsible `<details>` block with the summary "File changes".
|
||||
|
||||
Inside, list every changed file. For each file, use the filename as a bold header, then underneath write one or two sentences about what was changed and why. Focus on intent, not implementation details.
|
||||
|
||||
Format:
|
||||
```
|
||||
<details>
|
||||
<summary>File changes</summary>
|
||||
|
||||
**path/to/file.ts**
|
||||
What changed and why.
|
||||
|
||||
**path/to/other.rs**
|
||||
What changed and why.
|
||||
|
||||
</details>
|
||||
```
|
||||
|
||||
### Section 3: Reproduction Steps
|
||||
|
||||
Numbered steps in plain English for how an engineer can see the outcome of this PR. Assume they know how to run the project. Focus on where to look and what they should see.
|
||||
|
||||
### Section 4: Screenshots/Demos (for UX changes)
|
||||
|
||||
If the PR includes visual changes, include before/after screenshots or a short demo. If there are no visual changes, omit this section entirely.
|
||||
|
||||
## Step 5: Push and Create PR
|
||||
|
||||
1. Push the branch to remote if it hasn't been pushed yet: `git push -u origin HEAD`
|
||||
2. Create the PR using `gh pr create` with the generated title and body. Use a HEREDOC for the body to preserve formatting.
|
||||
3. Output the PR URL as a clickable hyperlink so the user can open it directly.
|
||||
|
||||
## Tone
|
||||
|
||||
Write from the perspective of a product designer explaining their thinking to engineers. Be clear and concise — just enough to establish intent. They can read the code; your job is to guide their understanding of the "why."
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/code-review
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/create-pr
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/code-review
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/create-pr
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/code-review
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/create-pr
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Thank you for taking the time, please report a reproducible bug
|
||||
title: "[Bug] <Bug Title Here>"
|
||||
labels: bug
|
||||
assignees: add codeowner's @name here
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
*A clear and concise description of what the bug is.*
|
||||
|
||||
**To Reproduce:**
|
||||
*Steps to reproduce the behavior:*
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior:**
|
||||
*A clear and concise description of what you expected to happen.*
|
||||
|
||||
**Supporting Material**
|
||||
*If applicable, add screenshots, output log and/or other documentation to help explain your problem.*
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [ex: iOS]
|
||||
- Version
|
||||
|
||||
**Additional context**
|
||||
Add any other context that you feel is relevant about the problem here.
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: ❓ Questions and Help 🤔
|
||||
url: https://discord.gg/block-opensource (/add your discord channel if applicable)
|
||||
about: This issue tracker is not for support questions. Please refer to the community for more help.
|
||||
@@ -0,0 +1,142 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm check
|
||||
- run: pnpm typecheck
|
||||
|
||||
test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm test
|
||||
|
||||
desktop:
|
||||
name: Desktop Build & E2E
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
|
||||
- name: Cache Rust
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
|
||||
- name: Check Tauri
|
||||
run: cd src-tauri && cargo check
|
||||
|
||||
- name: Clippy
|
||||
run: cd src-tauri && cargo clippy -- -D warnings
|
||||
|
||||
- name: Format check
|
||||
run: cd src-tauri && cargo fmt --check
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E tests
|
||||
run: pnpm exec playwright test
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
retention-days: 7
|
||||
|
||||
rust-lint:
|
||||
name: Rust Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache Rust
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Format check
|
||||
run: cd src-tauri && cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
run: cd src-tauri && cargo clippy -- -D warnings
|
||||
@@ -0,0 +1,48 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Rust/Tauri build artifacts
|
||||
/target/
|
||||
src-tauri/target/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Editor / IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.*.sw?
|
||||
|
||||
# OS artifacts
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Scratch / working files
|
||||
.scratch/
|
||||
|
||||
# Playwright artifacts
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# App E2E screenshots
|
||||
tests/app-e2e/screenshots/
|
||||
|
||||
# Testing coverage
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Hermit (toolchain manager cache)
|
||||
.hermit/
|
||||
|
||||
# Claude
|
||||
.claude
|
||||
@@ -0,0 +1,194 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidelines for AI agents (and developers) working on this codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Goose2 is a Tauri 2 + React 19 desktop app. It uses TypeScript strict mode, Vite, and Tailwind CSS 3. The codebase follows a feature-sliced architecture organized under `src/app/`, `src/features/`, and `src/shared/`.
|
||||
|
||||
## First Steps
|
||||
|
||||
Treat this repo as partially Hermit-managed. Do not assume `just`, `pnpm`, `node`, or `lefthook` are available globally.
|
||||
|
||||
- In bash/zsh, run `source ./bin/activate-hermit` before using repo tools if the shell cannot find `just`, `pnpm`, or other managed binaries.
|
||||
- In fish, run `source ./bin/activate-hermit.fish`.
|
||||
- If PATH still looks wrong or you want to avoid shell assumptions, prefer repo-local binaries such as `./bin/just`, `./bin/pnpm`, and `./bin/lefthook`.
|
||||
- Biome is installed from `package.json` devDependencies, not from Hermit. Run it through `pnpm`, `pnpm exec biome`, or `npx biome` after `just setup`.
|
||||
- On a fresh clone, a newly created worktree, or after `just clean`, run `just setup` before relying on `pnpm`, Biome, or app-local tooling.
|
||||
- In new clones and worktrees, ensure git hooks are installed early with `lefthook install`. If `lefthook` is not on PATH, use `./bin/lefthook install`.
|
||||
- Agents starting in a fresh clone or worktree should do the setup and hook-install steps proactively rather than assuming the environment is already bootstrapped.
|
||||
- Use `just dev` for the normal desktop workflow. Use `just dev-frontend` only when you intentionally want the Vite app without Tauri.
|
||||
|
||||
## Common Commands
|
||||
|
||||
- `just setup` installs frontend dependencies with `pnpm install` and builds the Rust backend once.
|
||||
- `just dev` starts the desktop app in dev mode and wires Tauri to the local Vite server.
|
||||
- `just check` runs Biome checks and file-size checks.
|
||||
- `just test` runs the Vitest suite.
|
||||
- `just tauri-check` runs `cargo check` in `src-tauri`.
|
||||
- `just ci` is the main local verification gate.
|
||||
- `just clean` removes Rust build artifacts, `dist`, and `node_modules`, so `just setup` is required again before `just dev`.
|
||||
|
||||
## Architecture & File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
app/ — App shell, entry point, top-level providers
|
||||
features/ — Feature modules (see Feature Organization below)
|
||||
<feature>/
|
||||
ui/ — React components (required)
|
||||
hooks/ — Custom React hooks for feature logic (when needed)
|
||||
stores/ — Zustand state management (when feature needs shared state)
|
||||
api/ — Backend API integration (when feature calls backend)
|
||||
types.ts — Feature-specific type definitions (when needed)
|
||||
shared/
|
||||
types/ — Canonical shared type definitions (single source of truth)
|
||||
agents.ts — Agent, Persona, Provider types
|
||||
chat.ts — ChatState, TokenState, Session, SSE events
|
||||
messages.ts — Message, MessageContent, type guards
|
||||
ui/ — Reusable UI components (button, etc.)
|
||||
lib/ — Utilities (cn.ts for class merging)
|
||||
theme/ — Theme provider, appearance settings
|
||||
styles/ — Global CSS, design tokens
|
||||
hooks/ — Shared hooks
|
||||
api/ — API integration
|
||||
constants/ — Shared constants
|
||||
context/ — Shared contexts
|
||||
```
|
||||
|
||||
### Feature Organization
|
||||
|
||||
Not every feature needs every subdirectory. Use only what the feature requires:
|
||||
|
||||
| Pattern | Structure | Examples |
|
||||
|----------------------|----------------------------------|----------------------|
|
||||
| **Full-featured** | `stores/` + `hooks/` + `ui/` | agents, chat |
|
||||
| **Data-driven** | `stores/` + `api/` + `ui/` | projects |
|
||||
| **API features** | `api/` + `ui/` | skills |
|
||||
| **Simple features** | `ui/` only | home, settings, sidebar, status |
|
||||
| **Tabs** | `ui/` + `types.ts` | tabs |
|
||||
|
||||
### Import Rules for Features
|
||||
|
||||
- Shared types live in `src/shared/types/` — this is the single source of truth for cross-feature types.
|
||||
- There should be NO root-level `src/stores/` or `src/types/` directories.
|
||||
- Feature stores use feature-relative imports (e.g., `../stores/featureStore`).
|
||||
- Cross-feature imports use `@/features/*/stores/` or `@/shared/types/`.
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
- Use `cn()` from `@/shared/lib/cn` for Tailwind class merging.
|
||||
- Import paths use the `@/` alias (maps to `./src`).
|
||||
- Components are controlled where possible (state lifted to parent).
|
||||
- Use `@tabler/icons-react` for icons (transitioning from `lucide-react`; existing `lucide-react` usage is fine until migrated).
|
||||
- All `<button>` elements must have `type="button"` to prevent form submission.
|
||||
- Use semantic HTML (`<aside>`, `<nav>`, `<header>`, `<main>`).
|
||||
|
||||
## Localization
|
||||
|
||||
- UI copy should go through `react-i18next`, not hardcoded English strings, for app areas that are already on i18n.
|
||||
- Shared localization lives in `src/shared/i18n/`; use `useTranslation()` for text and the helpers in `src/shared/i18n/format.ts` for dates, times, numbers, currency, and relative time.
|
||||
- Keep translations in feature-scoped JSON namespaces under `src/shared/i18n/locales/<locale>/` instead of one large file, and use stable keys rather than English sentences as keys.
|
||||
- Do not translate user-authored content, agent/model output, or backend-only strings unless they are rendered directly as Goose UI.
|
||||
- `pnpm check` includes `check:i18n`, which flags obvious new raw UI strings in migrated surfaces. Use a narrow `i18n-check-ignore` comment only when the string should stay literal.
|
||||
|
||||
## Theming System
|
||||
|
||||
ThemeProvider manages three axes:
|
||||
|
||||
| Axis | Values | Persistence | Mechanism |
|
||||
|--------------|---------------------------------|-----------------|----------------------------------------------|
|
||||
| Theme mode | `light`, `dark`, `system` | localStorage | `.dark` class on `<html>` |
|
||||
| Accent color | Any hex value | localStorage | `--color-accent` CSS variable |
|
||||
| Density | `compact`, `comfortable`, `spacious` | localStorage | `--density-spacing` CSS variable (0.75/1/1.25) |
|
||||
|
||||
- CSS variables are defined in `globals.css` with light/dark variants.
|
||||
- Tailwind config maps CSS variables to semantic color names.
|
||||
- Color palette tokens: `background` (primary/secondary/tertiary), `foreground` (primary/secondary/tertiary), `border`, `ring`, plus semantic variants (`info`, `danger`, `success`, `warning`).
|
||||
|
||||
## Component Patterns
|
||||
|
||||
- Small, focused components — aim for under 200 lines.
|
||||
- Props interfaces live in the component file, or in `types.ts` for shared types.
|
||||
- Use `forwardRef` for components that need ref forwarding (React 19 makes this optional, but the pattern is still used).
|
||||
- Animations: CSS transitions via Tailwind classes; respect `prefers-reduced-motion`.
|
||||
- Entrance animations: use the `isLoaded` state pattern with `useEffect` + short timeout.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- ARIA roles on interactive elements (`role="tab"`, `role="tablist"`, `role="status"`).
|
||||
- `aria-label` on icon-only buttons.
|
||||
- `aria-hidden` on visually hidden content.
|
||||
- `aria-selected` on selectable items.
|
||||
- Color-only indicators must have text alternatives.
|
||||
- `prefers-reduced-motion` is respected globally.
|
||||
|
||||
## Tauri Integration
|
||||
|
||||
- The window starts hidden and is shown via `getCurrentWindow().show()` after React mounts.
|
||||
- Use `data-tauri-drag-region` on header areas for window dragging.
|
||||
- Title bar uses `titleBarStyle: "Overlay"` with `hiddenTitle: true` for a custom titlebar.
|
||||
- `tauri-plugin-window-state` persists window size and position.
|
||||
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
|
||||
|
||||
## Backend Architecture
|
||||
|
||||
All AI communication goes through **ACP (Agent Client Protocol)**:
|
||||
- The Rust backend spawns ACP agent binaries as child processes and communicates via **stdin/stdout JSON-RPC**.
|
||||
- Responses stream back to the frontend through **Tauri events** (`acp:text`, `acp:tool_call`, `acp:tool_result`, `acp:done`, etc.).
|
||||
- The frontend listens to these events via `@tauri-apps/api/event` (see `useAcpStream` hook).
|
||||
|
||||
For non AI communication, such as configuration:
|
||||
- Use **Tauri commands** (`invoke()` from `@tauri-apps/api/core`) for request/response operations (sessions, personas, skills, projects, etc.).
|
||||
- Use **Tauri events** (`listen()` from `@tauri-apps/api/event`) for streaming data from ACP.
|
||||
- Do **not** add HTTP fetch calls to a backend server, `apiFetch` utilities, or sidecar process management.
|
||||
|
||||
## Tooling
|
||||
|
||||
| Tool | Purpose |
|
||||
|-------------|-------------------------------------------------|
|
||||
| **Hermit** | Manages repo binaries such as `node`, `pnpm`, `just`, and `lefthook` |
|
||||
| **Just** | Task runner (`just dev`, `just build`, `just check`) |
|
||||
| **Lefthook**| Git hooks (pre-commit, pre-push) |
|
||||
| **Biome** | Linting and formatting |
|
||||
| **pnpm** | Package manager |
|
||||
|
||||
Additional tooling notes:
|
||||
|
||||
- Prefer repo-managed binaries over global tools when there is any ambiguity about PATH.
|
||||
- Hermit manages `node`, `pnpm`, `just`, and `lefthook`, while Biome comes from `node_modules` after `just setup`.
|
||||
- Tauri backend commands still rely on a working Rust/Cargo toolchain.
|
||||
- Pre-commit hooks run formatting plus `just check`.
|
||||
- Pre-push hooks run `just fmt-check`, `just clippy`, `just check`, `just test`, `just build`, and `just tauri-check`.
|
||||
- Do not use `--no-verify` to bypass hooks. Fix the underlying issue instead.
|
||||
|
||||
## Testing & Verification
|
||||
|
||||
- Unit/component tests use Vitest and Testing Library via `just test` or `pnpm test`.
|
||||
- E2E tests use Playwright via `just test-e2e` and `just test-e2e-all`.
|
||||
- File size enforcement runs through `pnpm check:file-sizes` and is included in `just check`.
|
||||
- Before handing off a change, run the smallest relevant verification step. Use `just ci` when you need the full local gate.
|
||||
- GitHub Actions also runs desktop-oriented checks, including Playwright coverage, that are broader than the local pre-push hook.
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- `react` 19.1, `react-dom` 19.1
|
||||
- `@tauri-apps/api` 2.x
|
||||
- `@tanstack/react-query` 5.x
|
||||
- `tailwindcss` 3.x with `tailwindcss-animate`
|
||||
- `@tabler/icons-react` for icons (migrating from `lucide-react`)
|
||||
- `class-variance-authority` for component variants
|
||||
- `clsx` + `tailwind-merge` for class merging
|
||||
- `@radix-ui/react-slot` for polymorphic components
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't import from `../` across feature boundaries — use `@/` paths.
|
||||
- Don't put business logic in UI components — extract to hooks or utilities.
|
||||
- Don't use inline styles except for dynamic values (like animation delays).
|
||||
- Don't add dependencies without checking if an existing one covers the need.
|
||||
- Don't skip `type="button"` on buttons.
|
||||
- Don't use color-only indicators without text alternatives.
|
||||
- Never use `--no-verify` when pushing — fix the underlying lint/hook issues.
|
||||
- Don't create root-level `src/types/` or `src/stores/` directories — types belong in `src/shared/types/`, stores belong in `src/features/<feature>/stores/`.
|
||||
- Don't duplicate type definitions across files — each type has one canonical location.
|
||||
@@ -0,0 +1,24 @@
|
||||
# This CODEOWNERS file denotes the project leads
|
||||
# and encodes their responsibilities for code review.
|
||||
|
||||
# Instructions: At a minimum, replace the '@GITHUB_USER_NAME_GOES_HERE'
|
||||
# here with at least one project lead.
|
||||
|
||||
# Lines starting with '#' are comments.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
# The format is described: https://github.blog/2017-07-06-introducing-code-owners/
|
||||
|
||||
# These owners will be the default owners for everything in the repo.
|
||||
* @wesbillman
|
||||
|
||||
|
||||
# -----------------------------------------------
|
||||
# BELOW THIS LINE ARE TEMPLATES, UNUSED
|
||||
# -----------------------------------------------
|
||||
# Order is important. The last matching pattern has the most precedence.
|
||||
# So if a pull request only touches javascript files, only these owners
|
||||
# will be requested to review.
|
||||
# *.js @octocat @github/js
|
||||
|
||||
# You can also use email addresses if you prefer.
|
||||
# docs/* docs@example.com
|
||||
@@ -0,0 +1 @@
|
||||
## [Click here for Block Open Source Project governance information](https://github.com/block/.github/blob/main/GOVERNANCE.md)
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Copyright 2026 Block, Inc.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Goose2
|
||||
|
||||
Goose2 is a Tauri 2 + React 19 desktop app.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. If your shell cannot find `just`, `pnpm`, or `lefthook`, activate Hermit.
|
||||
bash/zsh: `source ./bin/activate-hermit`
|
||||
fish: `source ./bin/activate-hermit.fish`
|
||||
2. Install git hooks: `lefthook install`
|
||||
3. Install dependencies: `just setup`
|
||||
4. Start the app: `just dev`
|
||||
|
||||
`just clean` removes Rust build artifacts, `dist`, and `node_modules`. Run `just setup` again before `just dev`.
|
||||
|
||||
`just setup` bootstraps a shared managed goose checkout in a home-level cache directory when it does not exist, fast-forwards it, builds a local `goose` binary, and stamps the exact branch/commit it used. `just dev` only does a lightweight preflight against that shared stamp; if the managed checkout is missing, stale, or built from the wrong branch, it warns and tells you to rerun `just setup`. By default the helper uses `~/Library/Caches/goose2-dev` on macOS, or `$XDG_CACHE_HOME/goose2-dev` / `~/.cache/goose2-dev` elsewhere. It prefers `origin/baxen/goose2` and falls back to `origin/main` when that branch does not exist yet.
|
||||
|
||||
Override the shared cache root or branch with `GOOSE_DEV_ROOT=/path/to/cache` and `GOOSE_DEV_BRANCH=my/integration-branch`. You can also override the checkout path directly with `GOOSE_DEV_REPO=/path/to/goose`, or the clone source with `GOOSE_DEV_CLONE_URL=...`.
|
||||
|
||||
Run `just` to list available commands, or see [justfile](./justfile) for the full recipe definitions.
|
||||
|
||||
## Important Files
|
||||
|
||||
- [AGENTS.md](./AGENTS.md) repo conventions and agent guidance
|
||||
- [justfile](./justfile) local setup, dev, test, and CI commands
|
||||
- [CODEOWNERS](./CODEOWNERS) code ownership
|
||||
- [.github/workflows/ci.yml](./.github/workflows/ci.yml) CI checks
|
||||
- [.github/ISSUE_TEMPLATE/](./.github/ISSUE_TEMPLATE/) issue templates
|
||||
- [GOVERNANCE.md](./GOVERNANCE.md) project governance
|
||||
- [LICENSE](./LICENSE) license terms
|
||||
|
||||
Project leads should keep this README, [CODEOWNERS](./CODEOWNERS), and the issue templates current. If this repo grows beyond the quick-start flow above, add a `CONTRIBUTING.md` and link it here once it exists.
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1,7 @@
|
||||
# Hermit environment
|
||||
|
||||
This is a [Hermit](https://github.com/cashapp/hermit) bin directory.
|
||||
|
||||
The symlinks in this directory are managed by Hermit and will automatically
|
||||
download and install Hermit itself as well as packages. These packages are
|
||||
local to this environment.
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# This file must be used with "source bin/activate-hermit" from bash or zsh.
|
||||
# You cannot run it directly
|
||||
#
|
||||
# THIS FILE IS GENERATED; DO NOT MODIFY
|
||||
|
||||
if [ "${BASH_SOURCE-}" = "$0" ]; then
|
||||
echo "You must source this script: \$ source $0" >&2
|
||||
exit 33
|
||||
fi
|
||||
|
||||
BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
|
||||
if "${BIN_DIR}/hermit" noop > /dev/null; then
|
||||
eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"
|
||||
|
||||
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
|
||||
hash -r 2>/dev/null
|
||||
fi
|
||||
|
||||
echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
|
||||
fi
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env fish
|
||||
|
||||
# This file must be sourced with "source bin/activate-hermit.fish" from Fish shell.
|
||||
# You cannot run it directly.
|
||||
#
|
||||
# THIS FILE IS GENERATED; DO NOT MODIFY
|
||||
|
||||
if status is-interactive
|
||||
set BIN_DIR (dirname (status --current-filename))
|
||||
|
||||
if "$BIN_DIR/hermit" noop > /dev/null
|
||||
# Source the activation script generated by Hermit
|
||||
"$BIN_DIR/hermit" activate "$BIN_DIR/.." | source
|
||||
|
||||
# Clear the command cache if applicable
|
||||
functions -c > /dev/null 2>&1
|
||||
|
||||
# Display activation message
|
||||
echo "Hermit environment $($HERMIT_ENV/bin/hermit env HERMIT_ENV) activated"
|
||||
end
|
||||
else
|
||||
echo "You must source this script: source $argv[0]" >&2
|
||||
exit 33
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# THIS FILE IS GENERATED; DO NOT MODIFY
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
export HERMIT_USER_HOME=~
|
||||
|
||||
if [ -z "${HERMIT_STATE_DIR}" ]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit"
|
||||
;;
|
||||
Linux)
|
||||
export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}"
|
||||
HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
|
||||
export HERMIT_CHANNEL
|
||||
export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}
|
||||
|
||||
if [ ! -x "${HERMIT_EXE}" ]; then
|
||||
echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
|
||||
INSTALL_SCRIPT="$(mktemp)"
|
||||
# This value must match that of the install script
|
||||
INSTALL_SCRIPT_SHA256="09ed936378857886fd4a7a4878c0f0c7e3d839883f39ca8b4f2f242e3126e1c6"
|
||||
if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then
|
||||
curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}"
|
||||
else
|
||||
# Install script is versioned by its sha256sum value
|
||||
curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}"
|
||||
# Verify install script's sha256sum
|
||||
openssl dgst -sha256 "${INSTALL_SCRIPT}" | \
|
||||
awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \
|
||||
'$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}'
|
||||
fi
|
||||
/bin/bash "${INSTALL_SCRIPT}" 1>&2
|
||||
fi
|
||||
|
||||
exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
github-token-auth {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.just-1.48.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.lefthook-2.1.4.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.pnpm-10.33.0.pkg
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**",
|
||||
"!src-tauri/gen",
|
||||
"!src-tauri/plugins/*/permissions/{schemas,autogenerated}",
|
||||
"!.agents",
|
||||
"!.worktrees",
|
||||
"!.claude/worktrees"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": false
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["src/shared/styles/globals.css"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": { "noImportantStyles": "off" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["src/shared/ui/**", "src/components/ai-elements/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"a11y": {
|
||||
"useSemanticElements": "off",
|
||||
"useFocusableInteractive": "off",
|
||||
"useKeyWithClickEvents": "off",
|
||||
"useAriaPropsForRole": "off",
|
||||
"noRedundantRoles": "off"
|
||||
},
|
||||
"correctness": { "useExhaustiveDependencies": "off" },
|
||||
"suspicious": { "noArrayIndexKey": "off", "noDocumentCookie": "off" },
|
||||
"security": { "noDangerouslySetInnerHtml": "off" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "ghost",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/shared/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/shared/ui",
|
||||
"utils": "@/shared/lib/cn",
|
||||
"ui": "@/shared/ui",
|
||||
"lib": "@/shared/lib"
|
||||
},
|
||||
"registryUrl": "https://block.github.io/ghost/r/registry.json",
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "@ghost/core";
|
||||
|
||||
export default defineConfig({
|
||||
designSystems: [
|
||||
{
|
||||
name: "goose2",
|
||||
registry: "https://block.github.io/ghost/r/registry.json",
|
||||
componentDir: "src/shared/ui",
|
||||
styleEntry: "src/shared/styles/globals.css",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Goose</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,183 @@
|
||||
# Default recipe
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# ── Dev Environment ──────────────────────────────────────────
|
||||
|
||||
# Sync and build the managed local goose checkout used for goose2 integration.
|
||||
goose-sync:
|
||||
GOOSE_DEV_MODE=required ./scripts/ensure-local-goose.sh
|
||||
|
||||
# Install dependencies
|
||||
setup:
|
||||
pnpm install
|
||||
GOOSE_DEV_MODE=required ./scripts/ensure-local-goose.sh
|
||||
cd src-tauri && cargo build
|
||||
|
||||
# ── Build & Check ────────────────────────────────────────────
|
||||
|
||||
# Run all checks (lint, format, typecheck, file sizes)
|
||||
check:
|
||||
pnpm check
|
||||
pnpm typecheck
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
pnpm format
|
||||
cd src-tauri && cargo fmt
|
||||
|
||||
# Check formatting without modifying
|
||||
fmt-check:
|
||||
pnpm exec biome format .
|
||||
cd src-tauri && cargo fmt --check
|
||||
|
||||
# Run clippy on Tauri backend
|
||||
clippy:
|
||||
cd src-tauri && cargo clippy -- -D warnings
|
||||
|
||||
# Build the frontend
|
||||
build:
|
||||
pnpm build
|
||||
|
||||
# Check Tauri Rust formatting
|
||||
tauri-fmt-check:
|
||||
cd src-tauri && cargo fmt --check
|
||||
|
||||
# Check Tauri Rust types
|
||||
tauri-check:
|
||||
cd src-tauri && cargo check
|
||||
|
||||
# Full CI gate
|
||||
ci: check clippy test build tauri-check
|
||||
|
||||
# ── Test ─────────────────────────────────────────────────────
|
||||
|
||||
# Run unit/component tests
|
||||
test:
|
||||
pnpm test
|
||||
|
||||
# Run tests in watch mode
|
||||
test-watch:
|
||||
pnpm test:watch
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
pnpm test:coverage
|
||||
|
||||
# Run E2E smoke tests (builds first)
|
||||
test-e2e:
|
||||
pnpm test:e2e:smoke
|
||||
|
||||
# Run all E2E tests (builds first)
|
||||
test-e2e-all:
|
||||
pnpm test:e2e
|
||||
|
||||
# ── Run ──────────────────────────────────────────────────────
|
||||
|
||||
# Start the desktop app in dev mode
|
||||
dev:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -n "${GOOSE_BIN:-}" ]]; then
|
||||
echo "Using explicitly set GOOSE_BIN: ${GOOSE_BIN}"
|
||||
else
|
||||
LOCAL_GOOSE_BIN="$(./scripts/ensure-local-goose.sh --check-bin)" || {
|
||||
rc=$?
|
||||
if [[ $rc -eq 2 ]]; then
|
||||
echo "❌ Local goose binary is not ready. Run 'just setup' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
exit $rc
|
||||
}
|
||||
if [[ -n "${LOCAL_GOOSE_BIN}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_BIN}"
|
||||
echo "Using local goose binary: ${GOOSE_BIN}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Derive a stable port from the working directory so the same worktree
|
||||
# always gets the same port. This avoids changing TAURI_CONFIG between
|
||||
# runs, which would invalidate Cargo's build cache and trigger a full
|
||||
# Rust rebuild every time.
|
||||
VITE_PORT=$(python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)")
|
||||
export VITE_PORT
|
||||
TAURI_CONFIG="{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}}}"
|
||||
|
||||
# In worktrees, generate a labeled icon so you can tell instances apart
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
GIT_DIR=$(git rev-parse --git-dir)
|
||||
if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then
|
||||
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
|
||||
WORKTREE_LABEL="${BRANCH_NAME##*/}"
|
||||
|
||||
ICON_DIR="$(pwd)/src-tauri/target/dev-icons"
|
||||
mkdir -p "$ICON_DIR"
|
||||
DEV_ICON="$ICON_DIR/icon.icns"
|
||||
|
||||
if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then
|
||||
echo "🌳 Worktree: ${WORKTREE_LABEL}"
|
||||
TAURI_CONFIG=$(python3 -c "import json,sys; a=json.loads(sys.argv[1]); a['bundle']={'icon':['$DEV_ICON']}; print(json.dumps(a))" "$TAURI_CONFIG")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm tauri dev --features app-test-driver --config "$TAURI_CONFIG"
|
||||
|
||||
# Start the desktop app with dev config
|
||||
dev-debug:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -n "${GOOSE_BIN:-}" ]]; then
|
||||
echo "Using explicitly set GOOSE_BIN: ${GOOSE_BIN}"
|
||||
else
|
||||
LOCAL_GOOSE_BIN="$(./scripts/ensure-local-goose.sh --check-bin)" || {
|
||||
rc=$?
|
||||
if [[ $rc -eq 2 ]]; then
|
||||
echo "❌ Local goose binary is not ready. Run 'just setup' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
exit $rc
|
||||
}
|
||||
if [[ -n "${LOCAL_GOOSE_BIN}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_BIN}"
|
||||
echo "Using local goose binary: ${GOOSE_BIN}"
|
||||
fi
|
||||
fi
|
||||
|
||||
VITE_PORT=$(python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)")
|
||||
export VITE_PORT
|
||||
EXTRA_CONFIG="--config {\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}}}"
|
||||
|
||||
# In worktrees, generate a labeled icon so you can tell instances apart
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
GIT_DIR=$(git rev-parse --git-dir)
|
||||
if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then
|
||||
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
|
||||
WORKTREE_LABEL="${BRANCH_NAME##*/}"
|
||||
|
||||
ICON_DIR="$(pwd)/src-tauri/target/dev-icons"
|
||||
mkdir -p "$ICON_DIR"
|
||||
DEV_ICON="$ICON_DIR/icon.icns"
|
||||
|
||||
if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then
|
||||
echo "🌳 Worktree: ${WORKTREE_LABEL}"
|
||||
EXTRA_CONFIG="$EXTRA_CONFIG --config {\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm tauri dev --config src-tauri/tauri.dev.conf.json $EXTRA_CONFIG
|
||||
|
||||
# Start only the frontend dev server
|
||||
dev-frontend:
|
||||
pnpm dev
|
||||
|
||||
# ── Utilities ────────────────────────────────────────────────
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
cd src-tauri && cargo clean
|
||||
rm -rf dist
|
||||
rm -rf node_modules
|
||||
@@ -0,0 +1,22 @@
|
||||
pre-commit:
|
||||
commands:
|
||||
format:
|
||||
run: npx biome format --write . && npx biome check --fix . && git add -u
|
||||
check:
|
||||
run: just check
|
||||
|
||||
pre-push:
|
||||
parallel: true
|
||||
commands:
|
||||
fmt-check:
|
||||
run: just fmt-check
|
||||
clippy:
|
||||
run: just clippy
|
||||
check:
|
||||
run: just check
|
||||
test:
|
||||
run: just test
|
||||
build:
|
||||
run: just build
|
||||
tauri-check:
|
||||
run: just tauri-check
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "goose2",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:file-sizes": "node ./scripts/check-file-sizes.mjs",
|
||||
"check:i18n": "node ./scripts/check-i18n-strings.mjs",
|
||||
"lint": "biome lint .",
|
||||
"check": "biome check . && pnpm check:file-sizes && pnpm check:i18n",
|
||||
"format": "biome format --write .",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "pnpm build && playwright test",
|
||||
"test:e2e:smoke": "pnpm build && playwright test --project=smoke",
|
||||
"test:e2e:personas": "pnpm build && playwright test --project=personas",
|
||||
"test:e2e:skills": "pnpm build && playwright test --project=skills",
|
||||
"test:e2e:drafts": "pnpm build && playwright test --project=drafts",
|
||||
"test:app-e2e": "vitest run --config vitest.app-e2e.config.ts",
|
||||
"test-driver": "tsx tests/app-e2e/lib/test-driver-cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@rive-app/react-webgl2": "^4.27.3",
|
||||
"@streamdown/cjk": "^1.0.3",
|
||||
"@streamdown/code": "^1.1.1",
|
||||
"@streamdown/math": "^1.0.2",
|
||||
"@streamdown/mermaid": "^1.0.2",
|
||||
"@tabler/icons-react": "^3.41.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"ai": "^6.0.142",
|
||||
"ansi-to-react": "^6.2.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"gsap": "^3.14.2",
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"media-chrome": "^4.18.3",
|
||||
"motion": "^12.38.0",
|
||||
"nanoid": "^5.1.7",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.72.0",
|
||||
"react-i18next": "^17.0.2",
|
||||
"react-jsx-parser": "^2.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.8.0",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"recharts": "^3.8.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.0.2",
|
||||
"sonner": "^2.0.7",
|
||||
"split-type": "^0.3.4",
|
||||
"streamdown": "^2.5.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"use-stick-to-bottom": "^1.1.3",
|
||||
"vaul": "^1.1.2",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.9",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.9.0",
|
||||
"vite": "^7.0.4",
|
||||
"vitest": "^3.2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const previewPort = 4173;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
timeout: 60_000,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [
|
||||
["list"],
|
||||
["html", { open: "never", outputFolder: "playwright-report" }],
|
||||
],
|
||||
use: {
|
||||
baseURL: `http://127.0.0.1:${previewPort}`,
|
||||
screenshot: "only-on-failure",
|
||||
trace: "on-first-retry",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "smoke",
|
||||
testMatch: ["**/smoke.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "personas",
|
||||
testMatch: ["**/personas.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skills",
|
||||
testMatch: ["**/skills.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "drafts",
|
||||
testMatch: ["**/drafts.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
// Opt-in reuse only. Reusing arbitrary local processes makes the suite
|
||||
// flaky when another test run or dev server happens to be bound here.
|
||||
command: `python3 -m http.server ${previewPort} -d dist`,
|
||||
cwd: ".",
|
||||
reuseExistingServer: process.env.PLAYWRIGHT_REUSE_SERVER === "1",
|
||||
url: `http://127.0.0.1:${previewPort}`,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", "helpers:pinGitHubActionDigests"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.94.1"
|
||||
profile = "default"
|
||||
@@ -0,0 +1,141 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const DEFAULT_LIMIT = 500;
|
||||
|
||||
// Add narrowly scoped exceptions here with justification
|
||||
const EXCEPTIONS = {
|
||||
"src/features/sidebar/ui/SidebarProjectsSection.tsx": {
|
||||
limit: 560,
|
||||
justification:
|
||||
"Drag-and-drop handlers plus activeProjectId highlight for draft-in-project sessions.",
|
||||
},
|
||||
"src/features/chat/ui/ChatView.tsx": {
|
||||
limit: 535,
|
||||
justification:
|
||||
"ACP prewarm guards, project-aware working dir selection, working context sync, and chat bootstrapping still live together here.",
|
||||
},
|
||||
"src/features/chat/ui/__tests__/ContextPanel.test.tsx": {
|
||||
limit: 550,
|
||||
justification:
|
||||
"Workspace widget integration tests cover branch switching, worktree creation, dirty-state dialogs, and picker interactions.",
|
||||
},
|
||||
"src/features/sidebar/ui/Sidebar.tsx": {
|
||||
limit: 580,
|
||||
justification:
|
||||
"Search-as-you-type filtering and draft-aware sidebar highlight logic.",
|
||||
},
|
||||
"src/app/AppShell.tsx": {
|
||||
limit: 640,
|
||||
justification:
|
||||
"Shell still coordinates ACP session loading, project reassignment, and app-level chat routing.",
|
||||
},
|
||||
"src/features/chat/hooks/useAcpStream.ts": {
|
||||
limit: 580,
|
||||
justification:
|
||||
"ACP replay, streaming, session binding, model-state event handling, and replay timeout are still centralized here.",
|
||||
},
|
||||
"src/features/chat/hooks/__tests__/useAcpStream.test.ts": {
|
||||
limit: 570,
|
||||
justification:
|
||||
"Covers replay buffering, timeout error state, streaming edge cases, and provider identity persistence in one cohesive suite.",
|
||||
},
|
||||
"src/features/chat/stores/__tests__/chatSessionStore.test.ts": {
|
||||
limit: 540,
|
||||
justification:
|
||||
"ACP session overlay regressions currently need one broad integration-style store suite.",
|
||||
},
|
||||
"src/features/chat/stores/chatSessionStore.ts": {
|
||||
limit: 640,
|
||||
justification:
|
||||
"ACP-backed session overlay persistence, draft migration, and sidebar-facing session merge logic live together for now.",
|
||||
},
|
||||
"src-tauri/src/services/acp/manager/dispatcher.rs": {
|
||||
limit: 540,
|
||||
justification:
|
||||
"ACP replay and live-stream event fan-out share one dispatcher with replay event counting for drain stabilisation.",
|
||||
},
|
||||
"src-tauri/src/services/acp/manager.rs": {
|
||||
limit: 630,
|
||||
justification:
|
||||
"ACP manager command dispatch loop — export/import/fork session ext_method dispatch adds boilerplate.",
|
||||
},
|
||||
"src-tauri/src/services/acp/manager/session_ops.rs": {
|
||||
limit: 620,
|
||||
justification:
|
||||
"Session prepare/load/list logic, working-dir updates, wait_for_replay_drain helper with iteration cap, and composite prepared-session reuse remain colocated while ACP session ownership stabilizes.",
|
||||
},
|
||||
};
|
||||
|
||||
// Directories excluded from size checks (imported library code)
|
||||
const EXCLUDED_DIRS = [
|
||||
"src/shared/ui",
|
||||
"src/components/ai-elements",
|
||||
"src/hooks",
|
||||
];
|
||||
|
||||
const DIRS_TO_CHECK = [
|
||||
{ dir: "src/app", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src/features", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src/shared", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src/components", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src/hooks", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src-tauri/src", glob: /\.rs$/ },
|
||||
];
|
||||
|
||||
function countLines(filePath) {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
return content.split("\n").length;
|
||||
}
|
||||
|
||||
function isExcluded(filePath) {
|
||||
const rel = relative(".", filePath);
|
||||
return EXCLUDED_DIRS.some((dir) => rel.startsWith(dir));
|
||||
}
|
||||
|
||||
function walkDir(dir, pattern) {
|
||||
const results = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkDir(fullPath, pattern));
|
||||
} else if (pattern.test(entry.name)) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const violations = [];
|
||||
|
||||
for (const { dir, glob } of DIRS_TO_CHECK) {
|
||||
const files = walkDir(dir, glob);
|
||||
for (const file of files) {
|
||||
if (isExcluded(file)) continue;
|
||||
const rel = relative(".", file);
|
||||
const limit = EXCEPTIONS[rel]?.limit ?? DEFAULT_LIMIT;
|
||||
const lines = countLines(file);
|
||||
if (lines > limit) {
|
||||
violations.push({ file: rel, lines, limit });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Desktop file size check failed:");
|
||||
for (const v of violations) {
|
||||
console.error(` - ${v.file}: ${v.lines} lines (limit ${v.limit})`);
|
||||
}
|
||||
console.error(
|
||||
"\nSplit the file or add a narrowly scoped exception in `scripts/check-file-sizes.mjs`.",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("File size check passed.");
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { extname, join, relative } from "node:path";
|
||||
import ts from "typescript";
|
||||
|
||||
const CHECKED_PATHS = [
|
||||
"src/app/ui",
|
||||
"src/features/agents",
|
||||
"src/features/chat/ui",
|
||||
"src/features/home",
|
||||
"src/features/projects",
|
||||
"src/features/settings",
|
||||
"src/features/skills",
|
||||
"src/features/sidebar",
|
||||
"src/features/status",
|
||||
"src/features/sessions",
|
||||
"src/shared/ui/ai-elements/code-block.tsx",
|
||||
"src/shared/ui/ai-elements/environment-variables.tsx",
|
||||
"src/shared/ui/ai-elements/message.tsx",
|
||||
"src/shared/ui/ai-elements/plan.tsx",
|
||||
"src/shared/ui/ai-elements/snippet.tsx",
|
||||
"src/shared/ui/ai-elements/stack-trace.tsx",
|
||||
"src/shared/ui/ai-elements/terminal.tsx",
|
||||
"src/shared/ui/ai-elements/context.tsx",
|
||||
"src/shared/ui/ai-elements/commit.tsx",
|
||||
];
|
||||
|
||||
const EXCLUDED_PATH_SEGMENTS = ["__tests__"];
|
||||
const EXCLUDED_FILE_MARKERS = [".test.", ".spec."];
|
||||
const CHECKED_EXTENSIONS = new Set([".ts", ".tsx"]);
|
||||
const TEXT_ATTRIBUTE_NAMES = new Set([
|
||||
"aria-label",
|
||||
"title",
|
||||
"placeholder",
|
||||
"alt",
|
||||
]);
|
||||
const TEXT_EXCLUDED_TAGS = new Set(["code", "pre", "kbd"]);
|
||||
const IGNORE_COMMENT = "i18n-check-ignore";
|
||||
|
||||
function walkPath(targetPath) {
|
||||
const entries = [];
|
||||
let statEntries;
|
||||
try {
|
||||
statEntries = readdirSync(targetPath, { withFileTypes: true });
|
||||
} catch {
|
||||
if (CHECKED_EXTENSIONS.has(extname(targetPath))) {
|
||||
return [targetPath];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const entry of statEntries) {
|
||||
const fullPath = join(targetPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
entries.push(...walkPath(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CHECKED_EXTENSIONS.has(extname(entry.name))) {
|
||||
entries.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function isExcluded(filePath) {
|
||||
const rel = relative(".", filePath);
|
||||
const normalizedRel = rel.replace(/\\/g, "/");
|
||||
const pathSegments = normalizedRel.split("/");
|
||||
|
||||
return (
|
||||
EXCLUDED_PATH_SEGMENTS.some((segment) => pathSegments.includes(segment)) ||
|
||||
EXCLUDED_FILE_MARKERS.some((marker) => normalizedRel.includes(marker))
|
||||
);
|
||||
}
|
||||
|
||||
function collapseWhitespace(text) {
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function isProbablyUserFacingText(text) {
|
||||
if (!text) return false;
|
||||
if (!/\p{L}/u.test(text)) return false;
|
||||
if (/^https?:\/\//.test(text)) return false;
|
||||
if (/^[./~@#][\w./-]+$/.test(text)) return false;
|
||||
if (/^[A-Z0-9_:-]+$/.test(text)) return false;
|
||||
if (/^[\w.-]+\.[A-Za-z]{2,}$/.test(text)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLineText(sourceText, sourceFile, lineIndex) {
|
||||
if (lineIndex < 0) return "";
|
||||
|
||||
const lineStarts = sourceFile.getLineStarts();
|
||||
if (lineIndex >= lineStarts.length) return "";
|
||||
|
||||
const lineStart = lineStarts[lineIndex];
|
||||
const lineEnd =
|
||||
lineIndex + 1 < lineStarts.length
|
||||
? lineStarts[lineIndex + 1]
|
||||
: sourceText.length;
|
||||
|
||||
return sourceText.slice(lineStart, lineEnd);
|
||||
}
|
||||
|
||||
function hasIgnoreComment(sourceText, sourceFile, node) {
|
||||
const start = node.getStart(sourceFile);
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(start);
|
||||
|
||||
return [line - 1, line].some((lineIndex) =>
|
||||
getLineText(sourceText, sourceFile, lineIndex).includes(IGNORE_COMMENT),
|
||||
);
|
||||
}
|
||||
|
||||
function getParentTagName(node) {
|
||||
if (ts.isJsxElement(node.parent)) {
|
||||
return node.parent.openingElement.tagName.getText();
|
||||
}
|
||||
|
||||
if (ts.isJsxSelfClosingElement(node.parent)) {
|
||||
return node.parent.tagName.getText();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeJsxText(text) {
|
||||
return collapseWhitespace(text);
|
||||
}
|
||||
|
||||
function extractStringFromExpression(expression) {
|
||||
if (!expression) return null;
|
||||
|
||||
if (ts.isStringLiteralLike(expression)) {
|
||||
return expression.text;
|
||||
}
|
||||
|
||||
if (ts.isNoSubstitutionTemplateLiteral(expression)) {
|
||||
return expression.text;
|
||||
}
|
||||
|
||||
if (ts.isTemplateExpression(expression)) {
|
||||
let text = expression.head.text;
|
||||
for (const span of expression.templateSpans) {
|
||||
text += `{${span.expression.getText()}}${span.literal.text}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatLocation(sourceFile, position) {
|
||||
const { line, character } =
|
||||
sourceFile.getLineAndCharacterOfPosition(position);
|
||||
return `${sourceFile.fileName}:${line + 1}:${character + 1}`;
|
||||
}
|
||||
|
||||
function collectViolations(filePath) {
|
||||
const sourceText = readFileSync(filePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
const violations = [];
|
||||
|
||||
function report(node, kind, text) {
|
||||
const normalizedText = collapseWhitespace(text);
|
||||
if (!isProbablyUserFacingText(normalizedText)) return;
|
||||
if (hasIgnoreComment(sourceText, sourceFile, node)) return;
|
||||
|
||||
violations.push({
|
||||
location: formatLocation(sourceFile, node.getStart(sourceFile)),
|
||||
kind,
|
||||
text: normalizedText,
|
||||
});
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isJsxText(node)) {
|
||||
const tagName = getParentTagName(node);
|
||||
if (tagName && TEXT_EXCLUDED_TAGS.has(tagName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = normalizeJsxText(node.getText(sourceFile));
|
||||
report(node, "jsx-text", text);
|
||||
}
|
||||
|
||||
if (ts.isJsxAttribute(node)) {
|
||||
const attributeName = node.name.text;
|
||||
if (TEXT_ATTRIBUTE_NAMES.has(attributeName) && node.initializer) {
|
||||
if (ts.isStringLiteral(node.initializer)) {
|
||||
report(
|
||||
node.initializer,
|
||||
`prop:${attributeName}`,
|
||||
node.initializer.text,
|
||||
);
|
||||
}
|
||||
|
||||
if (ts.isJsxExpression(node.initializer)) {
|
||||
const text = extractStringFromExpression(node.initializer.expression);
|
||||
if (text) {
|
||||
report(node.initializer, `prop:${attributeName}`, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isJsxExpression(node) && node.expression) {
|
||||
if (ts.isJsxAttribute(node.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = extractStringFromExpression(node.expression);
|
||||
if (text) {
|
||||
report(node, "jsx-expression", text);
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return violations;
|
||||
}
|
||||
|
||||
const files = CHECKED_PATHS.flatMap(walkPath)
|
||||
.filter((filePath) => !isExcluded(filePath))
|
||||
.sort();
|
||||
|
||||
const violations = files.flatMap((filePath) => collectViolations(filePath));
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("i18n string check failed:");
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
` - ${violation.location} [${violation.kind}] ${JSON.stringify(violation.text)}`,
|
||||
);
|
||||
}
|
||||
console.error("");
|
||||
console.error(
|
||||
`Wrap user-facing strings in translations or annotate a narrow exception with "${IGNORE_COMMENT}".`,
|
||||
);
|
||||
console.error(
|
||||
"The current enforcement scope is intentionally limited to app areas already migrated to i18n.",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("i18n string check passed.");
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ensure-local-goose.sh [--print-bin | --check-bin]
|
||||
|
||||
Syncs and builds a dedicated local goose checkout for goose2 development.
|
||||
|
||||
Environment variables:
|
||||
GOOSE_DEV_MODE auto|required (default: auto)
|
||||
GOOSE_DEV_ROOT path to the shared goose2 dev cache root
|
||||
(default: platform cache dir under home)
|
||||
GOOSE_DEV_REPO path to the managed goose checkout
|
||||
(default: $GOOSE_DEV_ROOT/goose)
|
||||
GOOSE_DEV_STAMP_FILE path to the shared build stamp file
|
||||
(default: $GOOSE_DEV_ROOT/stamp.env)
|
||||
GOOSE_DEV_CLONE_URL git clone URL for the managed goose checkout
|
||||
(default: https://github.com/block/goose.git)
|
||||
GOOSE_DEV_REMOTE git remote to sync from (default: origin)
|
||||
GOOSE_DEV_BRANCH preferred branch to use (default: baxen/goose2)
|
||||
GOOSE_DEV_FALLBACK_BRANCH fallback branch when the preferred branch does
|
||||
not exist remotely (default: main)
|
||||
GOOSE_DEV_ALLOW_DIRTY 1 to allow syncing/building a dirty checkout
|
||||
EOF
|
||||
}
|
||||
|
||||
action="build"
|
||||
print_bin=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--print-bin)
|
||||
print_bin=1
|
||||
shift
|
||||
;;
|
||||
--check-bin)
|
||||
action="check"
|
||||
print_bin=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
mode="${GOOSE_DEV_MODE:-auto}"
|
||||
clone_url="${GOOSE_DEV_CLONE_URL:-https://github.com/block/goose.git}"
|
||||
remote="${GOOSE_DEV_REMOTE:-origin}"
|
||||
preferred_branch="${GOOSE_DEV_BRANCH:-baxen/goose2}"
|
||||
fallback_branch="${GOOSE_DEV_FALLBACK_BRANCH:-main}"
|
||||
allow_dirty="${GOOSE_DEV_ALLOW_DIRTY:-0}"
|
||||
|
||||
log() {
|
||||
echo "[goose-dev] $*" >&2
|
||||
}
|
||||
|
||||
fail_or_skip() {
|
||||
local message="$1"
|
||||
if [[ "${mode}" == "required" ]]; then
|
||||
echo "${message}" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "${message}"
|
||||
# In check mode, exit 2 so callers (e.g. just dev) can detect "not ready"
|
||||
# and block instead of silently continuing without a goose binary.
|
||||
if [[ "${action}" == "check" ]]; then
|
||||
exit 2
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
default_goose_dev_root() {
|
||||
if [[ -n "${XDG_CACHE_HOME:-}" ]]; then
|
||||
printf '%s/goose2-dev\n' "${XDG_CACHE_HOME}"
|
||||
return
|
||||
fi
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
printf '%s/Library/Caches/goose2-dev\n' "${HOME}"
|
||||
;;
|
||||
*)
|
||||
printf '%s/.cache/goose2-dev\n' "${HOME}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
goose_dev_root="${GOOSE_DEV_ROOT:-$(default_goose_dev_root)}"
|
||||
goose_repo="${GOOSE_DEV_REPO:-${goose_dev_root}/goose}"
|
||||
stamp_file="${GOOSE_DEV_STAMP_FILE:-${goose_dev_root}/stamp.env}"
|
||||
bin_path="${goose_repo}/target/debug/goose"
|
||||
|
||||
resolve_remote_head() {
|
||||
local branch_name="$1"
|
||||
git -C "${goose_repo}" ls-remote --heads "${remote}" "${branch_name}" 2>/dev/null | awk 'NR == 1 { print $1 }'
|
||||
}
|
||||
|
||||
resolve_branch() {
|
||||
local resolved_branch="${preferred_branch}"
|
||||
local resolved_head
|
||||
resolved_head="$(resolve_remote_head "${resolved_branch}")"
|
||||
|
||||
if [[ -z "${resolved_head}" && "${resolved_branch}" != "${fallback_branch}" ]]; then
|
||||
log "Remote branch ${remote}/${resolved_branch} not found; falling back to ${remote}/${fallback_branch}."
|
||||
resolved_branch="${fallback_branch}"
|
||||
resolved_head="$(resolve_remote_head "${resolved_branch}")"
|
||||
fi
|
||||
|
||||
if [[ -z "${resolved_head}" ]]; then
|
||||
if [[ "${mode}" == "required" ]]; then
|
||||
echo "Could not resolve ${remote}/${resolved_branch} for managed goose checkout at ${goose_repo}." >&2
|
||||
return 1
|
||||
fi
|
||||
log "Could not resolve ${remote}/${resolved_branch} for managed goose checkout at ${goose_repo}."
|
||||
return 2
|
||||
fi
|
||||
|
||||
RESOLVED_BRANCH="${resolved_branch}"
|
||||
RESOLVED_REMOTE_HEAD="${resolved_head}"
|
||||
return 0
|
||||
}
|
||||
|
||||
write_stamp() {
|
||||
local branch_name="$1"
|
||||
local commit_sha="$2"
|
||||
|
||||
mkdir -p "$(dirname "${stamp_file}")"
|
||||
{
|
||||
printf 'STAMP_REPO=%q\n' "${goose_repo}"
|
||||
printf 'STAMP_BRANCH=%q\n' "${branch_name}"
|
||||
printf 'STAMP_COMMIT=%q\n' "${commit_sha}"
|
||||
printf 'STAMP_BIN=%q\n' "${bin_path}"
|
||||
} >"${stamp_file}"
|
||||
}
|
||||
|
||||
ensure_checkout_exists() {
|
||||
if [[ -d "${goose_repo}/.git" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${action}" == "check" ]]; then
|
||||
fail_or_skip "Managed goose checkout not found at ${goose_repo}. Rerun just setup."
|
||||
fi
|
||||
|
||||
log "Cloning managed goose checkout into ${goose_repo}."
|
||||
mkdir -p "$(dirname "${goose_repo}")"
|
||||
git clone "${clone_url}" "${goose_repo}" >/dev/null 2>&1 || {
|
||||
fail_or_skip "Failed to clone managed goose checkout from ${clone_url} into ${goose_repo}."
|
||||
}
|
||||
}
|
||||
|
||||
ensure_checkout_exists
|
||||
|
||||
if [[ "${allow_dirty}" != "1" ]]; then
|
||||
if [[ -n "$(git -C "${goose_repo}" status --porcelain)" ]]; then
|
||||
fail_or_skip "Managed goose checkout at ${goose_repo} is dirty. Use a dedicated checkout or set GOOSE_DEV_ALLOW_DIRTY=1."
|
||||
fi
|
||||
fi
|
||||
|
||||
if resolve_branch; then
|
||||
branch="${RESOLVED_BRANCH}"
|
||||
remote_head="${RESOLVED_REMOTE_HEAD}"
|
||||
else
|
||||
resolve_branch_status=$?
|
||||
case "${resolve_branch_status}" in
|
||||
1)
|
||||
exit 1
|
||||
;;
|
||||
2)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected resolve_branch status: ${resolve_branch_status}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ "${action}" == "check" ]]; then
|
||||
if [[ ! -f "${stamp_file}" ]]; then
|
||||
fail_or_skip "Managed goose checkout is configured, but no local goose build stamp was found. Rerun just setup."
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "${stamp_file}"
|
||||
|
||||
if [[ "${STAMP_REPO:-}" != "${goose_repo}" ]]; then
|
||||
fail_or_skip "Managed goose checkout changed since the last local goose build. Rerun just setup."
|
||||
fi
|
||||
|
||||
if [[ "${STAMP_BRANCH:-}" != "${branch}" ]]; then
|
||||
fail_or_skip "Managed goose branch is now ${branch}, but the local goose build was prepared for ${STAMP_BRANCH:-unknown}. Rerun just setup."
|
||||
fi
|
||||
|
||||
if [[ ! -x "${STAMP_BIN:-}" ]]; then
|
||||
fail_or_skip "Local goose binary was not found at ${STAMP_BIN:-unknown}. Rerun just setup."
|
||||
fi
|
||||
|
||||
local_head="$(git -C "${goose_repo}" rev-parse HEAD)"
|
||||
if [[ "${STAMP_COMMIT:-}" != "${local_head}" ]]; then
|
||||
fail_or_skip "Managed goose checkout changed after the last local build. Rerun just setup."
|
||||
fi
|
||||
|
||||
if [[ "${STAMP_COMMIT:-}" != "${remote_head}" ]]; then
|
||||
fail_or_skip "Managed goose checkout is behind ${remote}/${branch}. Rerun just setup."
|
||||
fi
|
||||
|
||||
if [[ "${print_bin}" == "1" ]]; then
|
||||
printf '%s\n' "${STAMP_BIN}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git -C "${goose_repo}" fetch "${remote}" "${branch}" >/dev/null 2>&1
|
||||
|
||||
remote_ref="refs/remotes/${remote}/${branch}"
|
||||
if ! git -C "${goose_repo}" show-ref --verify --quiet "${remote_ref}"; then
|
||||
fail_or_skip "Fetched ${remote}/${branch}, but ${remote_ref} is not available in ${goose_repo}."
|
||||
fi
|
||||
|
||||
if git -C "${goose_repo}" show-ref --verify --quiet "refs/heads/${branch}"; then
|
||||
git -C "${goose_repo}" checkout "${branch}" >/dev/null 2>&1
|
||||
else
|
||||
git -C "${goose_repo}" checkout -b "${branch}" --track "${remote}/${branch}" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Reset to the remote head. This is a managed build-only checkout, so we
|
||||
# always want to match the remote exactly. A plain `pull --ff-only` would
|
||||
# fail when the remote branch has been force-pushed (rebased/amended).
|
||||
git -C "${goose_repo}" reset --hard "${remote}/${branch}" >/dev/null 2>&1
|
||||
|
||||
log "Building goose from ${goose_repo} on ${branch}."
|
||||
(
|
||||
cd "${goose_repo}"
|
||||
cargo build -p goose-cli --bin goose
|
||||
)
|
||||
|
||||
if [[ -n "$(git -C "${goose_repo}" status --porcelain -- Cargo.lock)" ]]; then
|
||||
# Cargo may refresh the lockfile while compiling a freshly synced checkout.
|
||||
# This managed repo is only a build source for goose2, so restore the tracked
|
||||
# lockfile to keep the checkout clean for later preflight checks.
|
||||
git -C "${goose_repo}" checkout -- Cargo.lock
|
||||
fi
|
||||
|
||||
if [[ ! -x "${bin_path}" ]]; then
|
||||
echo "Expected goose binary at ${bin_path}, but it was not built successfully." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_stamp "${branch}" "$(git -C "${goose_repo}" rev-parse HEAD)"
|
||||
|
||||
log "Local goose binary ready at ${bin_path}."
|
||||
if [[ "${print_bin}" == "1" ]]; then
|
||||
printf '%s\n' "${bin_path}"
|
||||
fi
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env swift
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
// Generate a dev icon with worktree name badge
|
||||
// Usage: generate-dev-icon.swift <input-icns> <output-icns> <label>
|
||||
|
||||
guard CommandLine.arguments.count == 4 else {
|
||||
fputs("Usage: \(CommandLine.arguments[0]) <input-icns> <output-icns> <label>\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let inputPath = CommandLine.arguments[1]
|
||||
let outputPath = CommandLine.arguments[2]
|
||||
let label = CommandLine.arguments[3]
|
||||
|
||||
// Load the icns file
|
||||
guard let iconImage = NSImage(contentsOfFile: inputPath) else {
|
||||
fputs("Failed to load image: \(inputPath)\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Get the largest representation for best quality
|
||||
guard let rep = iconImage.representations.max(by: { $0.pixelsWide < $1.pixelsWide }) else {
|
||||
fputs("No image representations found\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let size = NSSize(width: rep.pixelsWide, height: rep.pixelsHigh)
|
||||
|
||||
// Create a new image with the badge
|
||||
let newImage = NSImage(size: size)
|
||||
newImage.lockFocus()
|
||||
|
||||
// Draw the original icon
|
||||
iconImage.draw(in: NSRect(origin: .zero, size: size))
|
||||
|
||||
// Configure badge
|
||||
let singleLineHeight = size.height * 0.22
|
||||
let padding = size.width * 0.03
|
||||
let maxBadgeWidth = size.width * 0.9
|
||||
|
||||
// Text attributes - calculate font size to fit
|
||||
let maxFontSize = singleLineHeight * 0.65
|
||||
var fontSize = maxFontSize
|
||||
var attributes: [NSAttributedString.Key: Any]
|
||||
|
||||
// Helper to wrap text on `-` characters
|
||||
func wrapText(_ text: String, maxWidth: CGFloat, attributes: [NSAttributedString.Key: Any]) -> [String] {
|
||||
let singleLineSize = (text as NSString).size(withAttributes: attributes)
|
||||
if singleLineSize.width <= maxWidth {
|
||||
return [text]
|
||||
}
|
||||
|
||||
// Split on `-` and try to form lines
|
||||
let parts = text.components(separatedBy: "-")
|
||||
if parts.count == 1 {
|
||||
return [text] // No `-` to wrap on
|
||||
}
|
||||
|
||||
var lines: [String] = []
|
||||
var currentLine = ""
|
||||
|
||||
for (index, part) in parts.enumerated() {
|
||||
let separator = index == 0 ? "" : "-"
|
||||
let testLine = currentLine.isEmpty ? part : currentLine + separator + part
|
||||
let testSize = (testLine as NSString).size(withAttributes: attributes)
|
||||
|
||||
if testSize.width <= maxWidth || currentLine.isEmpty {
|
||||
currentLine = testLine
|
||||
} else {
|
||||
lines.append(currentLine)
|
||||
currentLine = part
|
||||
}
|
||||
}
|
||||
if !currentLine.isEmpty {
|
||||
lines.append(currentLine)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// Find font size that fits (allowing up to 2 lines)
|
||||
var lines: [String] = []
|
||||
repeat {
|
||||
attributes = [
|
||||
.font: NSFont.systemFont(ofSize: fontSize, weight: .bold),
|
||||
.foregroundColor: NSColor.white
|
||||
]
|
||||
lines = wrapText(label, maxWidth: maxBadgeWidth - padding * 4, attributes: attributes)
|
||||
fontSize -= 1
|
||||
} while lines.count > 2 && fontSize > 8
|
||||
|
||||
// Calculate text dimensions using typographic metrics
|
||||
let lineHeight = (lines.first! as NSString).size(withAttributes: attributes).height
|
||||
let textHeight = lineHeight * CGFloat(lines.count)
|
||||
let maxLineWidth = lines.map { ($0 as NSString).size(withAttributes: attributes).width }.max() ?? 0
|
||||
|
||||
// Badge dimensions based on text
|
||||
let badgeHeight = textHeight + padding * 2
|
||||
let cornerRadius = badgeHeight * 0.2
|
||||
let badgeWidth = maxLineWidth + padding * 4
|
||||
let badgeX = (size.width - badgeWidth) / 2
|
||||
let badgeY = size.height - badgeHeight - padding - size.height * 0.05
|
||||
|
||||
// Draw badge background (light blue semi-transparent)
|
||||
let badgePath = NSBezierPath(roundedRect: NSRect(x: badgeX, y: badgeY, width: badgeWidth, height: badgeHeight),
|
||||
xRadius: cornerRadius, yRadius: cornerRadius)
|
||||
NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: 0.85).setFill()
|
||||
badgePath.fill()
|
||||
|
||||
// Draw text centered in badge (multiple lines, bottom to top)
|
||||
for (index, line) in lines.reversed().enumerated() {
|
||||
let lineSize = (line as NSString).size(withAttributes: attributes)
|
||||
let textX = badgeX + (badgeWidth - lineSize.width) / 2
|
||||
let textY = badgeY + padding + lineHeight * CGFloat(index)
|
||||
(line as NSString).draw(at: NSPoint(x: textX, y: textY), withAttributes: attributes)
|
||||
}
|
||||
|
||||
newImage.unlockFocus()
|
||||
|
||||
// Convert to icns format
|
||||
// First, create PNG data at multiple sizes for icns
|
||||
guard let tiffData = newImage.tiffRepresentation,
|
||||
let bitmapRep = NSBitmapImageRep(data: tiffData) else {
|
||||
fputs("Failed to create bitmap representation\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// For simplicity, we'll create a PNG and then use iconutil if available,
|
||||
// or just save as PNG for the icon (Tauri can use PNG)
|
||||
guard let pngData = bitmapRep.representation(using: .png, properties: [:]) else {
|
||||
fputs("Failed to create PNG data\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// If output is .icns, we need to create an iconset and use iconutil
|
||||
if outputPath.hasSuffix(".icns") {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
let iconsetPath = tempDir.appendingPathComponent("goose-dev.iconset")
|
||||
|
||||
// Remove existing iconset if present
|
||||
try? FileManager.default.removeItem(at: iconsetPath)
|
||||
try! FileManager.default.createDirectory(at: iconsetPath, withIntermediateDirectories: true)
|
||||
|
||||
// Generate all required sizes for iconset
|
||||
let sizes: [(name: String, size: Int)] = [
|
||||
("icon_16x16", 16),
|
||||
("icon_16x16@2x", 32),
|
||||
("icon_32x32", 32),
|
||||
("icon_32x32@2x", 64),
|
||||
("icon_128x128", 128),
|
||||
("icon_128x128@2x", 256),
|
||||
("icon_256x256", 256),
|
||||
("icon_256x256@2x", 512),
|
||||
("icon_512x512", 512),
|
||||
("icon_512x512@2x", 1024)
|
||||
]
|
||||
|
||||
for (name, targetSize) in sizes {
|
||||
let resizedImage = NSImage(size: NSSize(width: targetSize, height: targetSize))
|
||||
resizedImage.lockFocus()
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
newImage.draw(in: NSRect(x: 0, y: 0, width: targetSize, height: targetSize))
|
||||
resizedImage.unlockFocus()
|
||||
|
||||
guard let resizedTiff = resizedImage.tiffRepresentation,
|
||||
let resizedBitmap = NSBitmapImageRep(data: resizedTiff),
|
||||
let resizedPng = resizedBitmap.representation(using: .png, properties: [:]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let filePath = iconsetPath.appendingPathComponent("\(name).png")
|
||||
try! resizedPng.write(to: filePath)
|
||||
}
|
||||
|
||||
// Use iconutil to create icns
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil")
|
||||
process.arguments = ["-c", "icns", iconsetPath.path, "-o", outputPath]
|
||||
try! process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
// Cleanup
|
||||
try? FileManager.default.removeItem(at: iconsetPath)
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
fputs("iconutil failed\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
} else {
|
||||
// Just save as PNG
|
||||
try! pngData.write(to: URL(fileURLWithPath: outputPath))
|
||||
}
|
||||
|
||||
print("Generated: \(outputPath)")
|
||||
@@ -0,0 +1,58 @@
|
||||
[package]
|
||||
name = "goose2"
|
||||
version = "0.1.0"
|
||||
description = "Goose desktop app"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "Goose"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "goose2_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["protocol-asset"] }
|
||||
tauri-plugin-app-test-driver = { path = "plugins/app-test-driver" }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-dialog = ">=2,<2.7"
|
||||
tauri-plugin-window-state = "2"
|
||||
tauri-plugin-log = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
dirs = "6.0.0"
|
||||
log = "0.4.29"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde_yaml = "0.9"
|
||||
etcetera = "0.8"
|
||||
futures = "0.3"
|
||||
tokio-stream = "0.1"
|
||||
tokio-util = { version = "0.7", features = ["compat", "rt"] }
|
||||
async-trait = "0.1"
|
||||
agent-client-protocol = { version = "0.10.4", features = ["unstable_session_fork"] }
|
||||
tokio-tungstenite = "0.21.0"
|
||||
acp-client = { git = "https://github.com/block/builderbot", rev = "db184d20cb48e0c90bbd3fea4a4a871fc9d8a6ad" }
|
||||
doctor = { git = "https://github.com/block/builderbot", rev = "8e1c3ec145edc0df5f04b4427cfd758378036862" }
|
||||
ignore = "0.4.25"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
keyring = { version = "3", features = ["apple-native"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
keyring = { version = "3", features = ["windows-native"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
keyring = { version = "3", features = ["linux-native-sync-persistent", "crypto-rust"] }
|
||||
|
||||
[features]
|
||||
app-test-driver = []
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-close",
|
||||
"opener:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
{ "path": "$HOME/**" },
|
||||
{ "path": "$HOME/.goose/**" },
|
||||
{ "path": "$HOME/.goose/artifacts/**" },
|
||||
{ "path": "$TEMP/**" },
|
||||
{ "path": "/Volumes/**" },
|
||||
{ "path": "/mnt/**" },
|
||||
{ "path": "/workspace/**" },
|
||||
{ "path": "/workspaces/**" },
|
||||
{ "path": "/opt/**" },
|
||||
{ "path": "/srv/**" },
|
||||
{ "path": "*:/**" }
|
||||
]
|
||||
},
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"app-test-driver:default",
|
||||
"core:webview:allow-set-webview-zoom"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$HOME/.goose/artifacts/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","app-test-driver:default","core:webview:allow-set-webview-zoom"]}}
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 779 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |