cleanup: remove current goose2 code (#9368)
@@ -1,380 +0,0 @@
|
||||
# ACP First Spike: Configured Extensions List
|
||||
|
||||
## Purpose
|
||||
|
||||
Use configured extensions as the first `ui/desktop` REST-to-ACP migration slice.
|
||||
|
||||
This spike is intentionally smaller than chat or sessions. It proves that desktop can talk directly to `/acp` through `goosed` during migration, without depending on ACP session ID behavior or chat streaming semantics.
|
||||
|
||||
## Scope
|
||||
|
||||
Migrate the read-only configured extensions list behind a feature flag.
|
||||
|
||||
```text
|
||||
Current REST:
|
||||
GET /config/extensions
|
||||
|
||||
Target ACP:
|
||||
_goose/config/extensions
|
||||
```
|
||||
|
||||
Do not migrate add/update/delete in this first slice. Those can remain REST until the read path and ACP client plumbing are proven.
|
||||
|
||||
## Why This Slice
|
||||
|
||||
- No session ID dependency.
|
||||
- No chat streaming or prompt lifecycle.
|
||||
- No tool approval.
|
||||
- No message-shape conversion.
|
||||
- Exercises direct renderer WebSocket to `/acp`.
|
||||
- Exercises ACP initialization and custom `_goose/*` request dispatch.
|
||||
- Easy to compare REST and ACP responses.
|
||||
|
||||
## Relevant Existing Code
|
||||
|
||||
Desktop REST read path:
|
||||
|
||||
- `ui/desktop/src/components/ConfigContext.tsx`
|
||||
- imports `getExtensions as apiGetExtensions`
|
||||
- `refreshExtensions()` calls `apiGetExtensions()`
|
||||
- initial provider load also calls `apiGetExtensions()`
|
||||
|
||||
Desktop UI consumer:
|
||||
|
||||
- `ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx`
|
||||
- consumes `extensionsList` from `useConfig()`
|
||||
- calls `getExtensions(true)` to refresh
|
||||
|
||||
REST backend:
|
||||
|
||||
- `crates/goose-server/src/routes/config_management.rs`
|
||||
- `GET /config/extensions`
|
||||
- `POST /config/extensions`
|
||||
- `DELETE /config/extensions/{name}`
|
||||
|
||||
ACP backend:
|
||||
|
||||
- `crates/goose/src/acp/server/extensions.rs`
|
||||
- `_goose/config/extensions`
|
||||
- `_goose/config/extensions/add`
|
||||
- `_goose/config/extensions/remove`
|
||||
- `_goose/config/extensions/toggle`
|
||||
|
||||
ACP client reference:
|
||||
|
||||
- `ui/goose2/src/shared/api/createWebSocketStream.ts`
|
||||
- `ui/goose2/src/shared/api/acpConnection.ts`
|
||||
- `ui/goose2/src/shared/api/acpApi.ts`
|
||||
|
||||
Treat `ui/goose2` as a reference only. Do not share runtime code with it because `ui/goose2` is expected to move out of this repo.
|
||||
|
||||
## Current Gap Found
|
||||
|
||||
REST `GET /config/extensions` filters hidden extensions:
|
||||
|
||||
```rust
|
||||
goose::config::get_all_extensions()
|
||||
.into_iter()
|
||||
.filter(|ext| !goose::agents::extension_manager::is_hidden_extension(&ext.config.name()))
|
||||
```
|
||||
|
||||
ACP `_goose/config/extensions` currently calls `crate::config::extensions::get_all_extensions()` and does not apply the same hidden-extension filter.
|
||||
|
||||
Recommendation: fix ACP to match REST before enabling this feature flag. The goal is to prove the migration path without introducing UI-visible behavior differences.
|
||||
|
||||
## Backend Plan
|
||||
|
||||
## Step-By-Step Review Plan
|
||||
|
||||
Work in small reviewable slices. After each step, stop and review before moving to the next one.
|
||||
|
||||
### Step 1: Inspect router/auth shape
|
||||
|
||||
Goal: confirm the exact `goosed` router composition and ACP transport shape before editing.
|
||||
|
||||
Review points:
|
||||
|
||||
- where REST `X-Secret-Key` middleware is applied
|
||||
- whether ACP can be mounted as a separate branch
|
||||
- whether `goose::acp::transport::create_router` causes route collisions
|
||||
- where ACP token auth should live
|
||||
- whether Cargo dependencies already allow `goose-server` to call ACP router code
|
||||
|
||||
Expected outcome: a precise patch plan for backend mounting.
|
||||
|
||||
### Step 2: Add ACP-only router helper
|
||||
|
||||
Goal: avoid route collisions by exposing only `/acp` routes for embedding in `goosed`.
|
||||
|
||||
Likely file:
|
||||
|
||||
- `crates/goose/src/acp/transport/mod.rs`
|
||||
|
||||
Reason: existing `create_router(...)` includes `/health`, `/status`, and MCP app proxy routes. `goosed` already owns some of those routes.
|
||||
|
||||
Expected outcome: a helper such as `create_acp_router(...)` or equivalent that only mounts:
|
||||
|
||||
```text
|
||||
/acp POST
|
||||
/acp GET
|
||||
/acp DELETE
|
||||
```
|
||||
|
||||
### Step 3: Mount `/acp` in `goosed`
|
||||
|
||||
Goal: serve REST and ACP from the same `goosed` process during migration.
|
||||
|
||||
Likely files:
|
||||
|
||||
- `crates/goose-server/src/commands/agent.rs`
|
||||
- maybe `crates/goose-server/src/routes/mod.rs`
|
||||
|
||||
Expected shape:
|
||||
|
||||
```text
|
||||
goosed
|
||||
REST routes protected by X-Secret-Key
|
||||
/acp protected by ACP token auth
|
||||
```
|
||||
|
||||
### Step 4: Expose ACP URL/token to renderer
|
||||
|
||||
Goal: let renderer connect directly to `/acp` over WebSocket.
|
||||
|
||||
Likely files:
|
||||
|
||||
- `ui/desktop/src/main.ts`
|
||||
- `ui/desktop/src/preload.ts`
|
||||
- related Electron type declarations if present
|
||||
|
||||
Expected renderer-facing value:
|
||||
|
||||
```text
|
||||
ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
|
||||
```
|
||||
|
||||
### Step 5: Add minimal desktop ACP client
|
||||
|
||||
Goal: create enough client plumbing to initialize ACP and call one custom method.
|
||||
|
||||
Suggested files:
|
||||
|
||||
```text
|
||||
ui/desktop/src/acp/createWebSocketStream.ts
|
||||
ui/desktop/src/acp/acpConnection.ts
|
||||
ui/desktop/src/acp/acpApi.ts
|
||||
```
|
||||
|
||||
Reference, not shared dependency:
|
||||
|
||||
- `ui/goose2/src/shared/api/createWebSocketStream.ts`
|
||||
- `ui/goose2/src/shared/api/acpConnection.ts`
|
||||
- `ui/goose2/src/shared/api/acpApi.ts`
|
||||
|
||||
### Step 6: Migrate configured extensions read path behind a flag
|
||||
|
||||
Goal: call `_goose/config/extensions` for read-only extension listing when enabled.
|
||||
|
||||
Primary integration file:
|
||||
|
||||
- `ui/desktop/src/components/ConfigContext.tsx`
|
||||
|
||||
Keep writes on REST in this slice:
|
||||
|
||||
- add extension
|
||||
- remove extension
|
||||
- toggle extension
|
||||
- bundled extension sync/prune
|
||||
|
||||
### Step 7: Validate parity
|
||||
|
||||
Goal: prove ACP and REST return equivalent visible extension data.
|
||||
|
||||
Validation checklist is below.
|
||||
|
||||
## Backend Details
|
||||
|
||||
### Mount `/acp` in `goosed`
|
||||
|
||||
Add the ACP Axum router to `goosed agent`.
|
||||
|
||||
Likely files:
|
||||
|
||||
- `crates/goose-server/src/commands/agent.rs`
|
||||
- `crates/goose-server/src/routes/mod.rs`
|
||||
|
||||
Use existing ACP pieces:
|
||||
|
||||
```rust
|
||||
goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig}
|
||||
goose::acp::transport::create_router
|
||||
```
|
||||
|
||||
Keep REST and ACP as separate route branches.
|
||||
|
||||
Watch for route collisions:
|
||||
|
||||
- `/status`
|
||||
- `/mcp-app-proxy`
|
||||
- `/mcp-app-guest`
|
||||
|
||||
### Add ACP token auth
|
||||
|
||||
The renderer should connect through direct WebSocket:
|
||||
|
||||
```text
|
||||
ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
|
||||
```
|
||||
|
||||
Do not put `/acp` behind the REST `X-Secret-Key` middleware because browser WebSocket cannot set arbitrary headers.
|
||||
|
||||
Guardrails:
|
||||
|
||||
- Use a random ACP token for the `goosed` process.
|
||||
- Prefer an ACP-specific token over reusing the raw REST `X-Secret-Key`.
|
||||
- Never log full `/acp?token=...` URLs.
|
||||
- Accept ACP token auth only for the local desktop backend.
|
||||
- Keep REST `X-Secret-Key` for REST routes during migration.
|
||||
|
||||
### Fix `_goose/config/extensions` parity
|
||||
|
||||
Update ACP `_goose/config/extensions` to match REST behavior:
|
||||
|
||||
- filter hidden extensions
|
||||
- preserve warnings
|
||||
- preserve the response shape needed by desktop
|
||||
|
||||
The ACP response currently injects a `config_key` field. Verify the desktop shape expected by `ExtensionEntry` and normalize on the client if needed.
|
||||
|
||||
## Desktop Plan
|
||||
|
||||
### Add ACP client files
|
||||
|
||||
Suggested files:
|
||||
|
||||
```text
|
||||
ui/desktop/src/acp/createWebSocketStream.ts
|
||||
ui/desktop/src/acp/acpConnection.ts
|
||||
ui/desktop/src/acp/acpApi.ts
|
||||
```
|
||||
|
||||
Base these on `ui/goose2`, but adapt to Electron.
|
||||
|
||||
Differences from `ui/goose2`:
|
||||
|
||||
- URL lookup should use Electron, not Tauri.
|
||||
- Build ACP URL from the existing `goosed` host.
|
||||
- Append the ACP token.
|
||||
- Initialize ACP once and reconnect on close.
|
||||
|
||||
Example URL shape:
|
||||
|
||||
```ts
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
const acpUrl = baseUrl.replace(/^http/, 'ws') + '/acp?token=' + encodeURIComponent(token);
|
||||
```
|
||||
|
||||
If `goosed` is running HTTPS, this becomes `wss://.../acp?...`.
|
||||
|
||||
### Add ACP extensions API wrapper
|
||||
|
||||
Add a wrapper such as:
|
||||
|
||||
```ts
|
||||
getConfigExtensionsViaAcp(): Promise<ExtensionResponse>
|
||||
```
|
||||
|
||||
It should call:
|
||||
|
||||
```text
|
||||
_goose/config/extensions
|
||||
```
|
||||
|
||||
Normalize the response to the current desktop `ExtensionResponse` shape:
|
||||
|
||||
```ts
|
||||
type ExtensionResponse = {
|
||||
extensions: ExtensionEntry[];
|
||||
warnings: string[];
|
||||
};
|
||||
```
|
||||
|
||||
### Add a feature flag
|
||||
|
||||
Suggested name:
|
||||
|
||||
```text
|
||||
acpConfigExtensions
|
||||
```
|
||||
|
||||
The exact flag mechanism should follow existing desktop feature-flag conventions if present. If there is no suitable framework, use a local env/config gate for the spike.
|
||||
|
||||
### Route read path through ACP only when enabled
|
||||
|
||||
Primary integration file:
|
||||
|
||||
- `ui/desktop/src/components/ConfigContext.tsx`
|
||||
|
||||
Candidate call sites:
|
||||
|
||||
- `refreshExtensions()`
|
||||
- initial load effect that fetches extensions after bundled sync
|
||||
|
||||
Keep these mutation paths on REST for the first spike:
|
||||
|
||||
- `addExtension`
|
||||
- `removeExtension`
|
||||
- `toggleExtension`
|
||||
- bundled extension sync/prune
|
||||
|
||||
After REST mutations complete, refresh can use ACP when the flag is enabled.
|
||||
|
||||
### Add fallback behavior
|
||||
|
||||
For the spike, if ACP connection or `_goose/config/extensions` fails, log the error and fall back to REST.
|
||||
|
||||
This fallback is only for the spike. Once a feature area is fully migrated, the matching REST endpoint should be removed.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Compare REST and ACP for the same local config:
|
||||
|
||||
- same visible extension count
|
||||
- same names
|
||||
- same `enabled` values
|
||||
- same extension types (`builtin`, `stdio`, `streamable_http`, etc.)
|
||||
- same warnings
|
||||
- hidden extensions do not appear
|
||||
- settings/extensions UI renders correctly
|
||||
- add/update/delete still work through REST and refresh the list
|
||||
- failed ACP connection falls back to REST while the spike flag is enabled
|
||||
|
||||
## Removal Rule
|
||||
|
||||
Do not remove `GET /config/extensions` after this first read-only spike if writes still use REST sync logic that depends on the endpoint.
|
||||
|
||||
Remove the REST extension endpoints only after the full extension config surface has migrated:
|
||||
|
||||
```text
|
||||
GET /config/extensions
|
||||
POST /config/extensions
|
||||
DELETE /config/extensions/{name}
|
||||
```
|
||||
|
||||
Corresponding ACP methods:
|
||||
|
||||
```text
|
||||
_goose/config/extensions
|
||||
_goose/config/extensions/add
|
||||
_goose/config/extensions/remove
|
||||
_goose/config/extensions/toggle
|
||||
```
|
||||
|
||||
## Done Criteria
|
||||
|
||||
- `goosed` exposes token-authenticated `/acp`.
|
||||
- `ui/desktop` can open a direct renderer WebSocket to `/acp`.
|
||||
- ACP initialize succeeds.
|
||||
- `_goose/config/extensions` returns REST-equivalent visible extension data.
|
||||
- Feature flag can switch extension list reads between REST and ACP.
|
||||
- Existing extension settings UI behaves the same under the ACP read path.
|
||||
@@ -1,707 +0,0 @@
|
||||
# Desktop ACP Migration Spike
|
||||
|
||||
## Goal
|
||||
|
||||
`ui/desktop` currently talks to `goosed agent` through REST/OpenAPI. We want to migrate the desktop app to talk to Goose through ACP, while keeping the migration gradual and avoiding a large bundle-size increase.
|
||||
|
||||
The preferred migration direction is:
|
||||
|
||||
- Keep bundling only `goosed` during migration to avoid shipping both large binaries.
|
||||
- Add ACP serving at `/acp` inside `goosed agent` as a temporary bridge.
|
||||
- Let `ui/desktop` talk directly to `/acp` for migrated surfaces.
|
||||
- Keep existing REST routes only for unmigrated areas.
|
||||
- Once all REST behavior has moved to ACP, stop bundling/running `goosed` and switch desktop to the existing `goose serve` ACP server.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
### Desktop backend
|
||||
|
||||
`goosed` is an Axum HTTP server.
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `crates/goose-server/src/main.rs`
|
||||
- `crates/goose-server/src/commands/agent.rs`
|
||||
- `crates/goose-server/src/routes/mod.rs`
|
||||
- `ui/desktop/src/goosed.ts`
|
||||
|
||||
`ui/desktop/src/goosed.ts` finds and spawns the bundled `goosed` binary:
|
||||
|
||||
```ts
|
||||
const spawnCommand = goosedPath;
|
||||
const spawnArgs = ['agent'];
|
||||
```
|
||||
|
||||
The renderer configures the generated OpenAPI client against the `goosed` URL in `ui/desktop/src/renderer.tsx`.
|
||||
|
||||
### Desktop REST usage
|
||||
|
||||
`ui/desktop` imports generated API methods from `ui/desktop/src/api`.
|
||||
|
||||
Important chat/session paths today:
|
||||
|
||||
- `ui/desktop/src/sessions.ts`
|
||||
- `startAgent` creates sessions.
|
||||
- `ui/desktop/src/hooks/useChatStream.ts`
|
||||
- `resumeAgent`
|
||||
- `sessionReply`
|
||||
- `sessionCancel`
|
||||
- `getSession`
|
||||
- `updateFromSession`
|
||||
- `ui/desktop/src/hooks/useSessionEvents.ts`
|
||||
- opens `GET /sessions/{id}/events` as SSE.
|
||||
|
||||
The current REST streaming model is Goose-specific:
|
||||
|
||||
- `POST /sessions/{id}/reply`
|
||||
- `GET /sessions/{id}/events`
|
||||
- event types such as `Message`, `Finish`, `Error`, `Notification`, `ActiveRequests`
|
||||
- request routing through `request_id` / `chat_request_id`
|
||||
|
||||
## Existing ACP Implementation
|
||||
|
||||
Goose already has ACP support.
|
||||
|
||||
Relevant files:
|
||||
|
||||
- ACP agent implementation: `crates/goose/src/acp/server.rs`
|
||||
- ACP transport: `crates/goose/src/acp/transport/mod.rs`
|
||||
- ACP custom methods: `crates/goose/src/acp/server/custom_dispatch.rs`
|
||||
- Custom request types: `crates/goose-sdk/src/custom_requests.rs`
|
||||
- CLI entry points: `crates/goose-cli/src/cli.rs`
|
||||
|
||||
There are two existing ACP modes:
|
||||
|
||||
### `goose acp`
|
||||
|
||||
Runs ACP over stdio:
|
||||
|
||||
```rust
|
||||
Some(Command::Acp { builtins }) => goose::acp::server::run(builtins).await
|
||||
```
|
||||
|
||||
This is standard ACP, but in Electron it would require the main process to own stdio and bridge to the renderer.
|
||||
|
||||
### `goose serve`
|
||||
|
||||
Runs ACP over HTTP/SSE/WebSocket:
|
||||
|
||||
```rust
|
||||
Some(Command::Serve { host, port, builtins }) => handle_serve_command(host, port, builtins).await
|
||||
```
|
||||
|
||||
The transport router registers:
|
||||
|
||||
```rust
|
||||
/health
|
||||
/status
|
||||
/acp POST
|
||||
/acp GET
|
||||
/acp DELETE
|
||||
```
|
||||
|
||||
`GET /acp` upgrades to WebSocket when requested. Otherwise it behaves as SSE. `POST /acp` accepts JSON-RPC messages.
|
||||
|
||||
## Bundle Size Finding
|
||||
|
||||
Local release binary sizes:
|
||||
|
||||
```text
|
||||
target/release/goose 230M
|
||||
target/release/goosed 218M
|
||||
```
|
||||
|
||||
Bundling both would add roughly:
|
||||
|
||||
```text
|
||||
goosed + goose = 448M uncompressed
|
||||
```
|
||||
|
||||
`ui/desktop/src/bin` currently contains both binaries locally:
|
||||
|
||||
```text
|
||||
ui/desktop/src/bin/goose 230M
|
||||
ui/desktop/src/bin/goosed 218M
|
||||
```
|
||||
|
||||
This is too large as a long-term plan.
|
||||
|
||||
## Recommended Backend Strategy
|
||||
|
||||
Do not bundle both `goose` and `goosed` for production migration. The temporary bridge is to mount `/acp` into `goosed`; the final backend is `goose serve`.
|
||||
|
||||
Migration backend:
|
||||
|
||||
1. Add ACP serving to `goosed agent`.
|
||||
2. Keep REST routes available on the same process during migration.
|
||||
3. Mount `/acp` in the existing `goosed` Axum app.
|
||||
4. Gradually migrate desktop feature areas from REST to ACP.
|
||||
5. Remove each relevant REST endpoint once its feature area is migrated.
|
||||
|
||||
Migration shape:
|
||||
|
||||
```text
|
||||
ui/desktop renderer
|
||||
-> http(s)://127.0.0.1:<port>/acp
|
||||
goosed process
|
||||
existing REST routes temporarily
|
||||
ACP /acp
|
||||
```
|
||||
|
||||
This keeps the backend bundle around current `goosed` size plus a small ACP wiring delta, instead of adding a second 230M binary.
|
||||
|
||||
Final backend:
|
||||
|
||||
```text
|
||||
ui/desktop renderer
|
||||
-> ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
|
||||
goose serve
|
||||
standard ACP methods
|
||||
_goose/* custom ACP methods
|
||||
```
|
||||
|
||||
In the final state `goosed` is not bundled or spawned by the desktop app.
|
||||
|
||||
Before deleting the `goosed` bridge, verify that the final `goose serve` path has the same
|
||||
desktop-specific ACP behavior that the bridge depended on during migration. In particular:
|
||||
|
||||
- config and data directories match the desktop app's expected Goose paths
|
||||
- builtin extension setup matches what desktop needs
|
||||
- ACP initialization uses the correct desktop platform identity
|
||||
- any state initialized today by `goosed` startup is either no longer needed or is initialized by
|
||||
the final desktop `goose serve` launch path
|
||||
|
||||
## Migration Bridge vs Final Server
|
||||
|
||||
Mounting `/acp` in `goosed` should be treated as a bridge, not the destination.
|
||||
|
||||
```text
|
||||
During migration:
|
||||
ui/desktop
|
||||
-> REST for unmigrated features
|
||||
-> /acp for migrated features
|
||||
|
||||
bundled backend:
|
||||
goosed
|
||||
REST routes
|
||||
temporary /acp route
|
||||
```
|
||||
|
||||
```text
|
||||
After migration:
|
||||
ui/desktop
|
||||
-> /acp only
|
||||
|
||||
bundled backend:
|
||||
goose serve
|
||||
standard ACP
|
||||
_goose/* custom ACP methods
|
||||
```
|
||||
|
||||
The migration rule is:
|
||||
|
||||
```text
|
||||
When a feature moves to /acp:
|
||||
remove its corresponding REST endpoint from goosed
|
||||
```
|
||||
|
||||
The final cutover from `goosed agent` to `goose serve` is blocked until no desktop runtime feature depends on REST/OpenAPI.
|
||||
|
||||
Expected effort:
|
||||
|
||||
- Mounting `/acp` in `goosed`: relatively easy because both servers are Axum and the ACP router already exists.
|
||||
- Removing `goosed` later: medium effort, because every REST-only desktop capability must first be moved into standard ACP or `_goose/*` custom ACP methods.
|
||||
|
||||
## Alternative Considered: Move REST Into `goose serve`
|
||||
|
||||
It is possible to make `goose serve` also mount `goose-server` REST routes. That would make `goose-cli` depend on `goose-server`.
|
||||
|
||||
Tradeoffs:
|
||||
|
||||
- Simpler single `goose` binary packaging.
|
||||
- But the general-purpose CLI binary gets desktop REST/OpenAPI/tunnel/gateway/server dependencies.
|
||||
- Likely increases `goose` binary size.
|
||||
- Blurs the CLI and desktop backend boundary.
|
||||
|
||||
The cleaner migration bridge is the reverse: add ACP to `goosed` temporarily. The final target remains `goose serve`, not `goosed`.
|
||||
|
||||
## Streaming Differences
|
||||
|
||||
Adding `/acp` to `goosed` does not make the current desktop streaming code work unchanged.
|
||||
|
||||
### Current REST streaming
|
||||
|
||||
The current desktop chat stream expects:
|
||||
|
||||
- `GET /sessions/{id}/events`
|
||||
- Goose-specific `MessageEvent` objects
|
||||
- `ActiveRequests`
|
||||
- `request_id` / `chat_request_id`
|
||||
- `Message`, `Finish`, `Error`, `Notification`
|
||||
|
||||
### ACP streaming
|
||||
|
||||
ACP streaming is protocol-level:
|
||||
|
||||
- Client sends JSON-RPC `session/prompt`.
|
||||
- Server emits `session/update` notifications.
|
||||
- Updates include:
|
||||
- agent message chunks
|
||||
- user message chunks
|
||||
- tool calls
|
||||
- tool call updates
|
||||
- usage updates
|
||||
- session info updates
|
||||
- config option updates
|
||||
|
||||
Tool approval also changes:
|
||||
|
||||
- REST uses `/action-required/tool-confirmation`.
|
||||
- ACP sends `RequestPermissionRequest`.
|
||||
- The client must respond on the ACP connection.
|
||||
|
||||
## Recommended Client Strategy
|
||||
|
||||
Use WebSocket ACP directly from the renderer.
|
||||
|
||||
Preferred shape:
|
||||
|
||||
```text
|
||||
renderer -> wss://127.0.0.1:<port>/acp
|
||||
send initialize
|
||||
send session/new
|
||||
send session/load
|
||||
send session/prompt
|
||||
receive session/update notifications continuously
|
||||
respond to permission requests
|
||||
```
|
||||
|
||||
WebSocket is preferable to HTTP POST + SSE because it gives one bidirectional connection for requests, responses, notifications, and permission responses. Do not add an Electron-main IPC transport layer for normal ACP chat traffic.
|
||||
|
||||
## Existing Reference: `ui/goose2`
|
||||
|
||||
`ui/goose2` already has a client pattern that can be reused or adapted.
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `ui/goose2/src/shared/api/createWebSocketStream.ts`
|
||||
- `ui/goose2/src/shared/api/acpConnection.ts`
|
||||
- `ui/goose2/src/shared/api/acpApi.ts`
|
||||
- `ui/goose2/src-tauri/src/services/acp/goose_serve.rs`
|
||||
|
||||
`ui/goose2`:
|
||||
|
||||
- gets a `/acp` WebSocket URL from Tauri
|
||||
- creates a WebSocket stream
|
||||
- creates a `GooseClient`
|
||||
- initializes ACP with client capabilities
|
||||
- routes `sessionUpdate` notifications through a handler
|
||||
- exposes APIs such as:
|
||||
- `listSessions`
|
||||
- `newSession`
|
||||
- `loadSession`
|
||||
- `prompt`
|
||||
- `cancelSession`
|
||||
- `setProvider`
|
||||
- `setModel`
|
||||
- `_goose/*` custom methods
|
||||
|
||||
`ui/desktop` can use the same pattern, replacing Tauri URL lookup with Electron URL lookup.
|
||||
Because `ui/goose2` is expected to move out of this repo in the future, desktop should not share runtime code with it. Treat `ui/goose2` as a reference implementation and copy/adapt the small ACP client pieces into `ui/desktop`.
|
||||
|
||||
Example URL derivation:
|
||||
|
||||
```ts
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
const acpUrl = baseUrl.replace(/^http/, 'ws') + '/acp';
|
||||
```
|
||||
|
||||
If `goosed` is running HTTPS, this becomes `wss://.../acp`.
|
||||
|
||||
## ACP Auth Decision
|
||||
|
||||
Current `goosed` REST uses `X-Secret-Key`.
|
||||
|
||||
Browser WebSocket does not support arbitrary request headers. If `/acp` is mounted behind the same auth middleware, direct renderer WebSocket may fail.
|
||||
|
||||
Chosen direction: `/acp` should have ACP-compatible token auth.
|
||||
|
||||
During migration:
|
||||
|
||||
```text
|
||||
REST routes:
|
||||
X-Secret-Key header
|
||||
|
||||
ACP route:
|
||||
ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
|
||||
```
|
||||
|
||||
After REST is removed:
|
||||
|
||||
```text
|
||||
ACP only:
|
||||
ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
|
||||
```
|
||||
|
||||
This preserves a security boundary for the long-term desktop API while still allowing direct renderer WebSocket connections.
|
||||
|
||||
Guardrails:
|
||||
|
||||
- Use a random ACP token for the `goosed` process.
|
||||
- Prefer an ACP-specific token over reusing the raw REST `X-Secret-Key`.
|
||||
- Never log full `/acp?token=...` URLs.
|
||||
- Keep accepting `X-Secret-Key` only for REST during migration.
|
||||
- Accept ACP token auth only for the local desktop backend.
|
||||
- Keep REST and ACP as separate route branches so auth policy and endpoint removal stay clear.
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
1. Mount `/acp` outside `X-Secret-Key` auth, relying on localhost binding.
|
||||
2. Allow auth through a query parameter for `/acp`, for example `/acp?token=...`.
|
||||
3. Open the WebSocket from Electron main, where headers are easier, and bridge to renderer via IPC.
|
||||
4. Use HTTP/SSE transport where headers are possible, though this is less ergonomic for ACP permission/request-response flow.
|
||||
|
||||
Option 2 is the preferred approach. Option 1 matches current `ui/goose2` behavior, but it is weaker as the final desktop backend shape.
|
||||
|
||||
## Migration Routing
|
||||
|
||||
Keep REST and ACP side-by-side only for feature areas that have not moved yet.
|
||||
|
||||
Example shape:
|
||||
|
||||
```ts
|
||||
const backend = {
|
||||
sessions: flags.acpSessions ? acpSessions : restSessions,
|
||||
chat: flags.acpChat ? acpChat : restChat,
|
||||
providers: flags.acpProviders ? acpProviders : restProviders,
|
||||
};
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `goosed` remains default for unmigrated surfaces.
|
||||
- ACP is opt-in per feature area.
|
||||
- New functionality should prefer ACP unless blocked.
|
||||
- Once a feature area is migrated to `/acp`, remove the corresponding REST endpoint from `goosed` rather than keeping a permanent fallback.
|
||||
- Any missing behavior discovered during migration should be added to ACP before removing the REST endpoint.
|
||||
- Final milestone is no runtime dependency on generated REST APIs.
|
||||
|
||||
## Migration Order
|
||||
|
||||
### 1. Mount `/acp` in `goosed`
|
||||
|
||||
Add ACP Axum router to the `goosed agent` app.
|
||||
|
||||
Likely places:
|
||||
|
||||
- `crates/goose-server/src/commands/agent.rs`
|
||||
- `crates/goose-server/src/routes/mod.rs`
|
||||
|
||||
Use:
|
||||
|
||||
```rust
|
||||
goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig}
|
||||
goose::acp::transport::create_router
|
||||
```
|
||||
|
||||
Need to decide:
|
||||
|
||||
- route merge order
|
||||
- whether ACP MCP app proxy should be included once or deduplicated against existing goosed MCP app proxy routes
|
||||
|
||||
Auth model: use token-authenticated `/acp` for direct renderer WebSocket, separate from REST `X-Secret-Key`.
|
||||
|
||||
### 2. Add desktop ACP client
|
||||
|
||||
Add something like:
|
||||
|
||||
```text
|
||||
ui/desktop/src/acp/createWebSocketStream.ts
|
||||
ui/desktop/src/acp/acpConnection.ts
|
||||
ui/desktop/src/acp/acpApi.ts
|
||||
```
|
||||
|
||||
This can be based on `ui/goose2`.
|
||||
|
||||
### 3. Migrate session list/create/load
|
||||
|
||||
Map:
|
||||
|
||||
```text
|
||||
REST /agent/start -> ACP session/new
|
||||
REST /agent/resume -> ACP session/load
|
||||
REST /sessions -> ACP session/list
|
||||
REST /sessions/{id} -> ACP session/load plus replay/session metadata
|
||||
```
|
||||
|
||||
This proves:
|
||||
|
||||
- session IDs
|
||||
- history replay
|
||||
- session metadata
|
||||
- current model/provider/mode state
|
||||
|
||||
### 4. Migrate chat streaming
|
||||
|
||||
Map:
|
||||
|
||||
```text
|
||||
REST /sessions/{id}/reply
|
||||
REST /sessions/{id}/events
|
||||
-> ACP session/prompt + session/update notifications
|
||||
```
|
||||
|
||||
Prefer moving the chat state toward ACP-native events and data structures rather than preserving the old goosed `MessageEvent` model. A temporary adapter into existing desktop `Message` and `TokenState` shapes is acceptable only as a short bridge if it substantially lowers rollout risk.
|
||||
|
||||
### 5. Migrate tool approval and tool display
|
||||
|
||||
Map:
|
||||
|
||||
```text
|
||||
REST /action-required/tool-confirmation
|
||||
-> ACP RequestPermissionRequest response
|
||||
```
|
||||
|
||||
Tool display should move toward ACP-native `tool_call` / `tool_call_update` state. Any old desktop message-shape adapter should be treated as temporary migration glue.
|
||||
|
||||
### 6. Migrate provider/model/mode
|
||||
|
||||
Use ACP session config options:
|
||||
|
||||
```text
|
||||
setSessionConfigOption({ configId: "provider" })
|
||||
setSessionConfigOption({ configId: "model" })
|
||||
setSessionConfigOption({ configId: "mode" })
|
||||
```
|
||||
|
||||
Use existing Goose custom methods for provider inventory and setup:
|
||||
|
||||
```text
|
||||
_goose/providers/list
|
||||
_goose/providers/config/read
|
||||
_goose/providers/config/save
|
||||
_goose/providers/config/status
|
||||
_goose/providers/custom/*
|
||||
_goose/providers/catalog/*
|
||||
```
|
||||
|
||||
### 7. Migrate extensions/tools/resources
|
||||
|
||||
Existing custom ACP methods cover:
|
||||
|
||||
```text
|
||||
_goose/extensions/add
|
||||
_goose/extensions/remove
|
||||
_goose/config/extensions
|
||||
_goose/config/extensions/add
|
||||
_goose/config/extensions/remove
|
||||
_goose/config/extensions/toggle
|
||||
_goose/session/extensions
|
||||
_goose/tools
|
||||
_goose/tool/call
|
||||
_goose/resource/read
|
||||
_goose/working_dir/update
|
||||
```
|
||||
|
||||
### 8. Migrate settings and secondary surfaces
|
||||
|
||||
Existing ACP custom methods cover many settings/product surfaces:
|
||||
|
||||
```text
|
||||
_goose/preferences/*
|
||||
_goose/defaults/*
|
||||
_goose/onboarding/import/*
|
||||
_goose/sources/*
|
||||
_goose/dictation/*
|
||||
```
|
||||
|
||||
## First Spike Plan: Configured Extensions List
|
||||
|
||||
Before the ACP session-list PR lands, use configured extensions as the first migration slice. This avoids session ID semantics and chat streaming complexity while still proving the core `/acp` path.
|
||||
|
||||
### Scope
|
||||
|
||||
Migrate the read-only configured extensions list behind a feature flag.
|
||||
|
||||
```text
|
||||
Current REST:
|
||||
GET /config/extensions
|
||||
|
||||
Target ACP:
|
||||
_goose/config/extensions
|
||||
```
|
||||
|
||||
Do not migrate add/update/delete in this first slice. Those can remain REST until the read path and ACP client plumbing are proven.
|
||||
|
||||
### Why This Is A Good First Slice
|
||||
|
||||
- No session ID dependency.
|
||||
- No chat streaming or prompt lifecycle.
|
||||
- No tool approval.
|
||||
- No message-shape conversion.
|
||||
- Exercises direct renderer WebSocket to `/acp`.
|
||||
- Exercises ACP initialization and custom `_goose/*` request dispatch.
|
||||
- Easy to compare REST and ACP responses.
|
||||
|
||||
### Current Gap Found
|
||||
|
||||
REST `GET /config/extensions` filters hidden extensions:
|
||||
|
||||
```rust
|
||||
goose::config::get_all_extensions()
|
||||
.into_iter()
|
||||
.filter(|ext| !goose::agents::extension_manager::is_hidden_extension(&ext.config.name()))
|
||||
```
|
||||
|
||||
ACP `_goose/config/extensions` currently calls `crate::config::extensions::get_all_extensions()` and does not apply the same hidden-extension filter.
|
||||
|
||||
For this spike, either:
|
||||
|
||||
1. Fix ACP to match REST before enabling the feature flag, or
|
||||
2. Allow the mismatch only in local spike mode and track it as the first ACP gap.
|
||||
|
||||
Recommendation: fix ACP to match REST. The goal is to prove migration, not introduce UI-visible behavior differences.
|
||||
|
||||
### Backend Steps
|
||||
|
||||
1. Mount `/acp` in `goosed agent`.
|
||||
- Add or merge the ACP Axum router into `crates/goose-server`.
|
||||
- Keep REST and ACP as separate route branches.
|
||||
- Avoid collisions with existing `/status`, `/mcp-app-proxy`, and `/mcp-app-guest` routes.
|
||||
|
||||
2. Add ACP token auth for `/acp`.
|
||||
- Direct renderer WebSocket should connect with:
|
||||
```text
|
||||
ws(s)://127.0.0.1:<port>/acp?token=<acp-token>
|
||||
```
|
||||
- Do not put `/acp` behind REST `X-Secret-Key` middleware.
|
||||
- Do not log full token-bearing URLs.
|
||||
|
||||
3. Fix `_goose/config/extensions` parity.
|
||||
- Apply the same hidden-extension filtering as REST.
|
||||
- Preserve `warnings`.
|
||||
- Preserve enough shape for existing desktop extension rendering.
|
||||
|
||||
### Desktop Client Steps
|
||||
|
||||
1. Add ACP client files under `ui/desktop/src/acp/`.
|
||||
Suggested files:
|
||||
```text
|
||||
ui/desktop/src/acp/createWebSocketStream.ts
|
||||
ui/desktop/src/acp/acpConnection.ts
|
||||
ui/desktop/src/acp/acpApi.ts
|
||||
```
|
||||
|
||||
2. Base these on `ui/goose2`, but copy/adapt rather than share.
|
||||
- Replace Tauri URL lookup with Electron lookup.
|
||||
- Build ACP URL from the existing goosed host.
|
||||
- Append the ACP token.
|
||||
- Initialize ACP once and reconnect on close.
|
||||
|
||||
3. Add an ACP extensions API wrapper:
|
||||
```ts
|
||||
getConfigExtensionsViaAcp(): Promise<ExtensionResponse>
|
||||
```
|
||||
It should call:
|
||||
```text
|
||||
_goose/config/extensions
|
||||
```
|
||||
and normalize the response to the existing desktop `ExtensionResponse` shape.
|
||||
|
||||
4. Add a feature flag.
|
||||
Suggested name:
|
||||
```text
|
||||
acpConfigExtensions
|
||||
```
|
||||
|
||||
5. Route only the read path through ACP when the flag is enabled.
|
||||
Primary integration point:
|
||||
- `ui/desktop/src/components/ConfigContext.tsx`
|
||||
|
||||
Keep mutation paths on REST for this first spike:
|
||||
- `addExtension`
|
||||
- `removeExtension`
|
||||
- `toggleExtension`
|
||||
- bundled extension sync/prune
|
||||
|
||||
### Validation
|
||||
|
||||
Compare REST and ACP for the same local config:
|
||||
|
||||
- same visible extension count
|
||||
- same names
|
||||
- same `enabled` values
|
||||
- same extension types (`builtin`, `stdio`, `streamable_http`, etc.)
|
||||
- same warnings
|
||||
- hidden extensions do not appear
|
||||
- settings/extensions UI renders correctly
|
||||
- add/update/delete still work through REST and refresh the list
|
||||
- failed ACP connection falls back to REST while the spike flag is enabled
|
||||
|
||||
### Removal Rule
|
||||
|
||||
Do not remove `GET /config/extensions` after this first read-only spike if writes still use REST sync logic that depends on the endpoint.
|
||||
|
||||
Remove the REST extension endpoints only after the full extension config surface has migrated:
|
||||
|
||||
```text
|
||||
GET /config/extensions
|
||||
POST /config/extensions
|
||||
DELETE /config/extensions/{name}
|
||||
```
|
||||
|
||||
Corresponding ACP methods:
|
||||
|
||||
```text
|
||||
_goose/config/extensions
|
||||
_goose/config/extensions/add
|
||||
_goose/config/extensions/remove
|
||||
_goose/config/extensions/toggle
|
||||
```
|
||||
|
||||
## Known Gaps To Investigate
|
||||
|
||||
Likely REST-only or partially covered areas:
|
||||
|
||||
- recipe encode/decode/scan/schedule/create-from-session
|
||||
- schedules
|
||||
- local inference model management/downloads
|
||||
- tunnel/gateway
|
||||
- diagnostics/system info
|
||||
- telemetry
|
||||
- session sharing
|
||||
- app export/import/list app flows
|
||||
- MCP UI proxy details
|
||||
- current `ActiveRequests` reattach semantics
|
||||
|
||||
During the transition these can remain on REST fallback. The final target is to expose each required capability through Goose custom ACP methods under `_goose/...` unless it maps cleanly to standard ACP.
|
||||
|
||||
## Recommended End State
|
||||
|
||||
Short term:
|
||||
|
||||
```text
|
||||
goosed exposes REST + temporary /acp
|
||||
desktop uses REST by default, ACP by feature flag
|
||||
```
|
||||
|
||||
Migration:
|
||||
|
||||
```text
|
||||
desktop moves one feature area at a time to ACP
|
||||
missing backend behavior is added as standard ACP use or _goose custom methods
|
||||
matching goosed REST endpoints are removed as each feature migrates
|
||||
```
|
||||
|
||||
End state:
|
||||
|
||||
```text
|
||||
desktop talks to /acp directly
|
||||
goose serve is the single bundled desktop backend
|
||||
goosed is no longer bundled or spawned
|
||||
REST/OpenAPI is removed from desktop runtime behavior
|
||||
```
|
||||
|
||||
## Open Decisions
|
||||
|
||||
No major architecture decisions remain from this spike. Implementation details still need validation around route merge order, MCP app proxy deduplication, and exact token plumbing.
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/code-review
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/create-pr
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/edge-case-finder
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/code-review
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/create-pr
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/edge-case-finder
|
||||
@@ -1,48 +0,0 @@
|
||||
# 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
|
||||
@@ -1,275 +0,0 @@
|
||||
# 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 4. 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
|
||||
|
||||
### The frontend → ACP → goose core path
|
||||
|
||||
**All frontend ↔ backend communication in goose2 flows through a single path:**
|
||||
|
||||
```
|
||||
React UI ──► features/<feature>/api/ ──► @aaif/goose-sdk (TS) ──► goose-acp (WebSocket, ACP) ──► goose (core)
|
||||
```
|
||||
|
||||
**YOU MUST TREAT THE CLIENT as a THIN CLIENT****
|
||||
|
||||
- The Tauri shell spawns a long-lived `goose serve` process and exposes its WebSocket URL via the `get_goose_serve_url` Tauri command. That is essentially the only Tauri command the frontend needs for backend work — it is how the renderer discovers the ACP endpoint.
|
||||
- The frontend opens a WebSocket to `goose serve` and talks to it using `@aaif/goose-sdk` (published from `ui/sdk/`). The SDK is generated from the ACP custom-method definitions in `crates/goose-sdk/src/custom_requests.rs`, so every backend method has a typed TypeScript client method.
|
||||
- The goose2 TypeScript code should only be UI
|
||||
- `goose-acp` (`crates/goose-acp/src/server.rs`) is the server side of the WebSocket. It implements handlers for the custom ACP methods and calls into the `goose` core crate to do the actual work (providers, config, sessions, dictation, etc.).
|
||||
- `goose` is the pure domain crate. It knows nothing about Tauri or WebSockets — it just exposes Rust APIs that `goose-acp` handlers invoke.
|
||||
|
||||
**This is the pattern you must follow when adding any new backend-touching feature.** When you are vibecoding in this app, it is very tempting to reach for `invoke()` or add an HTTP fetch — don't. The rule is: if a feature needs to talk to `goose` core, it goes through the SDK → ACP → goose chain above.
|
||||
|
||||
### Don't build features entirely in the frontend
|
||||
|
||||
If a feature involves data, persistence, secrets, provider config, sessions, filesystem access, network calls to external services, or anything else that could plausibly be reused by the CLI or another goose surface, **the logic belongs in the `goose` core crate, exposed via a typed ACP method.** Do not:
|
||||
|
||||
- Stand up a feature whose business logic lives in a Zustand store, a React hook, or a `features/<feature>/api/` adapter that calls `localStorage`, `fetch`, or filesystem APIs directly.
|
||||
- Reach for `invoke()` to add a new Tauri command that proxies into `goose` — add an ACP custom method instead.
|
||||
- Do not use `localStorage` except for things it's absolutely needed for (specifically outlined in other parts of this AGENTS.md)
|
||||
- Duplicate types between TS and Rust — let the SDK generation produce them from `crates/goose-sdk/src/custom_requests.rs` and use the generated types ALWAYS. Do not create a shadow of a type you could use from the generated types.
|
||||
|
||||
The frontend's job is presentation, navigation, and orchestration of typed SDK calls. If you find a feature growing real logic on the React side, that's a signal to push it down into `goose-acp` + `goose`.
|
||||
|
||||
### File structure
|
||||
|
||||
The directory layout is organized to reinforce the path above. Every feature that touches the backend has an `api/` module that wraps `GooseClient` calls — UI and stores never touch the SDK directly.
|
||||
|
||||
```
|
||||
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 (frontend-only UI state; not a substitute for goose core)
|
||||
api/ — Thin wrappers around GooseClient SDK calls (the ONLY place ACP is touched)
|
||||
types.ts — Feature-specific type definitions (when needed)
|
||||
shared/
|
||||
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/ — Shared GooseClient wrappers used by multiple features (e.g. dictation)
|
||||
constants/ — Shared constants
|
||||
context/ — Shared contexts
|
||||
```
|
||||
|
||||
### Feature Organization
|
||||
|
||||
Not every feature needs every subdirectory. Use only what the feature requires. Note that anything beyond `ui/` only — i.e. anything with state or backend calls — should already have a corresponding ACP method on the goose side.
|
||||
|
||||
| Pattern | Structure | Examples | Backend shape |
|
||||
|----------------------|----------------------------------|---------------------------------|------------------------------------------------|
|
||||
| **Full-featured** | `stores/` + `hooks/` + `ui/` + `api/` | agents, chat | Typed ACP methods drive the store |
|
||||
| **Data-driven** | `stores/` + `api/` + `ui/` | projects | CRUD via ACP, store caches results |
|
||||
| **API features** | `api/` + `ui/` | skills, providers | Pure pass-through to ACP |
|
||||
| **Simple features** | `ui/` only | home, settings, sidebar, status | No backend (pure presentation) |
|
||||
| **Tabs** | `ui/` + `types.ts` | tabs | No backend (frontend-only UI state) |
|
||||
|
||||
If you're tempted to build a "Full-featured" or "Data-driven" feature without a corresponding `api/` module that calls a typed ACP method, stop and add the ACP method first. See "The canonical example" below.
|
||||
|
||||
### 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/`.
|
||||
- Only `features/<feature>/api/` and `shared/api/` modules may import from `@aaif/goose-sdk` or call `getClient()`. UI components, hooks, and stores must go through those wrappers.
|
||||
|
||||
## 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 | Hex color | localStorage | `--brand` / `--color-brand` CSS variables |
|
||||
| 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.
|
||||
- Distro bundle behavior, including feature flags, is documented in `distro/README.md`.
|
||||
|
||||
## Working in the ACP path: examples and rules
|
||||
|
||||
### The canonical example: skills-as-sources (PR #8675)
|
||||
|
||||
The skills → sources migration in [#8675](https://github.com/block/goose/pull/8675) is the clearest illustration of the rule. **It deleted 319 lines of Tauri-command code in `src-tauri/src/commands/skills.rs` and replaced them with ACP custom methods.** If you find yourself wanting to add an `invoke()` command that proxies to `goose`, that PR is what "doing it the other way" looks like. Copy this shape when adding new endpoints:
|
||||
|
||||
1. **Define the request/response in `crates/goose-sdk/src/custom_requests.rs`.** Use the `JsonRpcRequest` / `JsonRpcResponse` derives and the `#[request(method = "_goose/<area>/<action>", response = ...)]` attribute. Sources uses namespaced methods like `_goose/sources/create`, `_goose/sources/list`, `_goose/sources/update`, `_goose/sources/delete`, `_goose/sources/export`, `_goose/sources/import` with paired request/response structs (`CreateSourceRequest` / `CreateSourceResponse`, etc.). Keep the docs on those structs aligned with the implementation: today `_goose/sources/list` is still skill-only; create/import take an explicit target scope (`global`, plus `projectDir` for project sources), while update/delete/export operate on an existing skill by absolute `path`.
|
||||
2. **Implement the handler in `crates/goose-acp/src/server.rs`** with `#[custom_method(YourRequest)]`. Keep it thin: unpack the request, call into the `goose` crate, wrap the result. The sources handlers are ~5 lines each — e.g. `on_list_sources` just calls `goose::sources::list_sources(...)` and returns the typed response. Errors map to `agent_client_protocol::Error::invalid_params()` / `internal_error()`.
|
||||
3. **Put the real logic in the `goose` crate.** Sources lives in `crates/goose/src/sources.rs` — filesystem CRUD, frontmatter parsing, scope resolution, all of it. `goose-acp` knows nothing about where skills are stored on disk; it just forwards typed arguments. This separation is the point.
|
||||
4. **Regenerate the SDK.** The TS methods on `GooseClient` are generated into `ui/sdk/src/generated/`. Do not hand-edit generated files.
|
||||
5. **Call it from the frontend via a feature `api/` module.** See `ui/goose2/src/features/skills/api/skills.ts`. It calls `getClient()` from `acpConnection.ts` and invokes the SDK, then adapts the generic `SourceEntry` shape into a feature-friendly `SkillInfo`:
|
||||
```ts
|
||||
export async function listSkills(): Promise<SkillInfo[]> {
|
||||
const client = await getClient();
|
||||
const raw = await client.extMethod("_goose/sources/list", { type: "skill" });
|
||||
const sources = (raw.sources ?? []) as SourceEntry[];
|
||||
return sources.map(toSkillInfo);
|
||||
}
|
||||
```
|
||||
Feature code (hooks, stores, UI) imports from that `api/` module — it never touches the ACP client directly.
|
||||
|
||||
**Note on typed vs untyped calls.** Skills currently uses `client.extMethod("_goose/sources/...", ...)` (the untyped escape hatch) because it reshapes a generic `Source` API into skill-specific types. The **preferred** shape for new features is the typed generated methods — `client.goose.GooseFooBar({ ... })` — as used by dictation (`client.goose.GooseDictationTranscribe`) and the provider inventory (`client.goose.GooseProvidersList`). Reach for `extMethod()` only when you have a real reason to bypass the generated types.
|
||||
|
||||
For a minimal frontend `api/` wrapper using the typed shape, see `ui/goose2/src/features/providers/api/inventory.ts` — ~30 lines, typed SDK calls, thin adapter. For a fully worked end-to-end feature including OS-keychain handling and progress streaming, see the voice dictation feature ([#8609](https://github.com/block/goose/pull/8609)) and `ui/goose2/src/shared/api/dictation.ts`.
|
||||
|
||||
### Typed ACP config contracts
|
||||
|
||||
Build goose2 config flows around typed ACP methods whose contract matches the domain: provider config, preferences, defaults, dictation secrets, or extension config.
|
||||
|
||||
Keep raw Goose storage keys and secret-handling decisions behind backend-owned ACP methods. This lets Goose validate inputs, apply provider metadata, invalidate caches, refresh dependent state, and keep generated SDK types aligned with supported behavior.
|
||||
|
||||
For reference, define contracts in `crates/goose-sdk/src/custom_requests.rs`, implement them in `crates/goose/src/acp/server/`, regenerate `ui/sdk/src/generated/`, then call the generated `client.goose.*` methods from a feature or shared `api/` wrapper.
|
||||
|
||||
### When `invoke()` is still appropriate
|
||||
|
||||
Tauri commands (`invoke()` from `@tauri-apps/api/core`) are reserved for things that genuinely belong to the desktop shell, not to `goose` core. Provider config or secret mutations that affect the Goose runtime must flow through React → SDK → ACP → goose core so core can validate provider metadata, invalidate secret caches, refresh inventory, and apply provider changes consistently. In practice, Tauri is limited to:
|
||||
|
||||
- `get_goose_serve_url` — bootstrapping the ACP connection.
|
||||
- Native auth subprocesses and desktop-shell side effects.
|
||||
- Window state, filesystem dialogs, and other Tauri-plugin-backed capabilities.
|
||||
- Transitional provider cleanup such as `delete_provider_config` while local OAuth/cache side effects still live in the shell. `get_provider_config`, `check_all_provider_status`, and provider deletion duplicate provider/config knowledge in Tauri today and should move behind provider-scoped ACP methods.
|
||||
|
||||
The long-term provider-config API should be provider-scoped rather than a frontend composition of generic config/secret writes. Prefer methods such as `_goose/providers/config/read`, `_goose/providers/config/status`, `_goose/providers/config/save`, and `_goose/providers/config/delete`; inventory refresh can be part of the result, but active provider reload belongs in the config mutation/apply path, not in `_goose/providers/inventory/refresh`.
|
||||
|
||||
If the thing you're building is "get data from goose" or "tell goose to do something," it is **not** one of these cases. Add a custom ACP method instead.
|
||||
|
||||
### Don't
|
||||
|
||||
- Don't add HTTP `fetch` calls to a `goose` HTTP API, or reintroduce an `apiFetch` utility. There is no HTTP API for goose2 — the backend is the ACP WebSocket.
|
||||
- Don't manage a sidecar `goose` process from the renderer. The Tauri shell owns that lifecycle.
|
||||
- Don't add a new `invoke()` command in `src-tauri/` as a proxy to `goose` core. Add an ACP custom method instead.
|
||||
- Don't hand-edit `ui/sdk/src/generated/`. Regenerate.
|
||||
- Don't call the ACP client (`getClient()`) directly from UI components or stores. Go through a `shared/api/*.ts` (or `features/<feature>/api/*.ts`) module so the SDK surface is mockable in tests.
|
||||
|
||||
## 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.
|
||||
|
||||
## Performance Logging
|
||||
|
||||
- Frontend perf logs use `perfLog()` from `@/shared/lib/perfLog`. Messages are tagged `[perf:<channel>]` (startup, conn, load, newtab, prepare, send, api, stream, replay, chatview). Enabled automatically in Vite dev mode, or opt-in via `localStorage.setItem("goose.perf", "1")` in a release build.
|
||||
- Backend perf logs live in `crates/goose-acp/src/server.rs` under `target: "perf"` at `debug!` level. Off by default; enable with `RUST_LOG=perf=debug,info` on the `goose serve` process.
|
||||
- `just dev` and `just dev-debug` export `RUST_LOG=perf=debug,info` so the child `goose serve` emits perf logs without extra setup. Override by setting `RUST_LOG` in the environment before invoking `just`.
|
||||
|
||||
## 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`.
|
||||
- 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.
|
||||
- Don't build features end-to-end in the frontend. If it talks to data, secrets, providers, sessions, or the filesystem, it goes through `goose` core via a typed ACP method. See **Architecture → Don't build features entirely in the frontend**.
|
||||
@@ -1 +0,0 @@
|
||||
@AGENTS.md
|
||||
@@ -1,30 +0,0 @@
|
||||
# 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. Prepare workspace 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` installs UI workspace dependencies, builds the SDK package, and builds the local debug `goose` CLI binary. `just dev` exports `GOOSE_BIN` to that local binary and loads `src-tauri/tauri.dev.conf.json`, which clears the production `externalBin` requirement during development.
|
||||
|
||||
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.
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"$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",
|
||||
"!justfile"
|
||||
]
|
||||
},
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
# Goose2 distro bundles
|
||||
|
||||
A Goose2 distro bundle is an optional app-specific package of configuration and policy that the Tauri shell loads at startup.
|
||||
|
||||
## What a distro bundle is
|
||||
|
||||
A distro bundle lives under `ui/goose2/distro/` in development, and is bundled into the packaged app as a Tauri resource in production.
|
||||
|
||||
Current supported files:
|
||||
|
||||
- `distro.json` — distro manifest
|
||||
- `config.yaml` — optional Goose config passed to `goose serve`
|
||||
- `bin/` — optional executables or helper scripts prepended to `PATH` for `goose serve`
|
||||
|
||||
## How it is discovered
|
||||
|
||||
The Tauri app resolves the distro bundle in this order:
|
||||
|
||||
1. `GOOSE_DISTRO_DIR`, if set
|
||||
2. bundled Tauri resource dir at `resource_dir()/distro`
|
||||
|
||||
In development, `just dev` and `just dev-debug` automatically export `GOOSE_DISTRO_DIR` to `ui/goose2/distro` when that directory exists.
|
||||
|
||||
## Manifest shape
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"appVersion": "development",
|
||||
"featureToggles": {
|
||||
"costTracking": false
|
||||
},
|
||||
"providerAllowlist": "databricks"
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
- `appVersion?: string`
|
||||
- optional app version tag supplied by the distro
|
||||
|
||||
- `featureToggles?: Record<string, boolean>`
|
||||
- optional UI/product flags controlled by the distro
|
||||
- currently supported:
|
||||
- `costTracking`
|
||||
- `false` hides cost UI in the token/context usage surfaces
|
||||
- omitted behaves as enabled
|
||||
|
||||
- `providerAllowlist?: string`
|
||||
- comma-separated provider ids
|
||||
- suggests which model providers to show in Settings
|
||||
- suggests which Goose model options to show in the chat model picker
|
||||
|
||||
- `extensionAllowlist?: string`
|
||||
- comma-separated extension ids
|
||||
- reserved for future UI suggestions
|
||||
|
||||
## Runtime effects
|
||||
|
||||
When a distro bundle is present, Goose2 does two kinds of things with it.
|
||||
|
||||
### Frontend behavior
|
||||
|
||||
The frontend loads `get_distro_bundle` during app startup and stores the manifest in Zustand.
|
||||
|
||||
Today it uses that manifest to:
|
||||
|
||||
- filter model providers shown in provider settings via `providerAllowlist`
|
||||
- filter Goose model options shown in the chat input model picker via `providerAllowlist`
|
||||
- hide cost UI when `featureToggles.costTracking === false`
|
||||
|
||||
These allowlists are UI suggestions only. They do not enforce backend access control and do not invalidate existing sessions or saved model choices.
|
||||
|
||||
### Backend / shell behavior
|
||||
|
||||
When the Tauri shell launches the long-lived `goose serve` process, it applies the distro bundle like this:
|
||||
|
||||
- prepends `distro/bin` to `PATH` when present
|
||||
- adds `distro/config.yaml` to `GOOSE_ADDITIONAL_CONFIG_FILES` when present
|
||||
- sets `GOOSE_DISTRO_DIR` to the resolved distro root
|
||||
|
||||
This is shell-level behavior, so it is implemented as Tauri-side setup rather than an ACP method.
|
||||
|
||||
## Development notes
|
||||
|
||||
- packaged apps discover distro content from bundled Tauri resources
|
||||
- local development uses `GOOSE_DISTRO_DIR`
|
||||
- after changing `distro.json`, restart `just dev` so startup reloads the manifest
|
||||
|
||||
## Scope guidance
|
||||
|
||||
Use distro bundles for packaged-app policy and shell-level defaults.
|
||||
|
||||
Good fits:
|
||||
|
||||
- feature flags for Goose2 UI behavior
|
||||
- allowlists that suggest visible product choices
|
||||
- config or helper binaries that should be present when `goose serve` starts
|
||||
|
||||
Avoid using distro bundles as a replacement for normal app state, user settings, or ACP-backed domain data.
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"featureToggles": {
|
||||
"costTracking": true
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
<!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" />
|
||||
<script>
|
||||
(() => {
|
||||
const getDefaultAccent = (theme) =>
|
||||
theme === "dark" ? "#ffffff" : "#1a1a1a";
|
||||
|
||||
const normalizeHexColor = (color) => {
|
||||
const value = color?.trim();
|
||||
if (!value || value === "default") return null;
|
||||
|
||||
const hex = value.startsWith("#") ? value.slice(1) : value;
|
||||
if (/^[0-9a-fA-F]{3}$/.test(hex)) {
|
||||
return `#${hex
|
||||
.split("")
|
||||
.map((char) => char + char)
|
||||
.join("")
|
||||
.toLowerCase()}`;
|
||||
}
|
||||
if (/^[0-9a-fA-F]{6}$/.test(hex)) {
|
||||
return `#${hex.toLowerCase()}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getRelativeLuminance = (hexColor) => {
|
||||
const hex = hexColor.slice(1);
|
||||
const channels = [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)]
|
||||
.map((channel) => {
|
||||
const value = Number.parseInt(channel, 16) / 255;
|
||||
return value <= 0.04045
|
||||
? value / 12.92
|
||||
: ((value + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
|
||||
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
||||
};
|
||||
|
||||
const getContrastColor = (hexColor) => {
|
||||
const luminance = getRelativeLuminance(hexColor);
|
||||
const blackContrast = (luminance + 0.05) / 0.05;
|
||||
const whiteContrast = 1.05 / (luminance + 0.05);
|
||||
return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
|
||||
};
|
||||
|
||||
try {
|
||||
const root = document.documentElement;
|
||||
const storedTheme = localStorage.getItem("goose-theme") || "system";
|
||||
const resolvedTheme =
|
||||
storedTheme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: storedTheme;
|
||||
const accent =
|
||||
normalizeHexColor(localStorage.getItem("goose-accent-color")) ||
|
||||
getDefaultAccent(resolvedTheme);
|
||||
const foreground = getContrastColor(accent);
|
||||
const density = localStorage.getItem("goose-density") || "comfortable";
|
||||
|
||||
root.classList.add(resolvedTheme === "dark" ? "dark" : "light");
|
||||
root.style.colorScheme = resolvedTheme === "dark" ? "dark" : "light";
|
||||
root.style.setProperty("--brand", accent);
|
||||
root.style.setProperty("--brand-foreground", foreground);
|
||||
root.style.setProperty("--color-brand", accent);
|
||||
root.style.setProperty("--color-brand-foreground", foreground);
|
||||
root.style.accentColor = accent;
|
||||
if (density === "compact" || density === "spacious") {
|
||||
root.dataset.density = density;
|
||||
}
|
||||
} catch {
|
||||
// ThemeProvider applies the canonical theme state after React mounts.
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<title>Goose</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,285 +0,0 @@
|
||||
# Derive a stable default port from the working directory so the same worktree
|
||||
# usually gets the same port. Set VITE_PORT to override it.
|
||||
default_vite_port := `python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)"`
|
||||
|
||||
# Default recipe
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# ── Dev Environment ──────────────────────────────────────────
|
||||
|
||||
# Install dependencies, build workspace packages, and activate git hooks
|
||||
setup:
|
||||
cd ../ && pnpm install
|
||||
cd ../sdk && pnpm build
|
||||
cargo build --manifest-path ../../Cargo.toml -p goose-cli --bin goose
|
||||
|
||||
# ── 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 && TAURI_CONFIG='{"bundle":{"externalBin":[]}}' 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 && TAURI_CONFIG='{"bundle":{"externalBin":[]}}' cargo check
|
||||
|
||||
# Full CI gate
|
||||
ci: check clippy test build tauri-check
|
||||
|
||||
bundle:
|
||||
pnpm tauri build
|
||||
|
||||
# ── 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: build-core
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DEFAULT_VITE_PORT={{ default_vite_port }}
|
||||
VITE_PORT="${VITE_PORT:-$DEFAULT_VITE_PORT}"
|
||||
export VITE_PORT
|
||||
# Enable perf logs in the child `goose serve` process by default.
|
||||
# Override with e.g. RUST_LOG=info just dev to disable.
|
||||
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
||||
PROJECT_DIR=$(pwd)
|
||||
REPO_ROOT=$(cd ../.. && pwd)
|
||||
DISTRO_DIR="${PROJECT_DIR}/distro"
|
||||
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
|
||||
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
|
||||
fi
|
||||
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
|
||||
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
||||
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_DEBUG}"
|
||||
elif [[ -x "${LOCAL_GOOSE_RELEASE}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_RELEASE}"
|
||||
else
|
||||
unset GOOSE_BIN
|
||||
fi
|
||||
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./scripts/start-dev-vite.sh ${VITE_PORT}\",\"cwd\":\"${PROJECT_DIR}\",\"wait\":false}}}")
|
||||
|
||||
if [[ -n "${GOOSE_BIN:-}" ]]; then
|
||||
echo "Using local goose binary: ${GOOSE_BIN}"
|
||||
else
|
||||
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
|
||||
fi
|
||||
|
||||
if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
|
||||
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
|
||||
fi
|
||||
|
||||
# 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_ARGS+=(--config "{\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm tauri dev --features app-test-driver --config src-tauri/tauri.dev.conf.json "${EXTRA_CONFIG_ARGS[@]}"
|
||||
|
||||
# Start the desktop app with dev config
|
||||
dev-debug: build-core
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DEFAULT_VITE_PORT={{ default_vite_port }}
|
||||
VITE_PORT="${VITE_PORT:-$DEFAULT_VITE_PORT}"
|
||||
export VITE_PORT
|
||||
# Enable perf logs in the child `goose serve` process by default.
|
||||
# Override with e.g. RUST_LOG=info just dev-debug to disable.
|
||||
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
||||
PROJECT_DIR=$(pwd)
|
||||
REPO_ROOT=$(cd ../.. && pwd)
|
||||
DISTRO_DIR="$(pwd)/distro"
|
||||
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
|
||||
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
|
||||
fi
|
||||
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
|
||||
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
||||
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_DEBUG}"
|
||||
elif [[ -x "${LOCAL_GOOSE_RELEASE}" ]]; then
|
||||
export GOOSE_BIN="${LOCAL_GOOSE_RELEASE}"
|
||||
else
|
||||
unset GOOSE_BIN
|
||||
fi
|
||||
EXTRA_CONFIG_ARGS=(--config "{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./scripts/start-dev-vite.sh ${VITE_PORT}\",\"cwd\":\"${PROJECT_DIR}\",\"wait\":false}}}")
|
||||
|
||||
if [[ -n "${GOOSE_BIN:-}" ]]; then
|
||||
echo "Using local goose binary: ${GOOSE_BIN}"
|
||||
else
|
||||
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
|
||||
fi
|
||||
|
||||
if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
|
||||
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
|
||||
fi
|
||||
|
||||
# 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_ARGS+=(--config "{\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm tauri dev --config src-tauri/tauri.dev.conf.json "${EXTRA_CONFIG_ARGS[@]}"
|
||||
|
||||
# Start only the frontend dev server
|
||||
dev-frontend:
|
||||
pnpm dev
|
||||
|
||||
# Parse `git worktree list --porcelain` into tab-separated "path\tbranch" lines.
|
||||
# Uses sub() instead of $2 so worktree paths containing spaces are preserved.
|
||||
# Detached-HEAD worktrees (no branch line) are silently skipped.
|
||||
_worktree_awk := '/^worktree / { sub(/^worktree /, ""); wt=$0 } /^branch refs\/heads\// { b=$2; sub(/^refs\/heads\//, "", b); print wt "\t" b }'
|
||||
|
||||
# Compute a stable vite port from a directory path passed as $1.
|
||||
_port_cmd := "import hashlib,sys; h=int(hashlib.sha256(sys.argv[1].encode()).hexdigest(),16); print(10000+h%55000)"
|
||||
|
||||
# List worktrees with an active dev server
|
||||
running:
|
||||
#!/usr/bin/env bash
|
||||
git worktree list --porcelain | awk '{{ _worktree_awk }}' \
|
||||
| while IFS=$'\t' read -r wt branch; do
|
||||
dir="$wt/ui/goose2"
|
||||
port=$(python3 -c '{{ _port_cmd }}' "$dir")
|
||||
if lsof -ti :"$port" &>/dev/null; then
|
||||
echo "$branch"
|
||||
fi
|
||||
done
|
||||
|
||||
# Kill the dev process (if running). Optionally pass a branch name to kill another worktree's process.
|
||||
kill branch="":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -n "{{ branch }}" ]]; then
|
||||
WORKTREE_PATH=$(git worktree list --porcelain \
|
||||
| awk '{{ _worktree_awk }}' \
|
||||
| awk -F'\t' -v branch="{{ branch }}" '$2 == branch { print $1; exit }')
|
||||
if [[ -z "$WORKTREE_PATH" ]]; then
|
||||
echo "No worktree found for branch '{{ branch }}'"
|
||||
exit 1
|
||||
fi
|
||||
TARGET_DIR="$WORKTREE_PATH/ui/goose2"
|
||||
VITE_PORT=$(python3 -c '{{ _port_cmd }}' "$TARGET_DIR")
|
||||
else
|
||||
DEFAULT_VITE_PORT={{ default_vite_port }}
|
||||
VITE_PORT="${VITE_PORT:-$DEFAULT_VITE_PORT}"
|
||||
fi
|
||||
|
||||
PID=$(lsof -ti :"$VITE_PORT" 2>/dev/null | head -1) || true
|
||||
|
||||
if [[ -z "$PID" ]]; then
|
||||
echo "No process found on port $VITE_PORT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PROC_NAME=$(ps -p "$PID" -o comm= 2>/dev/null) || true
|
||||
PROC_BASENAME="${PROC_NAME##*/}"
|
||||
if [[ "$PROC_BASENAME" != "node" ]]; then
|
||||
echo "Process on port $VITE_PORT is '$PROC_NAME', not 'node' — refusing to kill"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PGID=$(ps -p "$PID" -o pgid= 2>/dev/null | tr -d ' ')
|
||||
if [[ -z "$PGID" || "$PGID" == "0" || "$PGID" == "1" ]]; then
|
||||
echo "Killing node (PID $PID) on port $VITE_PORT"
|
||||
kill -9 "$PID"
|
||||
else
|
||||
echo "Killing process group $PGID (found via node PID $PID on port $VITE_PORT)"
|
||||
kill -9 -"$PGID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Kill all dev processes across all worktrees
|
||||
kill-all:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
branches=$(just running)
|
||||
if [[ -z "$branches" ]]; then
|
||||
echo "No running dev servers found"
|
||||
exit 0
|
||||
fi
|
||||
while read -r branch; do
|
||||
just kill "$branch" || true
|
||||
done <<< "$branches"
|
||||
|
||||
# ── Utilities ────────────────────────────────────────────────
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
cd src-tauri && cargo clean
|
||||
rm -rf dist
|
||||
rm -rf node_modules
|
||||
|
||||
build-core:
|
||||
cargo build -p goose-cli --bin goose
|
||||
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"name": "goose2",
|
||||
"private": true,
|
||||
"version": "0.20.1",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:i18n": "node ./scripts/check-i18n-strings.mjs",
|
||||
"lint": "biome lint .",
|
||||
"check": "biome check . && 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": {
|
||||
"@aaif/goose-sdk": "workspace:*",
|
||||
"@agentclientprotocol/sdk": "^0.19.0",
|
||||
"@mcp-ui/client": "7.1.0",
|
||||
"@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.7.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-shell": "~2.3.5",
|
||||
"@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": "^4.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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}`,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "1.94.1"
|
||||
profile = "default"
|
||||
@@ -1,254 +0,0 @@
|
||||
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.");
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
#!/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)")
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reset the provider inventory tables to empty, as if migration 12 just ran.
|
||||
# This lets you test the first-use experience (cold inventory).
|
||||
#
|
||||
# Usage: ./scripts/reset-inventory.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DB="${GOOSE_DB:-$HOME/.local/share/goose/sessions/sessions.db}"
|
||||
|
||||
if [ ! -f "$DB" ]; then
|
||||
echo "Database not found at $DB"
|
||||
echo "Set GOOSE_DB to override the path."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Database: $DB"
|
||||
echo ""
|
||||
echo "Before:"
|
||||
echo " provider_inventory_entries: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_entries;')"
|
||||
echo " provider_inventory_models: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_models;')"
|
||||
|
||||
# ON DELETE CASCADE on provider_inventory_models means deleting entries clears both tables.
|
||||
# Delete models first since CASCADE isn't reliable in all sqlite3 builds,
|
||||
# then delete entries.
|
||||
sqlite3 "$DB" "DELETE FROM provider_inventory_models; DELETE FROM provider_inventory_entries;"
|
||||
|
||||
echo ""
|
||||
echo "After:"
|
||||
echo " provider_inventory_entries: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_entries;')"
|
||||
echo " provider_inventory_models: $(sqlite3 "$DB" 'SELECT COUNT(*) FROM provider_inventory_models;')"
|
||||
echo ""
|
||||
echo "Inventory tables are empty. Restart goose to test first-use flow."
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VITE_PORT="${1:-${VITE_PORT:-}}"
|
||||
if [[ -z "${VITE_PORT}" ]]; then
|
||||
echo "VITE_PORT is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PID=$(lsof -ti :"${VITE_PORT}" 2>/dev/null | head -1 || true)
|
||||
if [[ -n "${PID}" ]]; then
|
||||
PROC_ARGS="$(ps -p "${PID}" -o args= 2>/dev/null || true)"
|
||||
PROC_CWD="$(lsof -a -p "${PID}" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1)"
|
||||
if [[ "${PROC_CWD}" == "${PROJECT_DIR}" && "${PROC_ARGS}" == *"--port ${VITE_PORT} --strictPort"* ]]; then
|
||||
echo "Reusing existing Goose2 Vite dev server on port ${VITE_PORT} (PID ${PID})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PROC_NAME="$(ps -p "${PID}" -o comm= 2>/dev/null || true)"
|
||||
echo "Port ${VITE_PORT} is already in use by '${PROC_NAME}' (PID ${PID})." >&2
|
||||
echo "Run 'just goose2 kill' if it is a stale Goose2 dev server, or rerun with VITE_PORT=<free-port> just goose2 dev." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${PROJECT_DIR}"
|
||||
exec pnpm exec vite --port "${VITE_PORT}" --strictPort
|
||||
@@ -1,51 +0,0 @@
|
||||
[package]
|
||||
name = "goose2"
|
||||
version = "0.1.0"
|
||||
description = "Goose desktop app"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "goose-tauri"
|
||||
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"
|
||||
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"] }
|
||||
doctor = { git = "https://github.com/block/builderbot", rev = "8e1c3ec145edc0df5f04b4427cfd758378036862" }
|
||||
ignore = "0.4.25"
|
||||
base64 = "0.22"
|
||||
mime_guess = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
[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"
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Goose uses your microphone to capture voice input for dictation.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"$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",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-min-size",
|
||||
"opener:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$HOME/**"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.goose/**"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1 +0,0 @@
|
||||
{"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","core:window:allow-set-size","core:window:allow-set-min-size","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"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"]}}
|
||||
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 779 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 395 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "tauri-plugin-app-test-driver"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
links = "tauri-plugin-app-test-driver"
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
name = "tauri_plugin_app_test_driver"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2-app-kit = { version = "0.3.2", features = ["NSWindow", "NSResponder"] }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2", features = ["build"] }
|
||||
@@ -1,5 +0,0 @@
|
||||
const COMMANDS: &[&str] = &["driver_result"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS).build();
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-driver-result"
|
||||
description = "Enables the driver_result command without any pre-configured scope."
|
||||
commands.allow = ["driver_result"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-driver-result"
|
||||
description = "Denies the driver_result command without any pre-configured scope."
|
||||
commands.deny = ["driver_result"]
|
||||
@@ -1,43 +0,0 @@
|
||||
## Default Permission
|
||||
|
||||
Default permissions for the app test driver plugin.
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-driver-result`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`app-test-driver:allow-driver-result`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the driver_result command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`app-test-driver:deny-driver-result`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the driver_result command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -1,7 +0,0 @@
|
||||
"$schema" = "schemas/schema.json"
|
||||
|
||||
[default]
|
||||
description = """
|
||||
Default permissions for the app test driver plugin.
|
||||
"""
|
||||
permissions = ["allow-driver-result"]
|
||||
@@ -1,318 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PermissionFile",
|
||||
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "The default permission set for the plugin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultPermission"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"description": "A list of permissions sets defined",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSet"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"description": "Allowed or denied commands when using this permission.",
|
||||
"default": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Commands"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Allowed or denied scoped when using this permission.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Data that defines what is allowed by the scope.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Value": {
|
||||
"description": "All supported ACL values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents a null JSON value.",
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`bool`].",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"description": "Represents a valid ACL [`Number`].",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`String`].",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Represents a list of other [`Value`]s.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Number": {
|
||||
"description": "A valid ACL number.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents an [`i64`].",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`f64`].",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the driver_result command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-driver-result",
|
||||
"markdownDescription": "Enables the driver_result command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the driver_result command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-driver-result",
|
||||
"markdownDescription": "Denies the driver_result command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the app test driver plugin.\n\n#### This default permission set includes:\n\n- `allow-driver-result`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the app test driver plugin.\n\n#### This default permission set includes:\n\n- `allow-driver-result`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||