mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-02 04:34:35 +00:00
Compare commits
3 Commits
opencode/n
...
collapse-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d419a0211 | ||
|
|
8b168981aa | ||
|
|
724dd665ec |
@@ -1 +0,0 @@
|
||||
Fixed typecheck error by reverting key name from 'session.new.worktree.startup' back to 'session.new.workspace.startup' in packages/console/app/src/i18n/tr.ts.
|
||||
@@ -1 +0,0 @@
|
||||
Applied minor linguistic polishes to Turkish translations in packages/console/app/src/i18n/tr.ts. PR created at https://github.com/anomalyco/opencode/pull/15468
|
||||
@@ -27,7 +27,6 @@
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.bs.md">Bosanski</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
|
||||
6
bun.lock
6
bun.lock
@@ -545,7 +545,7 @@
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@pierre/diffs": "1.1.0-beta.13",
|
||||
"@playwright/test": "1.51.0",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
@@ -1469,9 +1469,7 @@
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.18", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-7ZF3YD9fxdbYsPnltz5cUqHacN7ztp8RX/fJLxwv8wIEORpP4+7dHz1h/qx3o4EW2xUrIhmbM8ImywLasB787Q=="],
|
||||
|
||||
"@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="],
|
||||
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-2XLuizbG90QDUQL+1M90XxfVZxjkIQ1cFYS46nnVO7g=",
|
||||
"aarch64-linux": "sha256-hlckiGAtbpAlwgcE7KgzKKRq9T2FEOSq3Q1MhuHfZ2c=",
|
||||
"aarch64-darwin": "sha256-V/8Kay+5bDb/BSVgBQhSMwzmRmkNGl3U0HFMVbVcMak=",
|
||||
"x86_64-darwin": "sha256-duLDF88Q/hXK5jwBy4dVxMSiTTS0R4obp9MlTuOF/Pw="
|
||||
"x86_64-linux": "sha256-dZoLhWe4smBsOF7WczMySLXSAB1YRO1vfhiOCL1rBf0=",
|
||||
"aarch64-linux": "sha256-J7nIz1xuVZEHun5WRZkYRySz29B0A8g5g0RRxnIWTYU=",
|
||||
"aarch64-darwin": "sha256-R2PuhX+EjUBuLE8MF0G0fcUwNaU+5n6V6uVeK89ulzw=",
|
||||
"x86_64-darwin": "sha256-Bvzfz9TsTpYriZNLSLgpNcNb+BgtkgpjoWqdOtF2IBg="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@pierre/diffs": "1.1.0-beta.13",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
# CreateEffect Simplification Implementation Spec
|
||||
|
||||
Reduce reactive misuse across `packages/app`.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
This work targets `packages/app/src`, which currently has 101 `createEffect` calls across 37 files.
|
||||
|
||||
The biggest clusters are `pages/session.tsx` (19), `pages/layout.tsx` (13), `pages/session/file-tabs.tsx` (6), and several context providers that mirror one store into another.
|
||||
|
||||
Key issues from the audit:
|
||||
|
||||
- Derived state is being written through effects instead of computed directly
|
||||
- Session and file resets are handled by watch-and-clear effects instead of keyed state boundaries
|
||||
- User-driven actions are hidden inside reactive effects
|
||||
- Context layers mirror and hydrate child stores with multiple sync effects
|
||||
- Several areas repeat the same imperative trigger pattern in multiple effects
|
||||
|
||||
Keep the implementation focused on removing unnecessary effects, not on broad UI redesign.
|
||||
|
||||
## Goals
|
||||
|
||||
- Cut high-churn `createEffect` usage in the hottest files first
|
||||
- Replace effect-driven derived state with reactive derivation
|
||||
- Replace reset-on-key effects with keyed ownership boundaries
|
||||
- Move event-driven work to direct actions and write paths
|
||||
- Remove mirrored store hydration where a single source of truth can exist
|
||||
- Leave necessary external sync effects in place, but make them narrower and clearer
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not rewrite unrelated component structure just to reduce the count
|
||||
- Do not change product behavior, navigation flow, or persisted data shape unless required for a cleaner write boundary
|
||||
- Do not remove effects that bridge to DOM, editors, polling, or external APIs unless there is a clearly safer equivalent
|
||||
- Do not attempt a repo-wide cleanup outside `packages/app`
|
||||
|
||||
## Effect Taxonomy And Replacement Rules
|
||||
|
||||
Use these rules during implementation.
|
||||
|
||||
### Prefer `createMemo`
|
||||
|
||||
Use `createMemo` when the target value is pure derived state from other signals or stores.
|
||||
|
||||
Do this when an effect only reads reactive inputs and writes another reactive value that could be computed instead.
|
||||
|
||||
Apply this to:
|
||||
|
||||
- `packages/app/src/pages/session.tsx:141`
|
||||
- `packages/app/src/pages/layout.tsx:557`
|
||||
- `packages/app/src/components/terminal.tsx:261`
|
||||
- `packages/app/src/components/session/session-header.tsx:309`
|
||||
|
||||
Rules:
|
||||
|
||||
- If no external system is touched, do not use `createEffect`
|
||||
- Derive once, then read the memo where needed
|
||||
- If normalization is required, prefer normalizing at the write boundary before falling back to a memo
|
||||
|
||||
### Prefer Keyed Remounts
|
||||
|
||||
Use keyed remounts when local UI state should reset because an identity changed.
|
||||
|
||||
Do this with `sessionKey`, `scope()`, or another stable identity instead of watching the key and manually clearing signals.
|
||||
|
||||
Apply this to:
|
||||
|
||||
- `packages/app/src/pages/session.tsx:325`
|
||||
- `packages/app/src/pages/session.tsx:336`
|
||||
- `packages/app/src/pages/session.tsx:477`
|
||||
- `packages/app/src/pages/session.tsx:869`
|
||||
- `packages/app/src/pages/session.tsx:963`
|
||||
- `packages/app/src/pages/session/message-timeline.tsx:149`
|
||||
- `packages/app/src/context/file.tsx:100`
|
||||
|
||||
Rules:
|
||||
|
||||
- If the desired behavior is "new identity, fresh local state," key the owner subtree
|
||||
- Keep state local to the keyed boundary so teardown and recreation handle the reset naturally
|
||||
|
||||
### Prefer Event Handlers And Actions
|
||||
|
||||
Use direct handlers, store actions, and async command functions when work happens because a user clicked, selected, reloaded, or navigated.
|
||||
|
||||
Do this when an effect is just watching for a flag change, command token, or event-bus signal to trigger imperative logic.
|
||||
|
||||
Apply this to:
|
||||
|
||||
- `packages/app/src/pages/layout.tsx:484`
|
||||
- `packages/app/src/pages/layout.tsx:652`
|
||||
- `packages/app/src/pages/layout.tsx:776`
|
||||
- `packages/app/src/pages/layout.tsx:1489`
|
||||
- `packages/app/src/pages/layout.tsx:1519`
|
||||
- `packages/app/src/components/file-tree.tsx:328`
|
||||
- `packages/app/src/pages/session/terminal-panel.tsx:55`
|
||||
- `packages/app/src/context/global-sync.tsx:148`
|
||||
- Duplicated trigger sets in:
|
||||
- `packages/app/src/pages/session/review-tab.tsx:122`
|
||||
- `packages/app/src/pages/session/review-tab.tsx:130`
|
||||
- `packages/app/src/pages/session/review-tab.tsx:138`
|
||||
- `packages/app/src/pages/session/file-tabs.tsx:367`
|
||||
- `packages/app/src/pages/session/file-tabs.tsx:378`
|
||||
- `packages/app/src/pages/session/file-tabs.tsx:389`
|
||||
- `packages/app/src/pages/session/use-session-hash-scroll.ts:144`
|
||||
- `packages/app/src/pages/session/use-session-hash-scroll.ts:149`
|
||||
- `packages/app/src/pages/session/use-session-hash-scroll.ts:167`
|
||||
|
||||
Rules:
|
||||
|
||||
- If the trigger is user intent, call the action at the source of that intent
|
||||
- If the same imperative work is triggered from multiple places, extract one function and call it directly
|
||||
|
||||
### Prefer `onMount` And `onCleanup`
|
||||
|
||||
Use `onMount` and `onCleanup` for lifecycle-only setup and teardown.
|
||||
|
||||
This is the right fit for subscriptions, one-time wiring, timers, and imperative integration that should not rerun for ordinary reactive changes.
|
||||
|
||||
Use this when:
|
||||
|
||||
- Setup should happen once per owner lifecycle
|
||||
- Cleanup should always pair with teardown
|
||||
- The work is not conceptually derived state
|
||||
|
||||
### Keep `createEffect` When It Is A Real Bridge
|
||||
|
||||
Keep `createEffect` when it synchronizes reactive data to an external imperative sink.
|
||||
|
||||
Examples that should remain, though they may be narrowed or split:
|
||||
|
||||
- DOM/editor sync in `packages/app/src/components/prompt-input.tsx:690`
|
||||
- Scroll sync in `packages/app/src/pages/session.tsx:685`
|
||||
- Scroll/hash sync in `packages/app/src/pages/session/use-session-hash-scroll.ts:149`
|
||||
- External sync in:
|
||||
- `packages/app/src/context/language.tsx:207`
|
||||
- `packages/app/src/context/settings.tsx:110`
|
||||
- `packages/app/src/context/sdk.tsx:26`
|
||||
- Polling in:
|
||||
- `packages/app/src/components/status-popover.tsx:59`
|
||||
- `packages/app/src/components/dialog-select-server.tsx:273`
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep the effect single-purpose
|
||||
- Make dependencies explicit and narrow
|
||||
- Avoid writing back into the same reactive graph unless absolutely required
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 0: Classification Pass
|
||||
|
||||
Before changing code, tag each targeted effect as one of: derive, reset, event, lifecycle, or external bridge.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Every targeted effect in this spec is tagged with a replacement strategy before refactoring starts
|
||||
- Shared helpers to be introduced are identified up front to avoid repeating patterns
|
||||
|
||||
### Phase 1: Derived-State Cleanup
|
||||
|
||||
Tackle highest-value, lowest-risk derived-state cleanup first.
|
||||
|
||||
Priority items:
|
||||
|
||||
- Normalize tabs at write boundaries and remove `packages/app/src/pages/session.tsx:141`
|
||||
- Stop syncing `workspaceOrder` in `packages/app/src/pages/layout.tsx:557`
|
||||
- Make prompt slash filtering reactive so `packages/app/src/components/prompt-input.tsx:652` can be removed
|
||||
- Replace other obvious derived-state effects in terminal and session header
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- No behavior change in tab ordering, prompt filtering, terminal display, or header state
|
||||
- Targeted derived-state effects are deleted, not just moved
|
||||
|
||||
### Phase 2: Keyed Reset Cleanup
|
||||
|
||||
Replace reset-on-key effects with keyed ownership boundaries.
|
||||
|
||||
Priority items:
|
||||
|
||||
- Key session-scoped UI and state by `sessionKey`
|
||||
- Key file-scoped state by `scope()`
|
||||
- Remove manual clear-and-reseed effects in session and file context
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Switching session or file scope recreates the intended local state cleanly
|
||||
- No stale state leaks across session or scope changes
|
||||
- Target reset effects are deleted
|
||||
|
||||
### Phase 3: Event-Driven Work Extraction
|
||||
|
||||
Move event-driven work out of reactive effects.
|
||||
|
||||
Priority items:
|
||||
|
||||
- Replace `globalStore.reload` effect dispatching with direct calls
|
||||
- Split mixed-responsibility effect in `packages/app/src/pages/layout.tsx:1489`
|
||||
- Collapse duplicated imperative trigger triplets into single functions
|
||||
- Move file-tree and terminal-panel imperative work to explicit handlers
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- User-triggered behavior still fires exactly once per intended action
|
||||
- No effect remains whose only job is to notice a command-like state and trigger an imperative function
|
||||
|
||||
### Phase 4: Context Ownership Cleanup
|
||||
|
||||
Remove mirrored child-store hydration patterns.
|
||||
|
||||
Priority items:
|
||||
|
||||
- Remove child-store hydration mirrors in `packages/app/src/context/global-sync/child-store.ts:184`, `:190`, `:193`
|
||||
- Simplify mirror logic in `packages/app/src/context/global-sync.tsx:130`, `:138`
|
||||
- Revisit `packages/app/src/context/layout.tsx:424` if it still mirrors instead of deriving
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- There is one clear source of truth for each synced value
|
||||
- Child stores no longer need effect-based hydration to stay consistent
|
||||
- Initialization and updates both work without manual mirror effects
|
||||
|
||||
### Phase 5: Cleanup And Keeper Review
|
||||
|
||||
Clean up remaining targeted hotspots and narrow the effects that should stay.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Remaining `createEffect` calls in touched files are all true bridges or clearly justified lifecycle sync
|
||||
- Mixed-responsibility effects are split into smaller units where still needed
|
||||
|
||||
## Detailed Work Items By Area
|
||||
|
||||
### 1. Normalize Tab State
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/pages/session.tsx:141`
|
||||
|
||||
Work:
|
||||
|
||||
- Move tab normalization into the functions that create, load, or update tab state
|
||||
- Make readers consume already-normalized tab data
|
||||
- Remove the effect that rewrites derived tab state after the fact
|
||||
|
||||
Rationale:
|
||||
|
||||
- Tabs should become valid when written, not be repaired later
|
||||
- This removes a feedback loop and makes state easier to trust
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- The effect at `packages/app/src/pages/session.tsx:141` is removed
|
||||
- Newly created and restored tabs are normalized before they enter local state
|
||||
- Tab rendering still matches current behavior for valid and edge-case inputs
|
||||
|
||||
### 2. Key Session-Owned State
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/pages/session.tsx:325`
|
||||
- `packages/app/src/pages/session.tsx:336`
|
||||
- `packages/app/src/pages/session.tsx:477`
|
||||
- `packages/app/src/pages/session.tsx:869`
|
||||
- `packages/app/src/pages/session.tsx:963`
|
||||
- `packages/app/src/pages/session/message-timeline.tsx:149`
|
||||
|
||||
Work:
|
||||
|
||||
- Identify state that should reset when `sessionKey` changes
|
||||
- Move that state under a keyed subtree or keyed owner boundary
|
||||
- Remove effects that watch `sessionKey` just to clear local state, refs, or temporary UI flags
|
||||
|
||||
Rationale:
|
||||
|
||||
- Session identity already defines the lifetime of this UI state
|
||||
- Keyed ownership makes reset behavior automatic and easier to reason about
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- The targeted reset effects are removed
|
||||
- Changing sessions resets only the intended session-local state
|
||||
- Scroll and editor state that should persist are not accidentally reset
|
||||
|
||||
### 3. Derive Workspace Order
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/pages/layout.tsx:557`
|
||||
|
||||
Work:
|
||||
|
||||
- Stop writing `workspaceOrder` from live workspace data in an effect
|
||||
- Represent user overrides separately from live workspace data
|
||||
- Compute effective order from current data plus overrides with a memo or pure helper
|
||||
|
||||
Rationale:
|
||||
|
||||
- Persisted user intent and live source data should not mirror each other through an effect
|
||||
- A computed effective order avoids drift and racey resync behavior
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- The effect at `packages/app/src/pages/layout.tsx:557` is removed
|
||||
- Workspace order updates correctly when workspaces appear, disappear, or are reordered by the user
|
||||
- User overrides persist without requiring a sync-back effect
|
||||
|
||||
### 4. Remove Child-Store Mirrors
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/context/global-sync.tsx:130`
|
||||
- `packages/app/src/context/global-sync.tsx:138`
|
||||
- `packages/app/src/context/global-sync.tsx:148`
|
||||
- `packages/app/src/context/global-sync/child-store.ts:184`
|
||||
- `packages/app/src/context/global-sync/child-store.ts:190`
|
||||
- `packages/app/src/context/global-sync/child-store.ts:193`
|
||||
- `packages/app/src/context/layout.tsx:424`
|
||||
|
||||
Work:
|
||||
|
||||
- Trace the actual ownership of global and child store values
|
||||
- Replace hydration and mirror effects with explicit initialization and direct updates
|
||||
- Remove the `globalStore.reload` event-bus pattern and call the needed reload paths directly
|
||||
|
||||
Rationale:
|
||||
|
||||
- Mirrors make it hard to tell which state is authoritative
|
||||
- Event-bus style state toggles hide control flow and create accidental reruns
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Child store hydration no longer depends on effect-based copying
|
||||
- Reload work can be followed from the event source to the handler without a reactive relay
|
||||
- State remains correct on first load, child creation, and subsequent updates
|
||||
|
||||
### 5. Key File-Scoped State
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/context/file.tsx:100`
|
||||
|
||||
Work:
|
||||
|
||||
- Move file-scoped local state under a boundary keyed by `scope()`
|
||||
- Remove any effect that watches `scope()` only to reset file-local state
|
||||
|
||||
Rationale:
|
||||
|
||||
- File scope changes are identity changes
|
||||
- Keyed ownership gives a cleaner reset than manual clear logic
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- The effect at `packages/app/src/context/file.tsx:100` is removed
|
||||
- Switching scopes resets only scope-local state
|
||||
- No previous-scope data appears after a scope change
|
||||
|
||||
### 6. Split Layout Side Effects
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/pages/layout.tsx:1489`
|
||||
- Related event-driven effects near `packages/app/src/pages/layout.tsx:484`, `:652`, `:776`, `:1519`
|
||||
|
||||
Work:
|
||||
|
||||
- Break the mixed-responsibility effect at `:1489` into direct actions and smaller bridge effects only where required
|
||||
- Move user-triggered branches into the actual command or handler that causes them
|
||||
- Remove any branch that only exists because one effect is handling unrelated concerns
|
||||
|
||||
Rationale:
|
||||
|
||||
- Mixed effects hide cause and make reruns hard to predict
|
||||
- Smaller units reduce accidental coupling and make future cleanup safer
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- The effect at `packages/app/src/pages/layout.tsx:1489` no longer mixes unrelated responsibilities
|
||||
- Event-driven branches execute from direct handlers
|
||||
- Remaining effects in this area each have one clear external sync purpose
|
||||
|
||||
### 7. Remove Duplicate Triggers
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/pages/session/review-tab.tsx:122`
|
||||
- `packages/app/src/pages/session/review-tab.tsx:130`
|
||||
- `packages/app/src/pages/session/review-tab.tsx:138`
|
||||
- `packages/app/src/pages/session/file-tabs.tsx:367`
|
||||
- `packages/app/src/pages/session/file-tabs.tsx:378`
|
||||
- `packages/app/src/pages/session/file-tabs.tsx:389`
|
||||
- `packages/app/src/pages/session/use-session-hash-scroll.ts:144`
|
||||
- `packages/app/src/pages/session/use-session-hash-scroll.ts:149`
|
||||
- `packages/app/src/pages/session/use-session-hash-scroll.ts:167`
|
||||
|
||||
Work:
|
||||
|
||||
- Extract one explicit imperative function per behavior
|
||||
- Call that function from each source event instead of replicating the same effect pattern multiple times
|
||||
- Preserve the scroll-sync effect that is truly syncing with the DOM, but remove duplicate trigger scaffolding around it
|
||||
|
||||
Rationale:
|
||||
|
||||
- Duplicate triggers make it easy to miss a case or fire twice
|
||||
- One named action is easier to test and reason about
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Repeated imperative effect triplets are collapsed into shared functions
|
||||
- Scroll behavior still works, including hash-based navigation
|
||||
- No duplicate firing is introduced
|
||||
|
||||
### 8. Make Prompt Filtering Reactive
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/components/prompt-input.tsx:652`
|
||||
- Keep `packages/app/src/components/prompt-input.tsx:690` as needed
|
||||
|
||||
Work:
|
||||
|
||||
- Convert slash filtering into a pure reactive derivation from the current input and candidate command list
|
||||
- Keep only the editor or DOM bridge effect if it is still needed for imperative syncing
|
||||
|
||||
Rationale:
|
||||
|
||||
- Filtering is classic derived state
|
||||
- It should not need an effect if it can be computed from current inputs
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- The effect at `packages/app/src/components/prompt-input.tsx:652` is removed
|
||||
- Filtered slash-command results update correctly as the input changes
|
||||
- The editor sync effect at `:690` still behaves correctly
|
||||
|
||||
### 9. Clean Up Smaller Derived-State Cases
|
||||
|
||||
Files:
|
||||
|
||||
- `packages/app/src/components/terminal.tsx:261`
|
||||
- `packages/app/src/components/session/session-header.tsx:309`
|
||||
|
||||
Work:
|
||||
|
||||
- Replace effect-written local state with memos or inline derivation
|
||||
- Remove intermediate setters when the value can be computed directly
|
||||
|
||||
Rationale:
|
||||
|
||||
- These are low-risk wins that reinforce the same pattern
|
||||
- They also help keep follow-up cleanup consistent
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Targeted effects are removed
|
||||
- UI output remains unchanged under the same inputs
|
||||
|
||||
## Verification And Regression Checks
|
||||
|
||||
Run focused checks after each phase, not only at the end.
|
||||
|
||||
### Suggested Verification
|
||||
|
||||
- Switch between sessions rapidly and confirm local session UI resets only where intended
|
||||
- Open, close, and reorder tabs and confirm order and normalization remain stable
|
||||
- Change workspaces, reload workspace data, and verify effective ordering is correct
|
||||
- Change file scope and confirm stale file state does not bleed across scopes
|
||||
- Trigger layout actions that previously depended on effects and confirm they still fire once
|
||||
- Use slash commands in the prompt and verify filtering updates as you type
|
||||
- Test review tab, file tab, and hash-scroll flows for duplicate or missing triggers
|
||||
- Verify global sync initialization, reload, and child-store creation paths
|
||||
|
||||
### Regression Checks
|
||||
|
||||
- No accidental infinite reruns
|
||||
- No double-firing network or command actions
|
||||
- No lost cleanup for listeners, timers, or scroll handlers
|
||||
- No preserved stale state after identity changes
|
||||
- No removed effect that was actually bridging to DOM or an external API
|
||||
|
||||
If available, add or update tests around pure helpers introduced during this cleanup.
|
||||
|
||||
Favor tests for derived ordering, normalization, and action extraction, since those are easiest to lock down.
|
||||
|
||||
## Definition Of Done
|
||||
|
||||
This work is done when all of the following are true:
|
||||
|
||||
- The highest-leverage targets in this spec are implemented
|
||||
- Each removed effect has been replaced by a clearer pattern: memo, keyed boundary, direct action, or lifecycle hook
|
||||
- The "should remain" effects still exist only where they serve a real external sync purpose
|
||||
- Touched files have fewer mixed-responsibility effects and clearer ownership of state
|
||||
- Manual verification covers session switching, file scope changes, workspace ordering, prompt filtering, and reload flows
|
||||
- No behavior regressions are found in the targeted areas
|
||||
|
||||
A reduced raw `createEffect` count is helpful, but it is not the main success metric.
|
||||
|
||||
The main success metric is clearer ownership and fewer effect-driven state repairs.
|
||||
|
||||
## Risks And Rollout Notes
|
||||
|
||||
Main risks:
|
||||
|
||||
- Keyed remounts can reset too much if state boundaries are drawn too high
|
||||
- Store mirror removal can break initialization order if ownership is not mapped first
|
||||
- Moving event work out of effects can accidentally skip triggers that were previously implicit
|
||||
|
||||
Rollout notes:
|
||||
|
||||
- Land in small phases, with each phase keeping the app behaviorally stable
|
||||
- Prefer isolated PRs by phase or by file cluster, especially for context-store changes
|
||||
- Review each remaining effect in touched files and leave it only if it clearly bridges to something external
|
||||
@@ -145,7 +145,6 @@ try {
|
||||
Object.assign(process.env, serverEnv)
|
||||
process.env.AGENT = "1"
|
||||
process.env.OPENCODE = "1"
|
||||
process.env.OPENCODE_PID = String(process.pid)
|
||||
|
||||
const log = await import("../../opencode/src/util/log")
|
||||
const install = await import("../../opencode/src/installation")
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
@@ -446,7 +447,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
>
|
||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="px-2.5 flex gap-4 items-center">
|
||||
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<div class="text-16-medium text-text-strong">
|
||||
<Switch>
|
||||
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
@@ -94,7 +95,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||
|
||||
@@ -5,12 +5,18 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||
|
||||
const CUSTOM_ID = "_custom"
|
||||
|
||||
function icon(id: string): IconName {
|
||||
if (iconNames.includes(id as IconName)) return id as IconName
|
||||
return "synthetic"
|
||||
}
|
||||
|
||||
export const DialogSelectProvider: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
@@ -63,7 +69,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
>
|
||||
{(i) => (
|
||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={icon(i.id)} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||
|
||||
@@ -325,6 +325,12 @@ export default function FileTree(props: {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const dir = file.tree.state(props.path)
|
||||
if (!shouldListExpanded({ level, dir })) return
|
||||
void file.tree.list(props.path)
|
||||
})
|
||||
|
||||
const nodes = createMemo(() => {
|
||||
const nodes = file.tree.children(props.path)
|
||||
const current = filter()
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
@@ -591,6 +592,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
setActive: setSlashActive,
|
||||
onInput: slashOnInput,
|
||||
onKeyDown: slashOnKeyDown,
|
||||
refetch: slashRefetch,
|
||||
} = useFilteredList<SlashCommand>({
|
||||
items: slashCommands,
|
||||
key: (x) => x?.id,
|
||||
@@ -647,6 +649,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sync.data.command,
|
||||
() => slashRefetch(),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
// Auto-scroll active command into view when navigating with keyboard
|
||||
createEffect(() => {
|
||||
const activeId = slashActive()
|
||||
@@ -1388,7 +1398,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id}
|
||||
id={local.model.current()!.provider.id as IconName}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
@@ -1418,7 +1428,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id}
|
||||
id={local.model.current()!.provider.id as IconName}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
|
||||
@@ -306,10 +306,11 @@ export function SessionHeader() {
|
||||
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
|
||||
const opening = createMemo(() => openRequest.app !== undefined)
|
||||
|
||||
const selectApp = (app: OpenApp) => {
|
||||
if (!options().some((item) => item.id === app)) return
|
||||
setPrefs("app", app)
|
||||
}
|
||||
createEffect(() => {
|
||||
const value = prefs.app
|
||||
if (options().some((o) => o.id === value)) return
|
||||
setPrefs("app", options()[0]?.id ?? "finder")
|
||||
})
|
||||
|
||||
const openDir = (app: OpenApp) => {
|
||||
if (opening() || !canOpen() || !platform.openPath) return
|
||||
@@ -457,7 +458,7 @@ export function SessionHeader() {
|
||||
value={current().id}
|
||||
onChange={(value) => {
|
||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||
selectApp(value as OpenApp)
|
||||
setPrefs("app", value as OpenApp)
|
||||
}}
|
||||
>
|
||||
<For each={options()}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useModels } from "@/context/models"
|
||||
@@ -97,7 +98,7 @@ export const SettingsModels: Component = () => {
|
||||
{(group) => (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={group.category as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
|
||||
</div>
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { createMemo, type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -32,6 +33,11 @@ export const SettingsProviders: Component = () => {
|
||||
const globalSync = useGlobalSync()
|
||||
const providers = useProviders()
|
||||
|
||||
const icon = (id: string): IconName => {
|
||||
if (iconNames.includes(id as IconName)) return id as IconName
|
||||
return "synthetic"
|
||||
}
|
||||
|
||||
const connected = createMemo(() => {
|
||||
return providers
|
||||
.connected()
|
||||
@@ -148,7 +154,7 @@ export const SettingsProviders: Component = () => {
|
||||
{(item) => (
|
||||
<div class="group flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
|
||||
<Tag>{type(item)}</Tag>
|
||||
</div>
|
||||
@@ -179,7 +185,7 @@ export const SettingsProviders: Component = () => {
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<span class="text-14-regular text-text-weak">
|
||||
@@ -222,7 +228,7 @@ export const SettingsProviders: Component = () => {
|
||||
>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
|
||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
||||
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { type ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { matchKeybind, parseKeybind } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -219,7 +219,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const terminalColors = createMemo(getTerminalColors)
|
||||
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
|
||||
|
||||
const scheduleFit = () => {
|
||||
if (disposed) return
|
||||
@@ -259,7 +259,8 @@ export const Terminal = (props: TerminalProps) => {
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const colors = terminalColors()
|
||||
const colors = getTerminalColors()
|
||||
setTerminalColors(colors)
|
||||
if (!term) return
|
||||
setOptionIfSupported(term, "theme", colors)
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
getOwner,
|
||||
Match,
|
||||
onCleanup,
|
||||
@@ -34,6 +35,7 @@ import { trimSessions } from "./global-sync/session-trim"
|
||||
import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
import { sanitizeProject } from "./global-sync/utils"
|
||||
import { usePlatform } from "./platform"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
|
||||
type GlobalStore = {
|
||||
@@ -52,6 +54,7 @@ type GlobalStore = {
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
@@ -61,7 +64,7 @@ function createGlobalSync() {
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
const sessionMeta = new Map<string, { limit: number }>()
|
||||
|
||||
const [projectCache, setProjectCache, projectInit] = persisted(
|
||||
const [projectCache, setProjectCache, , projectCacheReady] = persisted(
|
||||
Persist.global("globalSync.project", ["globalSync.project.v1"]),
|
||||
createStore({ value: [] as Project[] }),
|
||||
)
|
||||
@@ -77,57 +80,6 @@ function createGlobalSync() {
|
||||
reload: undefined,
|
||||
})
|
||||
|
||||
let active = true
|
||||
let projectWritten = false
|
||||
|
||||
onCleanup(() => {
|
||||
active = false
|
||||
})
|
||||
|
||||
const cacheProjects = () => {
|
||||
setProjectCache(
|
||||
"value",
|
||||
untrack(() => globalStore.project.map(sanitizeProject)),
|
||||
)
|
||||
}
|
||||
|
||||
const setProjects = (next: Project[] | ((draft: Project[]) => void)) => {
|
||||
projectWritten = true
|
||||
if (typeof next === "function") {
|
||||
setGlobalStore("project", produce(next))
|
||||
cacheProjects()
|
||||
return
|
||||
}
|
||||
setGlobalStore("project", next)
|
||||
cacheProjects()
|
||||
}
|
||||
|
||||
const setBootStore = ((...input: unknown[]) => {
|
||||
if (input[0] === "project" && Array.isArray(input[1])) {
|
||||
setProjects(input[1] as Project[])
|
||||
return input[1]
|
||||
}
|
||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||
}) as typeof setGlobalStore
|
||||
|
||||
const set = ((...input: unknown[]) => {
|
||||
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
||||
setProjects(input[1] as Project[] | ((draft: Project[]) => void))
|
||||
return input[1]
|
||||
}
|
||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||
}) as typeof setGlobalStore
|
||||
|
||||
if (projectInit instanceof Promise) {
|
||||
void projectInit.then(() => {
|
||||
if (!active) return
|
||||
if (projectWritten) return
|
||||
const cached = projectCache.value
|
||||
if (cached.length === 0) return
|
||||
setGlobalStore("project", cached)
|
||||
})
|
||||
}
|
||||
|
||||
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
||||
if (!sessionID) return
|
||||
if (!todos) {
|
||||
@@ -175,6 +127,30 @@ function createGlobalSync() {
|
||||
return sdk
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!projectCacheReady()) return
|
||||
if (globalStore.project.length !== 0) return
|
||||
const cached = projectCache.value
|
||||
if (cached.length === 0) return
|
||||
setGlobalStore("project", cached)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!projectCacheReady()) return
|
||||
const projects = globalStore.project
|
||||
if (projects.length === 0) {
|
||||
const cachedLength = untrack(() => projectCache.value.length)
|
||||
if (cachedLength !== 0) return
|
||||
}
|
||||
setProjectCache("value", projects.map(sanitizeProject))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (globalStore.reload !== "complete") return
|
||||
setGlobalStore("reload", undefined)
|
||||
queue.refresh()
|
||||
})
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
const pending = sessionLoads.get(directory)
|
||||
if (pending) return pending
|
||||
@@ -283,7 +259,13 @@ function createGlobalSync() {
|
||||
event,
|
||||
project: globalStore.project,
|
||||
refresh: queue.refresh,
|
||||
setGlobalProject: setProjects,
|
||||
setGlobalProject(next) {
|
||||
if (typeof next === "function") {
|
||||
setGlobalStore("project", produce(next))
|
||||
return
|
||||
}
|
||||
setGlobalStore("project", next)
|
||||
},
|
||||
})
|
||||
if (event.type === "server.connected" || event.type === "global.disposed") {
|
||||
for (const directory of Object.keys(children.children)) {
|
||||
@@ -334,7 +316,7 @@ function createGlobalSync() {
|
||||
unknownError: language.t("error.chain.unknown"),
|
||||
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
setGlobalStore: setBootStore,
|
||||
setGlobalStore,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -358,9 +340,7 @@ function createGlobalSync() {
|
||||
.update({ config })
|
||||
.then(bootstrap)
|
||||
.then(() => {
|
||||
queue.refresh()
|
||||
setGlobalStore("reload", undefined)
|
||||
queue.refresh()
|
||||
setGlobalStore("reload", "complete")
|
||||
})
|
||||
.catch((error) => {
|
||||
setGlobalStore("reload", undefined)
|
||||
@@ -370,7 +350,7 @@ function createGlobalSync() {
|
||||
|
||||
return {
|
||||
data: globalStore,
|
||||
set,
|
||||
set: setGlobalStore,
|
||||
get ready() {
|
||||
return globalStore.ready
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||
import { createRoot, createEffect, getOwner, onCleanup, runWithOwner, type Accessor, type Owner } from "solid-js"
|
||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
@@ -131,7 +131,8 @@ export function createChildStoreManager(input: {
|
||||
)
|
||||
if (!vcs) throw new Error("Failed to create persisted cache")
|
||||
const vcsStore = vcs[0]
|
||||
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
||||
const vcsReady = vcs[3]
|
||||
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcsReady })
|
||||
|
||||
const meta = runWithOwner(input.owner, () =>
|
||||
persisted(
|
||||
@@ -153,12 +154,10 @@ export function createChildStoreManager(input: {
|
||||
|
||||
const init = () =>
|
||||
createRoot((dispose) => {
|
||||
const initialMeta = meta[0].value
|
||||
const initialIcon = icon[0].value
|
||||
const child = createStore<State>({
|
||||
project: "",
|
||||
projectMeta: initialMeta,
|
||||
icon: initialIcon,
|
||||
projectMeta: meta[0].value,
|
||||
icon: icon[0].value,
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
@@ -182,27 +181,16 @@ export function createChildStoreManager(input: {
|
||||
children[directory] = child
|
||||
disposers.set(directory, dispose)
|
||||
|
||||
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
||||
if (!(init instanceof Promise)) return
|
||||
void init.then(() => {
|
||||
if (children[directory] !== child) return
|
||||
run()
|
||||
})
|
||||
}
|
||||
|
||||
onPersistedInit(vcs[2], () => {
|
||||
createEffect(() => {
|
||||
if (!vcsReady()) return
|
||||
const cached = vcsStore.value
|
||||
if (!cached?.branch) return
|
||||
child[1]("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
onPersistedInit(meta[2], () => {
|
||||
if (child[0].projectMeta !== initialMeta) return
|
||||
createEffect(() => {
|
||||
child[1]("projectMeta", meta[0].value)
|
||||
})
|
||||
|
||||
onPersistedInit(icon[2], () => {
|
||||
if (child[0].icon !== initialIcon) return
|
||||
createEffect(() => {
|
||||
child[1]("icon", icon[0].value)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,10 +7,8 @@ import { useServer } from "./server"
|
||||
import { usePlatform } from "./platform"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { same } from "@/utils/same"
|
||||
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
||||
import { createPathHelpers } from "./file/path"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
const DEFAULT_PANEL_WIDTH = 344
|
||||
@@ -98,38 +96,6 @@ function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string):
|
||||
return { all, active: tab }
|
||||
}
|
||||
|
||||
const sessionPath = (key: string) => {
|
||||
const dir = key.split("/")[0]
|
||||
if (!dir) return
|
||||
const root = decode64(dir)
|
||||
if (!root) return
|
||||
return createPathHelpers(() => root)
|
||||
}
|
||||
|
||||
const normalizeSessionTab = (path: ReturnType<typeof createPathHelpers> | undefined, tab: string) => {
|
||||
if (!tab.startsWith("file://")) return tab
|
||||
if (!path) return tab
|
||||
return path.tab(tab)
|
||||
}
|
||||
|
||||
const normalizeSessionTabList = (path: ReturnType<typeof createPathHelpers> | undefined, all: string[]) => {
|
||||
const seen = new Set<string>()
|
||||
return all.flatMap((tab) => {
|
||||
const value = normalizeSessionTab(path, tab)
|
||||
if (seen.has(value)) return []
|
||||
seen.add(value)
|
||||
return [value]
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
||||
const path = sessionPath(key)
|
||||
return {
|
||||
all: normalizeSessionTabList(path, tabs.all),
|
||||
active: tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active,
|
||||
}
|
||||
}
|
||||
|
||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||
name: "Layout",
|
||||
init: () => {
|
||||
@@ -181,46 +147,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
})()
|
||||
|
||||
const sessionTabs = value.sessionTabs
|
||||
const migratedSessionTabs = (() => {
|
||||
if (!isRecord(sessionTabs)) return sessionTabs
|
||||
|
||||
let changed = false
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(sessionTabs).map(([key, tabs]) => {
|
||||
if (!isRecord(tabs) || !Array.isArray(tabs.all)) return [key, tabs]
|
||||
|
||||
const current = {
|
||||
all: tabs.all.filter((tab): tab is string => typeof tab === "string"),
|
||||
active: typeof tabs.active === "string" ? tabs.active : undefined,
|
||||
}
|
||||
const normalized = normalizeStoredSessionTabs(key, current)
|
||||
if (current.all.length !== tabs.all.length) changed = true
|
||||
if (!same(current.all, normalized.all) || current.active !== normalized.active) changed = true
|
||||
if (tabs.active !== undefined && typeof tabs.active !== "string") changed = true
|
||||
return [key, normalized]
|
||||
}),
|
||||
)
|
||||
|
||||
if (!changed) return sessionTabs
|
||||
return next
|
||||
})()
|
||||
|
||||
if (
|
||||
migratedSidebar === sidebar &&
|
||||
migratedReview === review &&
|
||||
migratedFileTree === fileTree &&
|
||||
migratedSessionTabs === sessionTabs
|
||||
) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (migratedSidebar === sidebar && migratedReview === review && migratedFileTree === fileTree) return value
|
||||
return {
|
||||
...value,
|
||||
sidebar: migratedSidebar,
|
||||
review: migratedReview,
|
||||
fileTree: migratedFileTree,
|
||||
sessionTabs: migratedSessionTabs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,26 +745,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
tabs(sessionKey: string | Accessor<string>) {
|
||||
const key = createSessionKeyReader(sessionKey, ensureKey)
|
||||
const path = createMemo(() => sessionPath(key()))
|
||||
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
||||
const normalize = (tab: string) => normalizeSessionTab(path(), tab)
|
||||
const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all)
|
||||
return {
|
||||
tabs,
|
||||
active: createMemo(() => tabs().active),
|
||||
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
|
||||
setActive(tab: string | undefined) {
|
||||
const session = key()
|
||||
const next = tab ? normalize(tab) : tab
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: [], active: next })
|
||||
setStore("sessionTabs", session, { all: [], active: tab })
|
||||
} else {
|
||||
setStore("sessionTabs", session, "active", next)
|
||||
setStore("sessionTabs", session, "active", tab)
|
||||
}
|
||||
},
|
||||
setAll(all: string[]) {
|
||||
const session = key()
|
||||
const next = normalizeAll(all).filter((tab) => tab !== "review")
|
||||
const next = all.filter((tab) => tab !== "review")
|
||||
if (!store.sessionTabs[session]) {
|
||||
setStore("sessionTabs", session, { all: next, active: undefined })
|
||||
} else {
|
||||
@@ -841,7 +769,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
async open(tab: string) {
|
||||
const session = key()
|
||||
const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab))
|
||||
const next = nextSessionTabsForOpen(store.sessionTabs[session], tab)
|
||||
setStore("sessionTabs", session, next)
|
||||
},
|
||||
close(tab: string) {
|
||||
|
||||
@@ -43,11 +43,12 @@ type OptimisticRemoveInput = {
|
||||
|
||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (!messages) {
|
||||
draft.message[input.sessionID] = [input.message]
|
||||
}
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||
messages.splice(result.index, 0, input.message)
|
||||
} else {
|
||||
draft.message[input.sessionID] = [input.message]
|
||||
}
|
||||
draft.part[input.message.id] = sortParts(input.parts)
|
||||
}
|
||||
@@ -104,7 +105,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return globalSync.child(directory)
|
||||
}
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const messagePageSize = 200
|
||||
const messagePageSize = 400
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
@@ -121,12 +122,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return undefined
|
||||
}
|
||||
|
||||
const limitFor = (count: number) => {
|
||||
if (count <= messagePageSize) return messagePageSize
|
||||
return Math.ceil(count / messagePageSize) * messagePageSize
|
||||
}
|
||||
|
||||
const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => {
|
||||
const messages = await retry(() =>
|
||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }),
|
||||
)
|
||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
|
||||
const session = items
|
||||
.map((x) => x.info)
|
||||
.filter((m) => !!m?.id)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
||||
return {
|
||||
session,
|
||||
@@ -150,8 +159,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.then((next) => {
|
||||
batch(() => {
|
||||
input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
|
||||
for (const p of next.part) {
|
||||
input.setStore("part", p.id, p.part)
|
||||
for (const message of next.part) {
|
||||
input.setStore("part", message.id, reconcile(message.part, { key: "id" }))
|
||||
}
|
||||
setMeta("limit", key, input.limit)
|
||||
setMeta("complete", key, next.complete)
|
||||
@@ -220,9 +229,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
||||
const hasSession = (() => {
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
return match.found
|
||||
})()
|
||||
|
||||
const limit = meta.limit[key] ?? messagePageSize
|
||||
const hasMessages = store.message[sessionID] !== undefined
|
||||
const hydrated = meta.limit[key] !== undefined
|
||||
if (hasSession && hasMessages && hydrated) return
|
||||
|
||||
const count = store.message[sessionID]?.length ?? 0
|
||||
const limit = hydrated ? (meta.limit[key] ?? messagePageSize) : limitFor(count)
|
||||
|
||||
const sessionReq = hasSession
|
||||
? Promise.resolve()
|
||||
@@ -242,13 +259,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
|
||||
const messagesReq = loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
const messagesReq =
|
||||
hasMessages && hydrated
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
|
||||
return runInflight(inflight, key, () => Promise.all([sessionReq, messagesReq]).then(() => {}))
|
||||
},
|
||||
@@ -270,14 +290,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const existing = store.todo[sessionID]
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (existing !== undefined) {
|
||||
if (cached === undefined) {
|
||||
if (globalSync.data.session_todo[sessionID] === undefined) {
|
||||
globalSync.todo.set(sessionID, existing)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (cached !== undefined) {
|
||||
setStore("todo", sessionID, reconcile(cached, { key: "id" }))
|
||||
}
|
||||
@@ -304,12 +324,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const key = keyFor(sdk.directory, sessionID)
|
||||
return meta.loading[key] ?? false
|
||||
},
|
||||
async loadMore(sessionID: string, count?: number) {
|
||||
async loadMore(sessionID: string, count = messagePageSize) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const step = count ?? messagePageSize
|
||||
if (meta.loading[key]) return
|
||||
if (meta.complete[key]) return
|
||||
|
||||
@@ -319,7 +338,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit: currentLimit + step,
|
||||
limit: currentLimit + count,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -119,7 +119,7 @@ export const dict = {
|
||||
"dialog.model.manage.description": "Model seçicide hangi modellerin görüneceğini özelleştirin.",
|
||||
"dialog.model.manage.provider.toggle": "Tüm {{provider}} modellerini aç/kapat",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode'un sunduğu ücretsiz modeller",
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode tarafından sunulan ücretsiz modeller",
|
||||
"dialog.model.unpaid.addMore.title": "Popüler sağlayıcılardan daha fazla model ekleyin",
|
||||
|
||||
"dialog.provider.viewAll": "Daha fazla sağlayıcı göster",
|
||||
@@ -195,7 +195,7 @@ export const dict = {
|
||||
"provider.custom.error.baseURL.required": "Temel URL gerekli",
|
||||
"provider.custom.error.baseURL.format": "http:// veya https:// ile başlamalı",
|
||||
"provider.custom.error.required": "Gerekli",
|
||||
"provider.custom.error.duplicate": "Çakışma",
|
||||
"provider.custom.error.duplicate": "Tekrar",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} bağlantısı kesildi",
|
||||
"provider.disconnect.toast.disconnected.description": "{{provider}} modelleri artık kullanılabilir değil.",
|
||||
@@ -252,7 +252,7 @@ export const dict = {
|
||||
"prompt.example.10": "API dokümantasyonu oluştur",
|
||||
"prompt.example.11": "Veritabanı sorgularını optimize et",
|
||||
"prompt.example.12": "Girdi doğrulama ekle",
|
||||
"prompt.example.13": "... için yeni bir bileşen oluştur",
|
||||
"prompt.example.13": "İçin yeni bir bileşen oluştur...",
|
||||
"prompt.example.14": "Bu projeyi nasıl dağıtabilirim?",
|
||||
"prompt.example.15": "Kodumu en iyi uygulamalar için incele",
|
||||
"prompt.example.16": "Bu fonksiyona hata yönetimi ekle",
|
||||
@@ -263,13 +263,13 @@ export const dict = {
|
||||
"prompt.example.21": "Bir göç betiği yazmama yardım et",
|
||||
"prompt.example.22": "Bu uç nokta için önbellekleme uygula",
|
||||
"prompt.example.23": "Bu listeye sayfalama ekle",
|
||||
"prompt.example.24": "... için bir CLI komutu oluştur",
|
||||
"prompt.example.24": "İçin bir CLI komutu oluştur...",
|
||||
"prompt.example.25": "Ortam değişkenleri burada nasıl çalışıyor?",
|
||||
|
||||
"prompt.popover.emptyResults": "Eşleşen sonuç yok",
|
||||
"prompt.popover.emptyCommands": "Eşleşen komut yok",
|
||||
"prompt.dropzone.label": "Görsel veya PDF'leri buraya bırakın",
|
||||
"prompt.dropzone.file.label": "Dosyayı referans göstermek için bırakın",
|
||||
"prompt.dropzone.file.label": "@bahsetmek için dosyayı bırakın",
|
||||
"prompt.slash.badge.custom": "özel",
|
||||
"prompt.slash.badge.skill": "beceri",
|
||||
"prompt.slash.badge.mcp": "mcp",
|
||||
@@ -343,7 +343,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.recommended": "Önerilen: 128x128px",
|
||||
"dialog.project.edit.color": "Renk",
|
||||
"dialog.project.edit.color.select": "{{color}} rengini seç",
|
||||
"dialog.project.edit.worktree.startup": "Çalışma ağacı başlatma betiği",
|
||||
"dialog.project.edit.worktree.startup": "Çalışma alanı başlatma betiği",
|
||||
"dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "örneğin bun install",
|
||||
|
||||
@@ -454,7 +454,7 @@ export const dict = {
|
||||
"error.page.version": "Sürüm: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da ID özelliği yanlış mı yazıldı?",
|
||||
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?",
|
||||
|
||||
"error.globalSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?",
|
||||
"directory.error.invalidUrl": "URL'de geçersiz dizin.",
|
||||
|
||||
@@ -59,11 +59,11 @@ import { useLanguage, type Locale } from "@/context/language"
|
||||
import {
|
||||
childMapByParent,
|
||||
displayName,
|
||||
effectiveWorkspaceOrder,
|
||||
errorMessage,
|
||||
getDraggableId,
|
||||
latestRootSession,
|
||||
sortedRootSessions,
|
||||
syncWorkspaceOrder,
|
||||
workspaceKey,
|
||||
} from "./layout/helpers"
|
||||
import { collectOpenProjectDeepLinks, deepLinkEvent, drainPendingDeepLinks } from "./layout/deep-links"
|
||||
@@ -481,6 +481,21 @@ export default function Layout(props: ParentProps) {
|
||||
return projects.find((p) => p.worktree === root)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => ({ ready: pageReady(), project: currentProject() }),
|
||||
(value) => {
|
||||
if (!value.ready) return
|
||||
const project = value.project
|
||||
if (!project) return
|
||||
const last = server.projects.last()
|
||||
if (last === project.worktree) return
|
||||
server.projects.touch(project.worktree)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => ({ ready: pageReady(), layoutReady: layoutReady(), dir: params.dir, list: layout.projects.list() }),
|
||||
@@ -539,17 +554,29 @@ export default function Layout(props: ParentProps) {
|
||||
return layout.sidebar.workspaces(project.worktree)()
|
||||
})
|
||||
|
||||
const visibleSessionDirs = createMemo(() => {
|
||||
createEffect(() => {
|
||||
if (!pageReady()) return
|
||||
if (!layoutReady()) return
|
||||
const project = currentProject()
|
||||
if (!project) return [] as string[]
|
||||
if (!workspaceSetting()) return [project.worktree]
|
||||
if (!project) return
|
||||
|
||||
const activeDir = currentDir()
|
||||
return workspaceIds(project).filter((directory) => {
|
||||
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
||||
const active = directory === activeDir
|
||||
return expanded || active
|
||||
})
|
||||
const local = project.worktree
|
||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||
const existing = store.workspaceOrder[project.worktree]
|
||||
const merged = syncWorkspaceOrder(local, dirs, existing)
|
||||
if (!existing) {
|
||||
setStore("workspaceOrder", project.worktree, merged)
|
||||
return
|
||||
}
|
||||
|
||||
if (merged.length !== existing.length) {
|
||||
setStore("workspaceOrder", project.worktree, merged)
|
||||
return
|
||||
}
|
||||
|
||||
if (merged.some((d, i) => d !== existing[i])) {
|
||||
setStore("workspaceOrder", project.worktree, merged)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -566,17 +593,25 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
|
||||
const currentSessions = createMemo(() => {
|
||||
const project = currentProject()
|
||||
if (!project) return [] as Session[]
|
||||
const now = Date.now()
|
||||
const dirs = visibleSessionDirs()
|
||||
if (dirs.length === 0) return [] as Session[]
|
||||
|
||||
const result: Session[] = []
|
||||
for (const dir of dirs) {
|
||||
const [dirStore] = globalSync.child(dir, { bootstrap: true })
|
||||
const dirSessions = sortedRootSessions(dirStore, now)
|
||||
result.push(...dirSessions)
|
||||
if (workspaceSetting()) {
|
||||
const dirs = workspaceIds(project)
|
||||
const activeDir = currentDir()
|
||||
const result: Session[] = []
|
||||
for (const dir of dirs) {
|
||||
const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree
|
||||
const active = dir === activeDir
|
||||
if (!expanded && !active) continue
|
||||
const [dirStore] = globalSync.child(dir, { bootstrap: true })
|
||||
const dirSessions = sortedRootSessions(dirStore, now)
|
||||
result.push(...dirSessions)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return result
|
||||
const [projectStore] = globalSync.child(project.worktree)
|
||||
return sortedRootSessions(projectStore, now)
|
||||
})
|
||||
|
||||
type PrefetchQueue = {
|
||||
@@ -791,6 +826,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
navigateToSession(session)
|
||||
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
||||
}
|
||||
|
||||
function navigateSessionByUnseen(offset: number) {
|
||||
@@ -825,6 +861,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
navigateToSession(session)
|
||||
queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1057,41 +1094,12 @@ export default function Layout(props: ParentProps) {
|
||||
return meta?.worktree ?? directory
|
||||
}
|
||||
|
||||
function activeProjectRoot(directory: string) {
|
||||
return currentProject()?.worktree ?? projectRoot(directory)
|
||||
}
|
||||
|
||||
function touchProjectRoute() {
|
||||
const root = currentProject()?.worktree
|
||||
if (!root) return
|
||||
if (server.projects.last() !== root) server.projects.touch(root)
|
||||
return root
|
||||
}
|
||||
|
||||
function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
|
||||
setStore("lastProjectSession", root, { directory, id, at: Date.now() })
|
||||
return root
|
||||
}
|
||||
|
||||
function syncSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
|
||||
rememberSessionRoute(directory, id, root)
|
||||
notification.session.markViewed(id)
|
||||
const expanded = untrack(() => store.workspaceExpanded[directory])
|
||||
if (expanded === false) {
|
||||
setStore("workspaceExpanded", directory, true)
|
||||
}
|
||||
requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`))
|
||||
return root
|
||||
}
|
||||
|
||||
async function navigateToProject(directory: string | undefined) {
|
||||
if (!directory) return
|
||||
const root = projectRoot(directory)
|
||||
server.projects.touch(root)
|
||||
const project = layout.projects.list().find((item) => item.worktree === root)
|
||||
const dirs = project
|
||||
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
|
||||
: [root]
|
||||
const dirs = Array.from(new Set([root, ...(store.workspaceOrder[root] ?? []), ...(project?.sandboxes ?? [])]))
|
||||
const openSession = async (target: { directory: string; id: string }) => {
|
||||
const resolved = await globalSDK.client.session
|
||||
.get({ sessionID: target.id })
|
||||
@@ -1478,42 +1486,26 @@ export default function Layout(props: ParentProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const activeRoute = {
|
||||
session: "",
|
||||
sessionProject: "",
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [pageReady(), params.dir, params.id, currentProject()?.worktree] as const,
|
||||
([ready, dir, id]) => {
|
||||
if (!ready || !dir) {
|
||||
activeRoute.session = ""
|
||||
activeRoute.sessionProject = ""
|
||||
return
|
||||
}
|
||||
|
||||
() => ({ ready: pageReady(), dir: params.dir, id: params.id }),
|
||||
(value) => {
|
||||
if (!value.ready) return
|
||||
const dir = value.dir
|
||||
const id = value.id
|
||||
if (!dir || !id) return
|
||||
const directory = decode64(dir)
|
||||
if (!directory) return
|
||||
|
||||
const root = touchProjectRoute() ?? activeProjectRoot(directory)
|
||||
|
||||
if (!id) {
|
||||
activeRoute.session = ""
|
||||
activeRoute.sessionProject = ""
|
||||
return
|
||||
const at = Date.now()
|
||||
setStore("lastProjectSession", projectRoot(directory), { directory, id, at })
|
||||
notification.session.markViewed(id)
|
||||
const expanded = untrack(() => store.workspaceExpanded[directory])
|
||||
if (expanded === false) {
|
||||
setStore("workspaceExpanded", directory, true)
|
||||
}
|
||||
|
||||
const session = `${dir}/${id}`
|
||||
if (session !== activeRoute.session) {
|
||||
activeRoute.session = session
|
||||
activeRoute.sessionProject = syncSessionRoute(directory, id, root)
|
||||
return
|
||||
}
|
||||
|
||||
if (root === activeRoute.sessionProject) return
|
||||
activeRoute.sessionProject = rememberSessionRoute(directory, id, root)
|
||||
requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`))
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1524,29 +1516,40 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const loadedSessionDirs = new Set<string>()
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
visibleSessionDirs,
|
||||
(dirs) => {
|
||||
if (dirs.length === 0) {
|
||||
loadedSessionDirs.clear()
|
||||
return
|
||||
}
|
||||
createEffect(() => {
|
||||
const project = currentProject()
|
||||
const workspaces = workspaceSetting()
|
||||
const next = new Set<string>()
|
||||
if (!project) {
|
||||
loadedSessionDirs.clear()
|
||||
return
|
||||
}
|
||||
|
||||
const next = new Set(dirs)
|
||||
for (const directory of next) {
|
||||
if (loadedSessionDirs.has(directory)) continue
|
||||
globalSync.project.loadSessions(directory)
|
||||
}
|
||||
if (workspaces) {
|
||||
const activeDir = currentDir()
|
||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||
for (const directory of dirs) {
|
||||
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
||||
const active = directory === activeDir
|
||||
if (!expanded && !active) continue
|
||||
next.add(directory)
|
||||
}
|
||||
}
|
||||
|
||||
loadedSessionDirs.clear()
|
||||
for (const directory of next) {
|
||||
loadedSessionDirs.add(directory)
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
if (!workspaces) {
|
||||
next.add(project.worktree)
|
||||
}
|
||||
|
||||
for (const directory of next) {
|
||||
if (loadedSessionDirs.has(directory)) continue
|
||||
globalSync.project.loadSessions(directory)
|
||||
}
|
||||
|
||||
loadedSessionDirs.clear()
|
||||
for (const directory of next) {
|
||||
loadedSessionDirs.add(directory)
|
||||
}
|
||||
})
|
||||
|
||||
function handleDragStart(event: unknown) {
|
||||
const id = getDraggableId(event)
|
||||
@@ -1580,11 +1583,14 @@ export default function Layout(props: ParentProps) {
|
||||
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
|
||||
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||
|
||||
const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
|
||||
if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)]
|
||||
if (!extra) return ordered
|
||||
if (pending) return ordered
|
||||
return [...ordered, extra]
|
||||
const existing = store.workspaceOrder[project.worktree]
|
||||
if (!existing) return extra ? [...dirs, extra] : dirs
|
||||
|
||||
const merged = syncWorkspaceOrder(local, dirs, existing)
|
||||
if (pending && extra) return [local, extra, ...merged.filter((directory) => directory !== local)]
|
||||
if (!extra) return merged
|
||||
if (pending) return merged
|
||||
return [...merged, extra]
|
||||
}
|
||||
|
||||
const sidebarProject = createMemo(() => {
|
||||
@@ -1617,11 +1623,7 @@ export default function Layout(props: ParentProps) {
|
||||
const [item] = result.splice(fromIndex, 1)
|
||||
if (!item) return
|
||||
result.splice(toIndex, 0, item)
|
||||
setStore(
|
||||
"workspaceOrder",
|
||||
project.worktree,
|
||||
result.filter((directory) => workspaceKey(directory) !== workspaceKey(project.worktree)),
|
||||
)
|
||||
setStore("workspaceOrder", project.worktree, result)
|
||||
}
|
||||
|
||||
function handleWorkspaceDragEnd() {
|
||||
@@ -1659,9 +1661,10 @@ export default function Layout(props: ParentProps) {
|
||||
const existing = prev ?? []
|
||||
const next = existing.filter((item) => {
|
||||
const id = workspaceKey(item)
|
||||
return id !== root && id !== key
|
||||
if (id === root) return false
|
||||
return id !== key
|
||||
})
|
||||
return [created.directory, ...next]
|
||||
return [local, created.directory, ...next]
|
||||
})
|
||||
|
||||
globalSync.child(created.directory)
|
||||
|
||||
@@ -74,29 +74,9 @@ export const errorMessage = (err: unknown, fallback: string) => {
|
||||
return fallback
|
||||
}
|
||||
|
||||
export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => {
|
||||
const root = workspaceKey(local)
|
||||
const live = new Map<string, string>()
|
||||
|
||||
for (const dir of dirs) {
|
||||
const key = workspaceKey(dir)
|
||||
if (key === root) continue
|
||||
if (!live.has(key)) live.set(key, dir)
|
||||
}
|
||||
|
||||
if (!persisted?.length) return [local, ...live.values()]
|
||||
|
||||
const result = [local]
|
||||
for (const dir of persisted) {
|
||||
const key = workspaceKey(dir)
|
||||
if (key === root) continue
|
||||
const match = live.get(key)
|
||||
if (!match) continue
|
||||
result.push(match)
|
||||
live.delete(key)
|
||||
}
|
||||
|
||||
return [...result, ...live.values()]
|
||||
export const syncWorkspaceOrder = (local: string, dirs: string[], existing?: string[]) => {
|
||||
if (!existing) return dirs
|
||||
const keep = existing.filter((d) => d !== local && dirs.includes(d))
|
||||
const missing = dirs.filter((d) => d !== local && !existing.includes(d))
|
||||
return [local, ...missing, ...keep]
|
||||
}
|
||||
|
||||
export const syncWorkspaceOrder = effectiveWorkspaceOrder
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount, untrack } from "solid-js"
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -32,215 +32,6 @@ import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
|
||||
type SessionHistoryWindowInput = {
|
||||
sessionID: () => string | undefined
|
||||
messagesReady: () => boolean
|
||||
visibleUserMessages: () => UserMessage[]
|
||||
historyMore: () => boolean
|
||||
historyLoading: () => boolean
|
||||
loadMore: (sessionID: string) => Promise<void>
|
||||
userScrolled: () => boolean
|
||||
scroller: () => HTMLDivElement | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains the rendered history window for a session timeline.
|
||||
*
|
||||
* It keeps initial paint bounded to recent turns, reveals cached turns in
|
||||
* small batches while scrolling upward, and prefetches older history near top.
|
||||
*/
|
||||
function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
const turnInit = 10
|
||||
const turnBatch = 8
|
||||
const turnScrollThreshold = 200
|
||||
const turnPrefetchBuffer = 16
|
||||
const prefetchCooldownMs = 400
|
||||
const prefetchNoGrowthLimit = 2
|
||||
|
||||
const [state, setState] = createStore({
|
||||
turnID: undefined as string | undefined,
|
||||
turnStart: 0,
|
||||
prefetchUntil: 0,
|
||||
prefetchNoGrowth: 0,
|
||||
})
|
||||
|
||||
const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0)
|
||||
|
||||
const turnStart = createMemo(() => {
|
||||
const id = input.sessionID()
|
||||
const len = input.visibleUserMessages().length
|
||||
if (!id || len <= 0) return 0
|
||||
if (state.turnID !== id) return initialTurnStart(len)
|
||||
if (state.turnStart <= 0) return 0
|
||||
if (state.turnStart >= len) return initialTurnStart(len)
|
||||
return state.turnStart
|
||||
})
|
||||
|
||||
const setTurnStart = (start: number) => {
|
||||
const id = input.sessionID()
|
||||
const next = start > 0 ? start : 0
|
||||
if (!id) {
|
||||
setState({ turnID: undefined, turnStart: next })
|
||||
return
|
||||
}
|
||||
setState({ turnID: id, turnStart: next })
|
||||
}
|
||||
|
||||
const renderedUserMessages = createMemo(
|
||||
() => {
|
||||
const msgs = input.visibleUserMessages()
|
||||
const start = turnStart()
|
||||
if (start <= 0) return msgs
|
||||
return msgs.slice(start)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{
|
||||
equals: same,
|
||||
},
|
||||
)
|
||||
|
||||
const preserveScroll = (fn: () => void) => {
|
||||
const el = input.scroller()
|
||||
if (!el) {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
const beforeTop = el.scrollTop
|
||||
const beforeHeight = el.scrollHeight
|
||||
fn()
|
||||
requestAnimationFrame(() => {
|
||||
const delta = el.scrollHeight - beforeHeight
|
||||
if (!delta) return
|
||||
el.scrollTop = beforeTop + delta
|
||||
})
|
||||
}
|
||||
|
||||
const backfillTurns = () => {
|
||||
const start = turnStart()
|
||||
if (start <= 0) return
|
||||
|
||||
const next = start - turnBatch
|
||||
const nextStart = next > 0 ? next : 0
|
||||
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
||||
const loadAndReveal = async () => {
|
||||
const id = input.sessionID()
|
||||
if (!id) return
|
||||
|
||||
const start = turnStart()
|
||||
const beforeVisible = input.visibleUserMessages().length
|
||||
|
||||
if (start > 0) setTurnStart(0)
|
||||
|
||||
if (!input.historyMore() || input.historyLoading()) return
|
||||
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
|
||||
const afterVisible = input.visibleUserMessages().length
|
||||
const growth = afterVisible - beforeVisible
|
||||
if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0)
|
||||
if (growth <= 0) return
|
||||
if (turnStart() !== 0) return
|
||||
|
||||
const target = Math.min(afterVisible, Math.max(beforeVisible, renderedUserMessages().length) + turnBatch)
|
||||
const nextStart = Math.max(0, afterVisible - target)
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
/** Scroll/prefetch path: fetch older history from server. */
|
||||
const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => {
|
||||
const id = input.sessionID()
|
||||
if (!id) return
|
||||
if (!input.historyMore() || input.historyLoading()) return
|
||||
|
||||
if (opts?.prefetch) {
|
||||
const now = Date.now()
|
||||
if (state.prefetchUntil > now) return
|
||||
if (state.prefetchNoGrowth >= prefetchNoGrowthLimit) return
|
||||
setState("prefetchUntil", now + prefetchCooldownMs)
|
||||
}
|
||||
|
||||
const start = turnStart()
|
||||
const beforeVisible = input.visibleUserMessages().length
|
||||
const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length
|
||||
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
|
||||
const afterVisible = input.visibleUserMessages().length
|
||||
const growth = afterVisible - beforeVisible
|
||||
|
||||
if (opts?.prefetch) {
|
||||
setState("prefetchNoGrowth", growth > 0 ? 0 : state.prefetchNoGrowth + 1)
|
||||
} else if (growth > 0 && state.prefetchNoGrowth) {
|
||||
setState("prefetchNoGrowth", 0)
|
||||
}
|
||||
|
||||
if (growth <= 0) return
|
||||
if (turnStart() !== start) return
|
||||
|
||||
const reveal = !opts?.prefetch
|
||||
const currentRendered = renderedUserMessages().length
|
||||
const base = Math.max(beforeRendered, currentRendered)
|
||||
const target = reveal ? Math.min(afterVisible, base + turnBatch) : base
|
||||
const nextStart = Math.max(0, afterVisible - target)
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
const onScrollerScroll = () => {
|
||||
if (!input.userScrolled()) return
|
||||
const el = input.scroller()
|
||||
if (!el) return
|
||||
if (el.scrollTop >= turnScrollThreshold) return
|
||||
|
||||
const start = turnStart()
|
||||
if (start > 0) {
|
||||
if (start <= turnPrefetchBuffer) {
|
||||
void fetchOlderMessages({ prefetch: true })
|
||||
}
|
||||
backfillTurns()
|
||||
return
|
||||
}
|
||||
|
||||
void fetchOlderMessages()
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
input.sessionID,
|
||||
() => {
|
||||
setState({ prefetchUntil: 0, prefetchNoGrowth: 0 })
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [input.sessionID(), input.messagesReady()] as const,
|
||||
([id, ready]) => {
|
||||
if (!id || !ready) return
|
||||
setTurnStart(initialTurnStart(input.visibleUserMessages().length))
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
turnStart,
|
||||
setTurnStart,
|
||||
renderedUserMessages,
|
||||
loadAndReveal,
|
||||
onScrollerScroll,
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
@@ -347,6 +138,24 @@ export default function Page() {
|
||||
if (path) file.load(path)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const current = tabs().all()
|
||||
if (current.length === 0) return
|
||||
|
||||
const next = normalizeTabs(current)
|
||||
if (same(current, next)) return
|
||||
|
||||
tabs().setAll(next)
|
||||
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
if (!active.startsWith("file://")) return
|
||||
|
||||
const normalized = normalizeTab(active)
|
||||
if (active === normalized) return
|
||||
tabs().setActive(normalized)
|
||||
})
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||
@@ -369,6 +178,7 @@ export default function Page() {
|
||||
return sync.session.history.loading(id)
|
||||
})
|
||||
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
const userMessages = createMemo(
|
||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||
emptyUserMessages,
|
||||
@@ -401,6 +211,7 @@ export default function Page() {
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
turnStart: 0,
|
||||
mobileTab: "session" as "session" | "changes",
|
||||
changes: "session" as "session" | "turn",
|
||||
newSessionWorktree: "main",
|
||||
@@ -409,6 +220,20 @@ export default function Page() {
|
||||
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
|
||||
const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
|
||||
|
||||
const renderedUserMessages = createMemo(
|
||||
() => {
|
||||
const msgs = visibleUserMessages()
|
||||
const start = store.turnStart
|
||||
if (start <= 0) return msgs
|
||||
if (start >= msgs.length) return emptyUserMessages
|
||||
return msgs.slice(start)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{
|
||||
equals: same,
|
||||
},
|
||||
)
|
||||
|
||||
const newSessionWorktree = createMemo(() => {
|
||||
if (store.newSessionWorktree === "create") return "create"
|
||||
const project = sync.project
|
||||
@@ -477,15 +302,13 @@ export default function Page() {
|
||||
|
||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||
|
||||
createEffect(
|
||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
||||
if (!id) return
|
||||
untrack(() => {
|
||||
void sync.session.sync(id)
|
||||
void sync.session.todo(id)
|
||||
})
|
||||
}),
|
||||
)
|
||||
createEffect(() => {
|
||||
sdk.directory
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
void sync.session.sync(id)
|
||||
void sync.session.todo(id)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
@@ -1071,16 +894,88 @@ export default function Page() {
|
||||
},
|
||||
)
|
||||
|
||||
const historyWindow = createSessionHistoryWindow({
|
||||
sessionID: () => params.id,
|
||||
messagesReady,
|
||||
visibleUserMessages,
|
||||
historyMore,
|
||||
historyLoading,
|
||||
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
|
||||
userScrolled: autoScroll.userScrolled,
|
||||
scroller: () => scroller,
|
||||
})
|
||||
const turnInit = 20
|
||||
const turnBatch = 20
|
||||
let turnHandle: number | undefined
|
||||
let turnIdle = false
|
||||
|
||||
function cancelTurnBackfill() {
|
||||
const handle = turnHandle
|
||||
if (handle === undefined) return
|
||||
turnHandle = undefined
|
||||
|
||||
if (turnIdle && window.cancelIdleCallback) {
|
||||
window.cancelIdleCallback(handle)
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(handle)
|
||||
}
|
||||
|
||||
function scheduleTurnBackfill() {
|
||||
if (turnHandle !== undefined) return
|
||||
if (store.turnStart <= 0) return
|
||||
|
||||
if (window.requestIdleCallback) {
|
||||
turnIdle = true
|
||||
turnHandle = window.requestIdleCallback(() => {
|
||||
turnHandle = undefined
|
||||
backfillTurns()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
turnIdle = false
|
||||
turnHandle = window.setTimeout(() => {
|
||||
turnHandle = undefined
|
||||
backfillTurns()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function backfillTurns() {
|
||||
const start = store.turnStart
|
||||
if (start <= 0) return
|
||||
|
||||
const next = start - turnBatch
|
||||
const nextStart = next > 0 ? next : 0
|
||||
|
||||
const el = scroller
|
||||
if (!el) {
|
||||
setStore("turnStart", nextStart)
|
||||
scheduleTurnBackfill()
|
||||
return
|
||||
}
|
||||
|
||||
const beforeTop = el.scrollTop
|
||||
const beforeHeight = el.scrollHeight
|
||||
|
||||
setStore("turnStart", nextStart)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const delta = el.scrollHeight - beforeHeight
|
||||
if (!delta) return
|
||||
el.scrollTop = beforeTop + delta
|
||||
})
|
||||
|
||||
scheduleTurnBackfill()
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [params.id, messagesReady()] as const,
|
||||
([id, ready]) => {
|
||||
cancelTurnBackfill()
|
||||
setStore("turnStart", 0)
|
||||
if (!id || !ready) return
|
||||
|
||||
const len = visibleUserMessages().length
|
||||
const start = len > turnInit ? len - turnInit : 0
|
||||
setStore("turnStart", start)
|
||||
scheduleTurnBackfill()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createResizeObserver(
|
||||
() => promptDock,
|
||||
@@ -1107,12 +1002,13 @@ export default function Page() {
|
||||
sessionID: () => params.id,
|
||||
messagesReady,
|
||||
visibleUserMessages,
|
||||
turnStart: historyWindow.turnStart,
|
||||
turnStart: () => store.turnStart,
|
||||
currentMessageId: () => store.messageId,
|
||||
pendingMessage: () => ui.pendingMessage,
|
||||
setPendingMessage: (value) => setUi("pendingMessage", value),
|
||||
setActiveMessage,
|
||||
setTurnStart: historyWindow.setTurnStart,
|
||||
setTurnStart: (value) => setStore("turnStart", value),
|
||||
scheduleTurnBackfill,
|
||||
autoScroll,
|
||||
scroller: () => scroller,
|
||||
anchor,
|
||||
@@ -1125,6 +1021,7 @@ export default function Page() {
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
cancelTurnBackfill()
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
scrollSpy.destroy()
|
||||
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
||||
@@ -1179,7 +1076,6 @@ export default function Page() {
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
isDesktop={isDesktop()}
|
||||
onScrollSpyScroll={scrollSpy.onScroll}
|
||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
centered={centered()}
|
||||
setContentRef={(el) => {
|
||||
@@ -1189,16 +1085,21 @@ export default function Page() {
|
||||
const root = scroller
|
||||
if (root) scheduleScrollState(root)
|
||||
}}
|
||||
turnStart={historyWindow.turnStart()}
|
||||
turnStart={store.turnStart}
|
||||
onRenderEarlier={() => setStore("turnStart", 0)}
|
||||
historyMore={historyMore()}
|
||||
historyLoading={historyLoading()}
|
||||
onLoadEarlier={() => {
|
||||
void historyWindow.loadAndReveal()
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
setStore("turnStart", 0)
|
||||
sync.session.history.loadMore(id)
|
||||
}}
|
||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
||||
renderedUserMessages={renderedUserMessages()}
|
||||
anchor={anchor}
|
||||
onRegisterMessage={scrollSpy.register}
|
||||
onUnregisterMessage={scrollSpy.unregister}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -22,6 +23,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
customOn: [] as boolean[],
|
||||
editing: false,
|
||||
sending: false,
|
||||
collapsed: false,
|
||||
})
|
||||
|
||||
let root: HTMLDivElement | undefined
|
||||
@@ -31,6 +33,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
const input = createMemo(() => store.custom[store.tab] ?? "")
|
||||
const on = createMemo(() => store.customOn[store.tab] === true)
|
||||
const multi = createMemo(() => question()?.multiple === true)
|
||||
const picked = createMemo(() => store.answers[store.tab]?.length ?? 0)
|
||||
|
||||
const summary = createMemo(() => {
|
||||
const n = Math.min(store.tab + 1, total())
|
||||
@@ -39,6 +42,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
|
||||
const last = createMemo(() => store.tab >= total() - 1)
|
||||
|
||||
const fold = () => setStore("collapsed", (value) => !value)
|
||||
|
||||
const customUpdate = (value: string, selected: boolean = on()) => {
|
||||
const prev = input().trim()
|
||||
const next = value.trim()
|
||||
@@ -228,38 +233,62 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
const jump = (tab: number) => {
|
||||
const click = (target: EventTarget | null) => {
|
||||
if (store.sending) return
|
||||
setStore("tab", tab)
|
||||
setStore("editing", false)
|
||||
if (!(target instanceof Node)) {
|
||||
fold()
|
||||
return
|
||||
}
|
||||
|
||||
const list = root?.querySelector('[data-slot="question-options"]')
|
||||
if (list instanceof HTMLElement && list.contains(target)) return
|
||||
fold()
|
||||
}
|
||||
|
||||
return (
|
||||
<DockPrompt
|
||||
kind="question"
|
||||
ref={(el) => (root = el)}
|
||||
bodyProps={{
|
||||
onClick: (event) => click(event.target),
|
||||
}}
|
||||
header={
|
||||
<>
|
||||
<div
|
||||
data-action="session-question-toggle"
|
||||
class="flex flex-1 min-w-0 items-center gap-2 cursor-default select-none"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{ margin: "0 -10px", padding: "0 0 0 10px" }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
fold()
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
fold()
|
||||
}}
|
||||
>
|
||||
<div data-slot="question-header-title">{summary()}</div>
|
||||
<div data-slot="question-progress">
|
||||
<For each={questions()}>
|
||||
{(_, i) => (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="question-progress-segment"
|
||||
data-active={i() === store.tab}
|
||||
data-answered={
|
||||
(store.answers[i()]?.length ?? 0) > 0 ||
|
||||
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
||||
}
|
||||
disabled={store.sending}
|
||||
onClick={() => jump(i())}
|
||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<div class="ml-auto">
|
||||
<IconButton
|
||||
data-action="session-question-toggle-button"
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
classList={{ "rotate-180": store.collapsed }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
fold()
|
||||
}}
|
||||
aria-label={store.collapsed ? language.t("session.todo.expand") : language.t("session.todo.collapse")}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
@@ -279,56 +308,122 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div data-slot="question-text">{question()?.question}</div>
|
||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
<div
|
||||
data-slot="question-text"
|
||||
class="cursor-default"
|
||||
classList={{
|
||||
"mb-6": store.collapsed && picked() === 0,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
fold()
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
fold()
|
||||
}}
|
||||
>
|
||||
{question()?.question}
|
||||
</div>
|
||||
<Show when={store.collapsed && picked() > 0}>
|
||||
<div data-slot="question-hint" class="cursor-default mb-6">
|
||||
{picked()} answer{picked() === 1 ? "" : "s"} selected
|
||||
</div>
|
||||
</Show>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<div data-slot="question-answers" hidden={store.collapsed} aria-hidden={store.collapsed}>
|
||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
</Show>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={picked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={picked()}
|
||||
>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={picked()}
|
||||
aria-checked={on()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
onClick={customOpen}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={picked()}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
customToggle()
|
||||
}}
|
||||
>
|
||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={
|
||||
<button
|
||||
}
|
||||
>
|
||||
<form
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={on()}
|
||||
disabled={store.sending}
|
||||
onClick={customOpen}
|
||||
onMouseDown={(e) => {
|
||||
if (store.sending) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (e.target instanceof HTMLTextAreaElement) return
|
||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
@@ -347,80 +442,39 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<form
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={on()}
|
||||
onMouseDown={(e) => {
|
||||
if (store.sending) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (e.target instanceof HTMLTextAreaElement) return
|
||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
customToggle()
|
||||
}}
|
||||
>
|
||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<textarea
|
||||
ref={(el) =>
|
||||
setTimeout(() => {
|
||||
el.focus()
|
||||
el.style.height = "0px"
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}, 0)
|
||||
}
|
||||
data-slot="question-custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
rows={1}
|
||||
disabled={store.sending}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setStore("editing", false)
|
||||
return
|
||||
<textarea
|
||||
ref={(el) =>
|
||||
setTimeout(() => {
|
||||
el.focus()
|
||||
el.style.height = "0px"
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}, 0)
|
||||
}
|
||||
if (e.key !== "Enter" || e.shiftKey) return
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
onInput={(e) => {
|
||||
customUpdate(e.currentTarget.value)
|
||||
e.currentTarget.style.height = "0px"
|
||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
</Show>
|
||||
data-slot="question-custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
rows={1}
|
||||
disabled={store.sending}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
if (e.key !== "Enter" || e.shiftKey) return
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
onInput={(e) => {
|
||||
customUpdate(e.currentTarget.value)
|
||||
e.currentTarget.style.height = "0px"
|
||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</DockPrompt>
|
||||
)
|
||||
|
||||
@@ -67,7 +67,6 @@ export function FileTabContent(props: { tab: string }) {
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let scrollFrame: number | undefined
|
||||
let restoreFrame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
let codeScroll: HTMLElement[] = []
|
||||
let find: FileSearchHandle | null = null
|
||||
@@ -350,15 +349,6 @@ export function FileTabContent(props: { tab: string }) {
|
||||
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
||||
}
|
||||
|
||||
const queueRestore = () => {
|
||||
if (restoreFrame !== undefined) return
|
||||
|
||||
restoreFrame = requestAnimationFrame(() => {
|
||||
restoreFrame = undefined
|
||||
restoreScroll()
|
||||
})
|
||||
}
|
||||
|
||||
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
|
||||
if (codeScroll.length === 0) syncCodeScroll()
|
||||
|
||||
@@ -374,29 +364,46 @@ export function FileTabContent(props: { tab: string }) {
|
||||
setNote("commenting", null)
|
||||
}
|
||||
|
||||
let prev = {
|
||||
loaded: false,
|
||||
ready: false,
|
||||
active: false,
|
||||
}
|
||||
createEffect(
|
||||
on(
|
||||
() => state()?.loaded,
|
||||
(loaded) => {
|
||||
if (!loaded) return
|
||||
requestAnimationFrame(restoreScroll)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const loaded = !!state()?.loaded
|
||||
const ready = file.ready()
|
||||
const active = tabs().active() === props.tab
|
||||
const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
|
||||
prev = { loaded, ready, active }
|
||||
if (!restore) return
|
||||
queueRestore()
|
||||
})
|
||||
createEffect(
|
||||
on(
|
||||
() => file.ready(),
|
||||
(ready) => {
|
||||
if (!ready) return
|
||||
requestAnimationFrame(restoreScroll)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => tabs().active() === props.tab,
|
||||
(active) => {
|
||||
if (!active) return
|
||||
if (!state()?.loaded) return
|
||||
requestAnimationFrame(restoreScroll)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
for (const item of codeScroll) {
|
||||
item.removeEventListener("scroll", handleCodeScroll)
|
||||
}
|
||||
|
||||
if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
|
||||
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
|
||||
if (scrollFrame === undefined) return
|
||||
cancelAnimationFrame(scrollFrame)
|
||||
})
|
||||
|
||||
const renderFile = (source: string) => (
|
||||
@@ -414,7 +421,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
selectedLines={activeSelection()}
|
||||
commentedLines={commentedLines()}
|
||||
onRendered={() => {
|
||||
queueRestore()
|
||||
requestAnimationFrame(restoreScroll)
|
||||
}}
|
||||
annotations={commentsUi.annotations()}
|
||||
renderAnnotation={commentsUi.renderAnnotation}
|
||||
@@ -433,7 +440,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
mode: "auto",
|
||||
path: path(),
|
||||
current: state()?.content,
|
||||
onLoad: queueRestore,
|
||||
onLoad: () => requestAnimationFrame(restoreScroll),
|
||||
onError: (args: { kind: "image" | "audio" | "svg" }) => {
|
||||
if (args.kind !== "svg") return
|
||||
showToast({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, createEffect, createMemo, on, onCleanup, Show, startTransition, type JSX } from "solid-js"
|
||||
import { For, createEffect, createMemo, on, onCleanup, Show, type JSX } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -81,103 +81,6 @@ const markBoundaryGesture = (input: {
|
||||
}
|
||||
}
|
||||
|
||||
type StageConfig = {
|
||||
init: number
|
||||
batch: number
|
||||
}
|
||||
|
||||
type TimelineStageInput = {
|
||||
sessionKey: () => string
|
||||
turnStart: () => number
|
||||
messages: () => UserMessage[]
|
||||
config: StageConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer-mounts small timeline windows so revealing older turns does not
|
||||
* block first paint with a large DOM mount.
|
||||
*
|
||||
* Once staging completes for a session it never re-stages — backfill and
|
||||
* new messages render immediately.
|
||||
*/
|
||||
function createTimelineStaging(input: TimelineStageInput) {
|
||||
const [state, setState] = createStore({
|
||||
activeSession: "",
|
||||
completedSession: "",
|
||||
count: 0,
|
||||
})
|
||||
|
||||
const stagedCount = createMemo(() => {
|
||||
const total = input.messages().length
|
||||
if (input.turnStart() <= 0) return total
|
||||
if (state.completedSession === input.sessionKey()) return total
|
||||
const init = Math.min(total, input.config.init)
|
||||
if (state.count <= init) return init
|
||||
if (state.count >= total) return total
|
||||
return state.count
|
||||
})
|
||||
|
||||
const stagedUserMessages = createMemo(() => {
|
||||
const list = input.messages()
|
||||
const count = stagedCount()
|
||||
if (count >= list.length) return list
|
||||
return list.slice(Math.max(0, list.length - count))
|
||||
})
|
||||
|
||||
let frame: number | undefined
|
||||
const cancel = () => {
|
||||
if (frame === undefined) return
|
||||
cancelAnimationFrame(frame)
|
||||
frame = undefined
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [input.sessionKey(), input.turnStart() > 0, input.messages().length] as const,
|
||||
([sessionKey, isWindowed, total]) => {
|
||||
cancel()
|
||||
const shouldStage =
|
||||
isWindowed &&
|
||||
total > input.config.init &&
|
||||
state.completedSession !== sessionKey &&
|
||||
state.activeSession !== sessionKey
|
||||
if (!shouldStage) {
|
||||
setState({ activeSession: "", count: total })
|
||||
return
|
||||
}
|
||||
|
||||
let count = Math.min(total, input.config.init)
|
||||
setState({ activeSession: sessionKey, count })
|
||||
|
||||
const step = () => {
|
||||
if (input.sessionKey() !== sessionKey) {
|
||||
frame = undefined
|
||||
return
|
||||
}
|
||||
const currentTotal = input.messages().length
|
||||
count = Math.min(currentTotal, count + input.config.batch)
|
||||
startTransition(() => setState("count", count))
|
||||
if (count >= currentTotal) {
|
||||
setState({ completedSession: sessionKey, activeSession: "" })
|
||||
frame = undefined
|
||||
return
|
||||
}
|
||||
frame = requestAnimationFrame(step)
|
||||
}
|
||||
frame = requestAnimationFrame(step)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const isStaging = createMemo(() => {
|
||||
const key = input.sessionKey()
|
||||
return state.activeSession === key && state.completedSession !== key
|
||||
})
|
||||
|
||||
onCleanup(cancel)
|
||||
return { messages: stagedUserMessages, isStaging }
|
||||
}
|
||||
|
||||
export function MessageTimeline(props: {
|
||||
mobileChanges: boolean
|
||||
mobileFallback: JSX.Element
|
||||
@@ -190,11 +93,11 @@ export function MessageTimeline(props: {
|
||||
hasScrollGesture: () => boolean
|
||||
isDesktop: boolean
|
||||
onScrollSpyScroll: () => void
|
||||
onTurnBackfillScroll: () => void
|
||||
onAutoScrollInteraction: (event: MouseEvent) => void
|
||||
centered: boolean
|
||||
setContentRef: (el: HTMLDivElement) => void
|
||||
turnStart: number
|
||||
onRenderEarlier: () => void
|
||||
historyMore: boolean
|
||||
historyLoading: boolean
|
||||
onLoadEarlier: () => void
|
||||
@@ -202,6 +105,7 @@ export function MessageTimeline(props: {
|
||||
anchor: (id: string) => string
|
||||
onRegisterMessage: (el: HTMLDivElement, id: string) => void
|
||||
onUnregisterMessage: (id: string) => void
|
||||
lastUserMessageID?: string
|
||||
}) {
|
||||
let touchGesture: number | undefined
|
||||
|
||||
@@ -223,13 +127,6 @@ export function MessageTimeline(props: {
|
||||
const titleValue = createMemo(() => info()?.title)
|
||||
const parentID = createMemo(() => info()?.parentID)
|
||||
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
||||
const stageCfg = { init: 1, batch: 3 }
|
||||
const staging = createTimelineStaging({
|
||||
sessionKey,
|
||||
turnStart: () => props.turnStart,
|
||||
messages: () => props.renderedUserMessages,
|
||||
config: stageCfg,
|
||||
})
|
||||
|
||||
const [title, setTitle] = createStore({
|
||||
draft: "",
|
||||
@@ -446,10 +343,8 @@ export function MessageTimeline(props: {
|
||||
<div
|
||||
class="absolute left-1/2 -translate-x-1/2 bottom-6 z-[60] pointer-events-none transition-all duration-200 ease-out"
|
||||
classList={{
|
||||
"opacity-100 translate-y-0 scale-100":
|
||||
props.scroll.overflow && !props.scroll.bottom && !staging.isStaging(),
|
||||
"opacity-0 translate-y-2 scale-95 pointer-events-none":
|
||||
!props.scroll.overflow || props.scroll.bottom || staging.isStaging(),
|
||||
"opacity-100 translate-y-0 scale-100": props.scroll.overflow && !props.scroll.bottom,
|
||||
"opacity-0 translate-y-2 scale-95 pointer-events-none": !props.scroll.overflow || props.scroll.bottom,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
@@ -498,7 +393,6 @@ export function MessageTimeline(props: {
|
||||
}}
|
||||
onScroll={(e) => {
|
||||
props.onScheduleScrollState(e.currentTarget)
|
||||
props.onTurnBackfillScroll()
|
||||
if (!props.hasScrollGesture()) return
|
||||
props.onAutoScrollHandleScroll()
|
||||
props.onMarkScrollGesture(e.currentTarget)
|
||||
@@ -636,7 +530,14 @@ export function MessageTimeline(props: {
|
||||
"mt-0": !props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={props.turnStart > 0 || props.historyMore}>
|
||||
<Show when={props.turnStart > 0}>
|
||||
<div class="w-full flex justify-center">
|
||||
<Button variant="ghost" size="large" class="text-12-medium opacity-50" onClick={props.onRenderEarlier}>
|
||||
{language.t("session.messages.renderEarlier")}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.historyMore}>
|
||||
<div class="w-full flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -651,10 +552,9 @@ export function MessageTimeline(props: {
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<For each={staging.messages()}>
|
||||
<For each={props.renderedUserMessages}>
|
||||
{(message) => {
|
||||
const comments = createMemo(() => messageComments(sync.data.part[message.id] ?? []))
|
||||
const commentCount = createMemo(() => comments().length)
|
||||
return (
|
||||
<div
|
||||
id={props.anchor(message.id)}
|
||||
@@ -667,9 +567,8 @@ export function MessageTimeline(props: {
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
style={{ "content-visibility": "auto", "contain-intrinsic-size": "auto 500px" }}
|
||||
>
|
||||
<Show when={commentCount() > 0}>
|
||||
<Show when={comments().length > 0}>
|
||||
<div class="w-full px-4 md:px-5 pb-2">
|
||||
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
||||
<div class="flex w-max min-w-full justify-end gap-2">
|
||||
@@ -702,6 +601,7 @@ export function MessageTimeline(props: {
|
||||
<SessionTurn
|
||||
sessionID={sessionID() ?? ""}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={props.lastUserMessageID}
|
||||
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
|
||||
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, onCleanup, type JSX } from "solid-js"
|
||||
import { createEffect, on, onCleanup, type JSX } from "solid-js"
|
||||
import type { FileDiff } from "@opencode-ai/sdk/v2"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import type {
|
||||
@@ -119,12 +119,32 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
props.diffs().length
|
||||
props.diffStyle
|
||||
if (!layout.ready()) return
|
||||
queueRestore()
|
||||
})
|
||||
createEffect(
|
||||
on(
|
||||
() => props.diffs().length,
|
||||
() => queueRestore(),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.diffStyle,
|
||||
() => queueRestore(),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => layout.ready(),
|
||||
(ready) => {
|
||||
if (!ready) return
|
||||
queueRestore()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
|
||||
|
||||
@@ -56,9 +56,9 @@ export function TerminalPanel() {
|
||||
on(
|
||||
() => terminal.all().length,
|
||||
(count, prevCount) => {
|
||||
if (prevCount === undefined || prevCount <= 0 || count !== 0) return
|
||||
if (!opened()) return
|
||||
close()
|
||||
if (prevCount !== undefined && prevCount > 0 && count === 0) {
|
||||
if (opened()) view().terminal.toggle()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export const messageIdFromHash = (hash: string) => {
|
||||
@@ -19,6 +19,7 @@ export const useSessionHashScroll = (input: {
|
||||
setPendingMessage: (value: string | undefined) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
setTurnStart: (value: number) => void
|
||||
scheduleTurnBackfill: () => void
|
||||
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
|
||||
scroller: () => HTMLDivElement | undefined
|
||||
anchor: (id: string) => string
|
||||
@@ -28,7 +29,6 @@ export const useSessionHashScroll = (input: {
|
||||
const visibleUserMessages = createMemo(() => input.visibleUserMessages())
|
||||
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
|
||||
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
|
||||
let pendingKey = ""
|
||||
|
||||
const clearMessageHash = () => {
|
||||
if (!window.location.hash) return
|
||||
@@ -58,6 +58,7 @@ export const useSessionHashScroll = (input: {
|
||||
const index = messageIndex().get(message.id) ?? -1
|
||||
if (index !== -1 && index < input.turnStart()) {
|
||||
input.setTurnStart(index)
|
||||
input.scheduleTurnBackfill()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(input.anchor(message.id))
|
||||
@@ -131,6 +132,15 @@ export const useSessionHashScroll = (input: {
|
||||
if (el) input.scheduleScrollState(el)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(input.sessionKey, (key) => {
|
||||
if (!input.sessionID()) return
|
||||
const messageID = input.consumePendingMessage(key)
|
||||
if (!messageID) return
|
||||
input.setPendingMessage(messageID)
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
requestAnimationFrame(() => applyHash("auto"))
|
||||
@@ -142,20 +152,7 @@ export const useSessionHashScroll = (input: {
|
||||
visibleUserMessages()
|
||||
input.turnStart()
|
||||
|
||||
let targetId = input.pendingMessage()
|
||||
if (!targetId) {
|
||||
const key = input.sessionKey()
|
||||
if (pendingKey !== key) {
|
||||
pendingKey = key
|
||||
const next = input.consumePendingMessage(key)
|
||||
if (next) {
|
||||
input.setPendingMessage(next)
|
||||
targetId = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetId) targetId = messageIdFromHash(window.location.hash)
|
||||
const targetId = input.pendingMessage() ?? messageIdFromHash(window.location.hash)
|
||||
if (!targetId) return
|
||||
if (input.currentMessageId() === targetId) return
|
||||
|
||||
@@ -167,12 +164,9 @@ export const useSessionHashScroll = (input: {
|
||||
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const handler = () => {
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
requestAnimationFrame(() => applyHash("auto"))
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
const handler = () => requestAnimationFrame(() => applyHash("auto"))
|
||||
window.addEventListener("hashchange", handler)
|
||||
onCleanup(() => window.removeEventListener("hashchange", handler))
|
||||
})
|
||||
|
||||
@@ -7,21 +7,9 @@ import { Font } from "@opencode-ai/ui/font"
|
||||
import "@ibm/plex/css/ibm-plex.css"
|
||||
import "./app.css"
|
||||
import { LanguageProvider } from "~/context/language"
|
||||
import { I18nProvider, useI18n } from "~/context/i18n"
|
||||
import { I18nProvider } from "~/context/i18n"
|
||||
import { strip } from "~/lib/language"
|
||||
|
||||
function AppMeta() {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<>
|
||||
<Title>opencode</Title>
|
||||
<Meta name="description" content={i18n.t("app.meta.description")} />
|
||||
<Favicon />
|
||||
<Font />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router
|
||||
@@ -31,7 +19,10 @@ export default function App() {
|
||||
<LanguageProvider>
|
||||
<I18nProvider>
|
||||
<MetaProvider>
|
||||
<AppMeta />
|
||||
<Title>opencode</Title>
|
||||
<Meta name="description" content="OpenCode - The open source coding agent." />
|
||||
<Favicon />
|
||||
<Font />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MetaProvider>
|
||||
</I18nProvider>
|
||||
|
||||
@@ -124,8 +124,8 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
|
||||
<section data-component="top">
|
||||
<div onContextMenu={handleLogoContextMenu}>
|
||||
<A href={language.route("/")}>
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
|
||||
<img data-slot="logo light" src={logoLight} alt="OpenCode" width="189" height="34" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="OpenCode" width="189" height="34" />
|
||||
</A>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "الرئيسية",
|
||||
"nav.openMenu": "فتح القائمة",
|
||||
"nav.getStartedFree": "ابدأ مجانا",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "نسخ الشعار كـ SVG",
|
||||
"nav.context.copyWordmark": "نسخ اسم العلامة كـ SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "الوثائق",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "شعار opencode الفاتح",
|
||||
"notFound.logoDarkAlt": "شعار opencode الداكن",
|
||||
|
||||
"user.logout": "تسجيل الخروج",
|
||||
|
||||
"auth.callback.error.codeMissing": "لم يتم العثور على رمز التفويض.",
|
||||
|
||||
"workspace.select": "اختر مساحة العمل",
|
||||
"workspace.createNew": "+ إنشاء مساحة عمل جديدة",
|
||||
"workspace.modal.title": "إنشاء مساحة عمل جديدة",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "يجب أن يكون مبلغ الشحن ${{amount}} على الأقل",
|
||||
"error.reloadTriggerMin": "يجب أن يكون حد الرصيد ${{amount}} على الأقل",
|
||||
|
||||
"app.meta.description": "OpenCode - وكيل البرمجة مفتوح المصدر.",
|
||||
|
||||
"home.title": "OpenCode | وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر",
|
||||
|
||||
"temp.title": "opencode | وكيل برمجة بالذكاء الاصطناعي مبني للطرفية",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "، بما في ذلك النماذج المحلية",
|
||||
"temp.screenshot.caption": "واجهة OpenCode الطرفية مع سمة tokyonight",
|
||||
"temp.screenshot.alt": "واجهة OpenCode الطرفية بسمة tokyonight",
|
||||
"temp.logoLightAlt": "شعار opencode الفاتح",
|
||||
"temp.logoDarkAlt": "شعار opencode الداكن",
|
||||
|
||||
"home.banner.badge": "جديد",
|
||||
"home.banner.text": "تطبيق سطح المكتب متاح بنسخة تجريبية",
|
||||
@@ -247,24 +238,6 @@ export const dict = {
|
||||
"تتم استضافة جميع نماذج Zen في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
||||
"zen.api.error.modelFormatNotSupported": "النموذج {{model}} غير مدعوم للتنسيق {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "لا يوجد مزود متاح",
|
||||
"zen.api.error.providerNotSupported": "المزود {{provider}} غير مدعوم",
|
||||
"zen.api.error.missingApiKey": "مفتاح API مفقود.",
|
||||
"zen.api.error.invalidApiKey": "مفتاح API غير صالح.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"تم تجاوز حصة الاشتراك. يمكنك الاستمرار في استخدام النماذج المجانية.",
|
||||
"zen.api.error.noPaymentMethod": "لا توجد طريقة دفع. أضف طريقة دفع هنا: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "رصيد غير كاف. إدارة فواتيرك هنا: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"وصلت مساحة العمل الخاصة بك إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "النموذج معطل",
|
||||
|
||||
"black.meta.title": "OpenCode Black | الوصول إلى أفضل نماذج البرمجة في العالم",
|
||||
"black.meta.description": "احصل على وصول إلى Claude، GPT، Gemini والمزيد مع خطط اشتراك OpenCode Black.",
|
||||
"black.hero.title": "الوصول إلى أفضل نماذج البرمجة في العالم",
|
||||
@@ -473,7 +446,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع والمحاولة مرة أخرى.",
|
||||
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
|
||||
"workspace.reload.retry": "أعد المحاولة",
|
||||
"workspace.reload.error.paymentFailed": "فشلت عملية الدفع.",
|
||||
|
||||
"workspace.payments.title": "سجل المدفوعات",
|
||||
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
|
||||
@@ -591,10 +563,6 @@ export const dict = {
|
||||
"enterprise.form.send": "إرسال",
|
||||
"enterprise.form.sending": "جارٍ الإرسال...",
|
||||
"enterprise.form.success": "تم إرسال الرسالة، سنتواصل معك قريبًا.",
|
||||
"enterprise.form.success.submitted": "تم إرسال النموذج بنجاح.",
|
||||
"enterprise.form.error.allFieldsRequired": "جميع الحقول مطلوبة.",
|
||||
"enterprise.form.error.invalidEmailFormat": "تنسيق البريد الإلكتروني غير صالح.",
|
||||
"enterprise.form.error.internalServer": "خطأ داخلي في الخادم.",
|
||||
"enterprise.faq.title": "الأسئلة الشائعة",
|
||||
"enterprise.faq.q1": "ما هو OpenCode Enterprise؟",
|
||||
"enterprise.faq.a1":
|
||||
@@ -627,7 +595,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "الوكيل",
|
||||
"bench.list.table.model": "النموذج",
|
||||
"bench.list.table.score": "الدرجة",
|
||||
"bench.submission.error.allFieldsRequired": "جميع الحقول مطلوبة.",
|
||||
|
||||
"bench.detail.title": "المعيار - {{task}}",
|
||||
"bench.detail.notFound": "المهمة غير موجودة",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Início",
|
||||
"nav.openMenu": "Abrir menu",
|
||||
"nav.getStartedFree": "Começar grátis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copiar logo como SVG",
|
||||
"nav.context.copyWordmark": "Copiar marca como SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Documentação",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "logo opencode claro",
|
||||
"notFound.logoDarkAlt": "logo opencode escuro",
|
||||
|
||||
"user.logout": "Sair",
|
||||
|
||||
"auth.callback.error.codeMissing": "Nenhum código de autorização encontrado.",
|
||||
|
||||
"workspace.select": "Selecionar workspace",
|
||||
"workspace.createNew": "+ Criar novo workspace",
|
||||
"workspace.modal.title": "Criar novo workspace",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "O valor de recarga deve ser de pelo menos ${{amount}}",
|
||||
"error.reloadTriggerMin": "O gatilho de saldo deve ser de pelo menos ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - O agente de codificação de código aberto.",
|
||||
|
||||
"home.title": "OpenCode | O agente de codificação de código aberto com IA",
|
||||
|
||||
"temp.title": "opencode | Agente de codificação com IA feito para o terminal",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", incluindo modelos locais",
|
||||
"temp.screenshot.caption": "OpenCode TUI com o tema tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI com tema tokyonight",
|
||||
"temp.logoLightAlt": "logo opencode claro",
|
||||
"temp.logoDarkAlt": "logo opencode escuro",
|
||||
|
||||
"home.banner.badge": "Novo",
|
||||
"home.banner.text": "App desktop disponível em beta",
|
||||
@@ -251,24 +242,6 @@ export const dict = {
|
||||
"Todos os modelos Zen são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelo, com as",
|
||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
|
||||
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
|
||||
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} não suportado para o formato {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Nenhum provedor disponível",
|
||||
"zen.api.error.providerNotSupported": "Provedor {{provider}} não suportado",
|
||||
"zen.api.error.missingApiKey": "Chave de API ausente.",
|
||||
"zen.api.error.invalidApiKey": "Chave de API inválida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Cota de assinatura excedida. Você pode continuar usando modelos gratuitos.",
|
||||
"zen.api.error.noPaymentMethod": "Nenhuma forma de pagamento. Adicione uma forma de pagamento aqui: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gerencie seu faturamento aqui: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Seu workspace atingiu o limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "O modelo está desabilitado",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Acesse os melhores modelos de codificação do mundo",
|
||||
"black.meta.description": "Tenha acesso ao Claude, GPT, Gemini e mais com os planos de assinatura OpenCode Black.",
|
||||
"black.hero.title": "Acesse os melhores modelos de codificação do mundo",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Por favor, atualize sua forma de pagamento e tente novamente.",
|
||||
"workspace.reload.retrying": "Tentando novamente...",
|
||||
"workspace.reload.retry": "Tentar novamente",
|
||||
"workspace.reload.error.paymentFailed": "Pagamento falhou.",
|
||||
|
||||
"workspace.payments.title": "Histórico de Pagamentos",
|
||||
"workspace.payments.subtitle": "Transações de pagamento recentes.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Enviar",
|
||||
"enterprise.form.sending": "Enviando...",
|
||||
"enterprise.form.success": "Mensagem enviada, entraremos em contato em breve.",
|
||||
"enterprise.form.success.submitted": "Formulário enviado com sucesso.",
|
||||
"enterprise.form.error.allFieldsRequired": "Todos os campos são obrigatórios.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Formato de e-mail inválido.",
|
||||
"enterprise.form.error.internalServer": "Erro interno do servidor.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "O que é OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agente",
|
||||
"bench.list.table.model": "Modelo",
|
||||
"bench.list.table.score": "Pontuação",
|
||||
"bench.submission.error.allFieldsRequired": "Todos os campos são obrigatórios.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Tarefa não encontrada",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Hjem",
|
||||
"nav.openMenu": "Åbn menu",
|
||||
"nav.getStartedFree": "Kom i gang gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Kopier logo som SVG",
|
||||
"nav.context.copyWordmark": "Kopier wordmark som SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentation",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo light",
|
||||
"notFound.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"user.logout": "Log ud",
|
||||
|
||||
"auth.callback.error.codeMissing": "Ingen autorisationskode fundet.",
|
||||
|
||||
"workspace.select": "Vælg workspace",
|
||||
"workspace.createNew": "+ Opret nyt workspace",
|
||||
"workspace.modal.title": "Opret nyt workspace",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Genopfyldningsbeløb skal være mindst ${{amount}}",
|
||||
"error.reloadTriggerMin": "Saldogrænse skal være mindst ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - Den open source kodningsagent.",
|
||||
|
||||
"home.title": "OpenCode | Den open source AI-kodningsagent",
|
||||
|
||||
"temp.title": "opencode | AI-kodningsagent bygget til terminalen",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", inklusive lokale modeller",
|
||||
"temp.screenshot.caption": "opencode TUI med tokyonight-temaet",
|
||||
"temp.screenshot.alt": "opencode TUI med tokyonight-temaet",
|
||||
"temp.logoLightAlt": "opencode logo light",
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "Ny",
|
||||
"home.banner.text": "Desktop-app tilgængelig i beta",
|
||||
@@ -249,24 +240,6 @@ export const dict = {
|
||||
"Alle Zen-modeller er hostet i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning med",
|
||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} understøttes ikke for format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Ingen udbyder tilgængelig",
|
||||
"zen.api.error.providerNotSupported": "Udbyder {{provider}} understøttes ikke",
|
||||
"zen.api.error.missingApiKey": "Manglende API-nøgle.",
|
||||
"zen.api.error.invalidApiKey": "Ugyldig API-nøgle.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnementskvote overskredet. Du kan fortsætte med at bruge gratis modeller.",
|
||||
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Tilføj en betalingsmetode her: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Utilstrækkelig saldo. Administrer din fakturering her: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Dit workspace har nået sin månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Modellen er deaktiveret",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Få adgang til verdens bedste kodningsmodeller",
|
||||
"black.meta.description": "Få adgang til Claude, GPT, Gemini og mere med OpenCode Black-abonnementer.",
|
||||
"black.hero.title": "Få adgang til verdens bedste kodningsmodeller",
|
||||
@@ -476,7 +449,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Opdater din betalingsmetode, og prøv igen.",
|
||||
"workspace.reload.retrying": "Prøver igen...",
|
||||
"workspace.reload.retry": "Prøv igen",
|
||||
"workspace.reload.error.paymentFailed": "Betaling mislykkedes.",
|
||||
|
||||
"workspace.payments.title": "Betalingshistorik",
|
||||
"workspace.payments.subtitle": "Seneste betalingstransaktioner.",
|
||||
@@ -595,10 +567,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Send",
|
||||
"enterprise.form.sending": "Sender...",
|
||||
"enterprise.form.success": "Besked sendt, vi vender tilbage snart.",
|
||||
"enterprise.form.success.submitted": "Formular indsendt med succes.",
|
||||
"enterprise.form.error.allFieldsRequired": "Alle felter er påkrævet.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Ugyldigt e-mailformat.",
|
||||
"enterprise.form.error.internalServer": "Intern serverfejl.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Hvad er OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -631,7 +599,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Score",
|
||||
"bench.submission.error.allFieldsRequired": "Alle felter er påkrævet.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Opgave ikke fundet",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Startseite",
|
||||
"nav.openMenu": "Menü öffnen",
|
||||
"nav.getStartedFree": "Kostenlos starten",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Logo als SVG kopieren",
|
||||
"nav.context.copyWordmark": "Wortmarke als SVG kopieren",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentation",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "OpenCode Logo hell",
|
||||
"notFound.logoDarkAlt": "OpenCode Logo dunkel",
|
||||
|
||||
"user.logout": "Abmelden",
|
||||
|
||||
"auth.callback.error.codeMissing": "Kein Autorisierungscode gefunden.",
|
||||
|
||||
"workspace.select": "Workspace auswählen",
|
||||
"workspace.createNew": "+ Neuen Workspace erstellen",
|
||||
"workspace.modal.title": "Neuen Workspace erstellen",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Aufladebetrag muss mindestens ${{amount}} betragen",
|
||||
"error.reloadTriggerMin": "Guthaben-Auslöser muss mindestens ${{amount}} betragen",
|
||||
|
||||
"app.meta.description": "OpenCode - Der Open-Source Coding-Agent.",
|
||||
|
||||
"home.title": "OpenCode | Der Open-Source AI-Coding-Agent",
|
||||
|
||||
"temp.title": "OpenCode | Für das Terminal gebauter AI-Coding-Agent",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", einschließlich lokaler Modelle",
|
||||
"temp.screenshot.caption": "OpenCode TUI mit dem Tokyonight-Theme",
|
||||
"temp.screenshot.alt": "OpenCode TUI mit Tokyonight-Theme",
|
||||
"temp.logoLightAlt": "OpenCode Logo hell",
|
||||
"temp.logoDarkAlt": "OpenCode Logo dunkel",
|
||||
|
||||
"home.banner.badge": "Neu",
|
||||
"home.banner.text": "Desktop-App in der Beta verfügbar",
|
||||
@@ -251,24 +242,6 @@ export const dict = {
|
||||
"Alle Zen-Modelle werden in den USA gehostet. Anbieter folgen einer Zero-Retention-Policy und nutzen deine Daten nicht für Modelltraining, mit den",
|
||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
|
||||
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
||||
"zen.api.error.modelFormatNotSupported": "Modell {{model}} wird für das Format {{format}} nicht unterstützt",
|
||||
"zen.api.error.noProviderAvailable": "Kein Anbieter verfügbar",
|
||||
"zen.api.error.providerNotSupported": "Anbieter {{provider}} wird nicht unterstützt",
|
||||
"zen.api.error.missingApiKey": "Fehlender API-Key.",
|
||||
"zen.api.error.invalidApiKey": "Ungültiger API-Key.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnement-Quote überschritten. Du kannst weiterhin kostenlose Modelle nutzen.",
|
||||
"zen.api.error.noPaymentMethod": "Keine Zahlungsmethode. Füge hier eine Zahlungsmethode hinzu: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Unzureichendes Guthaben. Verwalte deine Abrechnung hier: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Dein Workspace hat sein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Modell ist deaktiviert",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Zugriff auf die weltweit besten Coding-Modelle",
|
||||
"black.meta.description": "Erhalte Zugriff auf Claude, GPT, Gemini und mehr mit OpenCode Black Abos.",
|
||||
"black.hero.title": "Zugriff auf die weltweit besten Coding-Modelle",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Bitte aktualisiere deine Zahlungsmethode und versuche es erneut.",
|
||||
"workspace.reload.retrying": "Versuche erneut...",
|
||||
"workspace.reload.retry": "Erneut versuchen",
|
||||
"workspace.reload.error.paymentFailed": "Zahlung fehlgeschlagen.",
|
||||
|
||||
"workspace.payments.title": "Zahlungshistorie",
|
||||
"workspace.payments.subtitle": "Kürzliche Zahlungstransaktionen.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Senden",
|
||||
"enterprise.form.sending": "Sende...",
|
||||
"enterprise.form.success": "Nachricht gesendet, wir melden uns bald.",
|
||||
"enterprise.form.success.submitted": "Formular erfolgreich gesendet.",
|
||||
"enterprise.form.error.allFieldsRequired": "Alle Felder sind erforderlich.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Ungültiges E-Mail-Format.",
|
||||
"enterprise.form.error.internalServer": "Interner Serverfehler.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Was ist OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Modell",
|
||||
"bench.list.table.score": "Score",
|
||||
"bench.submission.error.allFieldsRequired": "Alle Felder sind erforderlich.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Task nicht gefunden",
|
||||
|
||||
@@ -11,7 +11,6 @@ export const dict = {
|
||||
"nav.home": "Home",
|
||||
"nav.openMenu": "Open menu",
|
||||
"nav.getStartedFree": "Get started for free",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copy logo as SVG",
|
||||
"nav.context.copyWordmark": "Copy wordmark as SVG",
|
||||
@@ -39,13 +38,9 @@ export const dict = {
|
||||
"notFound.docs": "Docs",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo light",
|
||||
"notFound.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"user.logout": "Logout",
|
||||
|
||||
"auth.callback.error.codeMissing": "No authorization code found.",
|
||||
|
||||
"workspace.select": "Select workspace",
|
||||
"workspace.createNew": "+ Create New Workspace",
|
||||
"workspace.modal.title": "Create New Workspace",
|
||||
@@ -77,8 +72,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Reload amount must be at least ${{amount}}",
|
||||
"error.reloadTriggerMin": "Balance trigger must be at least ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - The open source coding agent.",
|
||||
|
||||
"home.title": "OpenCode | The open source AI coding agent",
|
||||
|
||||
"temp.title": "opencode | AI coding agent built for the terminal",
|
||||
@@ -94,8 +87,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", including local models",
|
||||
"temp.screenshot.caption": "opencode TUI with the tokyonight theme",
|
||||
"temp.screenshot.alt": "opencode TUI with tokyonight theme",
|
||||
"temp.logoLightAlt": "opencode logo light",
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "New",
|
||||
"home.banner.text": "Desktop app available in beta",
|
||||
@@ -243,24 +234,6 @@ export const dict = {
|
||||
"All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
||||
"zen.privacy.exceptionsLink": "following exceptions",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} not supported",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} not supported for format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "No provider available",
|
||||
"zen.api.error.providerNotSupported": "Provider {{provider}} not supported",
|
||||
"zen.api.error.missingApiKey": "Missing API key.",
|
||||
"zen.api.error.invalidApiKey": "Invalid API key.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Subscription quota exceeded. You can continue using free models.",
|
||||
"zen.api.error.noPaymentMethod": "No payment method. Add a payment method here: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Insufficient balance. Manage your billing here: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Your workspace has reached its monthly spending limit of ${{amount}}. Manage your limits here: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Model is disabled",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Access all the world's best coding models",
|
||||
"black.meta.description": "Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans.",
|
||||
"black.hero.title": "Access all the world's best coding models",
|
||||
@@ -470,7 +443,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Please update your payment method and try again.",
|
||||
"workspace.reload.retrying": "Retrying...",
|
||||
"workspace.reload.retry": "Retry",
|
||||
"workspace.reload.error.paymentFailed": "Payment failed.",
|
||||
|
||||
"workspace.payments.title": "Payments History",
|
||||
"workspace.payments.subtitle": "Recent payment transactions.",
|
||||
@@ -589,10 +561,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Send",
|
||||
"enterprise.form.sending": "Sending...",
|
||||
"enterprise.form.success": "Message sent, we'll be in touch soon.",
|
||||
"enterprise.form.success.submitted": "Form submitted successfully.",
|
||||
"enterprise.form.error.allFieldsRequired": "All fields are required.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Invalid email format.",
|
||||
"enterprise.form.error.internalServer": "Internal server error.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "What is OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -625,7 +593,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Score",
|
||||
"bench.submission.error.allFieldsRequired": "All fields are required.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Task not found",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Inicio",
|
||||
"nav.openMenu": "Abrir menú",
|
||||
"nav.getStartedFree": "Empezar gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copiar logo como SVG",
|
||||
"nav.context.copyWordmark": "Copiar marca como SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Documentación",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo claro",
|
||||
"notFound.logoDarkAlt": "opencode logo oscuro",
|
||||
|
||||
"user.logout": "Cerrar sesión",
|
||||
|
||||
"auth.callback.error.codeMissing": "No se encontró código de autorización.",
|
||||
|
||||
"workspace.select": "Seleccionar espacio de trabajo",
|
||||
"workspace.createNew": "+ Crear nuevo espacio de trabajo",
|
||||
"workspace.modal.title": "Crear nuevo espacio de trabajo",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "La cantidad de recarga debe ser al menos ${{amount}}",
|
||||
"error.reloadTriggerMin": "El disparador de saldo debe ser al menos ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - El agente de codificación de código abierto.",
|
||||
|
||||
"home.title": "OpenCode | El agente de codificación IA de código abierto",
|
||||
|
||||
"temp.title": "opencode | Agente de codificación IA creado para la terminal",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", incluyendo modelos locales",
|
||||
"temp.screenshot.caption": "opencode TUI con el tema tokyonight",
|
||||
"temp.screenshot.alt": "opencode TUI con tema tokyonight",
|
||||
"temp.logoLightAlt": "logo de opencode claro",
|
||||
"temp.logoDarkAlt": "logo de opencode oscuro",
|
||||
|
||||
"home.banner.badge": "Nuevo",
|
||||
"home.banner.text": "Aplicación de escritorio disponible en beta",
|
||||
@@ -252,24 +243,6 @@ export const dict = {
|
||||
"Todos los modelos Zen están alojados en EE. UU. Los proveedores siguen una política de cero retención y no usan tus datos para entrenamiento de modelos, con las",
|
||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
|
||||
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",
|
||||
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} no soportado para el formato {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Ningún proveedor disponible",
|
||||
"zen.api.error.providerNotSupported": "Proveedor {{provider}} no soportado",
|
||||
"zen.api.error.missingApiKey": "Falta la clave API.",
|
||||
"zen.api.error.invalidApiKey": "Clave API inválida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Cuota de suscripción excedida. Puedes continuar usando modelos gratuitos.",
|
||||
"zen.api.error.noPaymentMethod": "Sin método de pago. Añade un método de pago aquí: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gestiona tu facturación aquí: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Tu espacio de trabajo ha alcanzado su límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "El modelo está deshabilitado",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Accede a los mejores modelos de codificación del mundo",
|
||||
"black.meta.description": "Obtén acceso a Claude, GPT, Gemini y más con los planes de suscripción de OpenCode Black.",
|
||||
"black.hero.title": "Accede a los mejores modelos de codificación del mundo",
|
||||
@@ -479,7 +452,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Por favor actualiza tu método de pago e intenta de nuevo.",
|
||||
"workspace.reload.retrying": "Reintentando...",
|
||||
"workspace.reload.retry": "Reintentar",
|
||||
"workspace.reload.error.paymentFailed": "El pago falló.",
|
||||
|
||||
"workspace.payments.title": "Historial de Pagos",
|
||||
"workspace.payments.subtitle": "Transacciones de pago recientes.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Enviar",
|
||||
"enterprise.form.sending": "Enviando...",
|
||||
"enterprise.form.success": "Mensaje enviado, estaremos en contacto pronto.",
|
||||
"enterprise.form.success.submitted": "Formulario enviado con éxito.",
|
||||
"enterprise.form.error.allFieldsRequired": "Todos los campos son obligatorios.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Formato de correo inválido.",
|
||||
"enterprise.form.error.internalServer": "Error interno del servidor.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "¿Qué es OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agente",
|
||||
"bench.list.table.model": "Modelo",
|
||||
"bench.list.table.score": "Puntuación",
|
||||
"bench.submission.error.allFieldsRequired": "Todos los campos son obligatorios.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Tarea no encontrada",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { dict as en } from "./en"
|
||||
|
||||
export const dict = {
|
||||
...en,
|
||||
"app.meta.description": "OpenCode - L'agent de code open source.",
|
||||
"nav.github": "GitHub",
|
||||
"nav.docs": "Documentation",
|
||||
"nav.changelog": "Changelog",
|
||||
@@ -16,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Accueil",
|
||||
"nav.openMenu": "Ouvrir le menu",
|
||||
"nav.getStartedFree": "Commencer gratuitement",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copier le logo en SVG",
|
||||
"nav.context.copyWordmark": "Copier le logotype en SVG",
|
||||
@@ -44,8 +42,6 @@ export const dict = {
|
||||
"notFound.docs": "Documentation",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo light",
|
||||
"notFound.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"user.logout": "Se déconnecter",
|
||||
|
||||
@@ -79,7 +75,6 @@ export const dict = {
|
||||
"error.modelRequired": "Le modèle est requis",
|
||||
"error.reloadAmountMin": "Le montant de recharge doit être d'au moins {{amount}} $",
|
||||
"error.reloadTriggerMin": "Le seuil de déclenchement doit être d'au moins {{amount}} $",
|
||||
"auth.callback.error.codeMissing": "Aucun code d'autorisation trouvé.",
|
||||
|
||||
"home.title": "OpenCode | L'agent de code IA open source",
|
||||
|
||||
@@ -96,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", y compris les modèles locaux",
|
||||
"temp.screenshot.caption": "OpenCode TUI avec le thème tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI avec le thème tokyonight",
|
||||
"temp.logoLightAlt": "opencode logo light",
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "Nouveau",
|
||||
"home.banner.text": "Application desktop disponible en bêta",
|
||||
@@ -253,24 +246,6 @@ export const dict = {
|
||||
"Tous les modèles Zen sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
|
||||
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",
|
||||
"zen.api.error.modelFormatNotSupported": "Modèle {{model}} non pris en charge pour le format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Aucun fournisseur disponible",
|
||||
"zen.api.error.providerNotSupported": "Fournisseur {{provider}} non pris en charge",
|
||||
"zen.api.error.missingApiKey": "Clé API manquante.",
|
||||
"zen.api.error.invalidApiKey": "Clé API invalide.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Quota d'abonnement dépassé. Vous pouvez continuer à utiliser les modèles gratuits.",
|
||||
"zen.api.error.noPaymentMethod": "Aucune méthode de paiement. Ajoutez une méthode de paiement ici : {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Solde insuffisant. Gérez votre facturation ici : {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Votre espace de travail a atteint sa limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Le modèle est désactivé",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Accédez aux meilleurs modèles de code au monde",
|
||||
"black.meta.description": "Accédez à Claude, GPT, Gemini et plus avec les forfaits d'abonnement OpenCode Black.",
|
||||
"black.hero.title": "Accédez aux meilleurs modèles de code au monde",
|
||||
@@ -482,7 +457,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Veuillez mettre à jour votre méthode de paiement et réessayer.",
|
||||
"workspace.reload.retrying": "Nouvelle tentative...",
|
||||
"workspace.reload.retry": "Réessayer",
|
||||
"workspace.reload.error.paymentFailed": "Échec du paiement.",
|
||||
|
||||
"workspace.payments.title": "Historique des paiements",
|
||||
"workspace.payments.subtitle": "Transactions de paiement récentes.",
|
||||
@@ -607,10 +581,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Envoyer",
|
||||
"enterprise.form.sending": "Envoi...",
|
||||
"enterprise.form.success": "Message envoyé, nous vous contacterons bientôt.",
|
||||
"enterprise.form.success.submitted": "Formulaire soumis avec succès.",
|
||||
"enterprise.form.error.allFieldsRequired": "Tous les champs sont requis.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Format d'e-mail invalide.",
|
||||
"enterprise.form.error.internalServer": "Erreur interne du serveur.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Qu'est-ce que OpenCode Enterprise ?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -670,5 +640,4 @@ export const dict = {
|
||||
"bench.detail.table.duration": "Durée",
|
||||
"bench.detail.run.title": "Exécution {{n}}",
|
||||
"bench.detail.rawJson": "JSON brut",
|
||||
"bench.submission.error.allFieldsRequired": "Tous les champs sont requis.",
|
||||
} satisfies Dict
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Home",
|
||||
"nav.openMenu": "Apri menu",
|
||||
"nav.getStartedFree": "Inizia gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copia il logo come SVG",
|
||||
"nav.context.copyWordmark": "Copia il wordmark come SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Documentazione",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "logo chiaro di opencode",
|
||||
"notFound.logoDarkAlt": "logo scuro di opencode",
|
||||
|
||||
"user.logout": "Esci",
|
||||
|
||||
"auth.callback.error.codeMissing": "Nessun codice di autorizzazione trovato.",
|
||||
|
||||
"workspace.select": "Seleziona workspace",
|
||||
"workspace.createNew": "+ Crea nuovo workspace",
|
||||
"workspace.modal.title": "Crea nuovo workspace",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "L'importo della ricarica deve essere almeno ${{amount}}",
|
||||
"error.reloadTriggerMin": "La soglia del saldo deve essere almeno ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - L'agente di programmazione open source.",
|
||||
|
||||
"home.title": "OpenCode | L'agente di coding IA open source",
|
||||
|
||||
"temp.title": "opencode | Agente di coding IA costruito per il terminale",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", inclusi modelli locali",
|
||||
"temp.screenshot.caption": "OpenCode TUI con il tema tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI con tema tokyonight",
|
||||
"temp.logoLightAlt": "logo chiaro di opencode",
|
||||
"temp.logoDarkAlt": "logo scuro di opencode",
|
||||
|
||||
"home.banner.badge": "Nuovo",
|
||||
"home.banner.text": "App desktop disponibile in beta",
|
||||
@@ -249,24 +240,6 @@ export const dict = {
|
||||
"Tutti i modelli Zen sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
|
||||
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato",
|
||||
"zen.api.error.modelFormatNotSupported": "Modello {{model}} non supportato per il formato {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Nessun provider disponibile",
|
||||
"zen.api.error.providerNotSupported": "Provider {{provider}} non supportato",
|
||||
"zen.api.error.missingApiKey": "Chiave API mancante.",
|
||||
"zen.api.error.invalidApiKey": "Chiave API non valida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Quota dell'abbonamento superata. Puoi continuare a utilizzare modelli gratuiti.",
|
||||
"zen.api.error.noPaymentMethod": "Nessun metodo di pagamento. Aggiungi un metodo di pagamento qui: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insufficiente. Gestisci la tua fatturazione qui: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"La tua area di lavoro ha raggiunto il limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Il modello è disabilitato",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Accedi ai migliori modelli di coding al mondo",
|
||||
"black.meta.description":
|
||||
"Ottieni l'accesso a Claude, GPT, Gemini e altri con i piani di abbonamento OpenCode Black.",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Aggiorna il tuo metodo di pagamento e riprova.",
|
||||
"workspace.reload.retrying": "Riprovo...",
|
||||
"workspace.reload.retry": "Riprova",
|
||||
"workspace.reload.error.paymentFailed": "Pagamento fallito.",
|
||||
|
||||
"workspace.payments.title": "Cronologia Pagamenti",
|
||||
"workspace.payments.subtitle": "Transazioni di pagamento recenti.",
|
||||
@@ -597,10 +569,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Invia",
|
||||
"enterprise.form.sending": "Invio...",
|
||||
"enterprise.form.success": "Messaggio inviato, ti contatteremo presto.",
|
||||
"enterprise.form.success.submitted": "Modulo inviato con successo.",
|
||||
"enterprise.form.error.allFieldsRequired": "Tutti i campi sono obbligatori.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Formato email non valido.",
|
||||
"enterprise.form.error.internalServer": "Errore interno del server.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Cos'è OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -633,7 +601,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agente",
|
||||
"bench.list.table.model": "Modello",
|
||||
"bench.list.table.score": "Punteggio",
|
||||
"bench.submission.error.allFieldsRequired": "Tutti i campi sono obbligatori.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Task non trovato",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "ホーム",
|
||||
"nav.openMenu": "メニューを開く",
|
||||
"nav.getStartedFree": "無料ではじめる",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "ロゴをSVGでコピー",
|
||||
"nav.context.copyWordmark": "ワードマークをSVGでコピー",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "ドキュメント",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencodeのロゴ(ライト)",
|
||||
"notFound.logoDarkAlt": "opencodeのロゴ(ダーク)",
|
||||
|
||||
"user.logout": "ログアウト",
|
||||
|
||||
"auth.callback.error.codeMissing": "認証コードが見つかりません。",
|
||||
|
||||
"workspace.select": "ワークスペースを選択",
|
||||
"workspace.createNew": "+ 新しいワークスペースを作成",
|
||||
"workspace.modal.title": "新しいワークスペースを作成",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "リロード額は少なくとも ${{amount}} である必要があります",
|
||||
"error.reloadTriggerMin": "残高トリガーは少なくとも ${{amount}} である必要があります",
|
||||
|
||||
"app.meta.description": "OpenCode - オープンソースのコーディングエージェント。",
|
||||
|
||||
"home.title": "OpenCode | オープンソースのAIコーディングエージェント",
|
||||
|
||||
"temp.title": "OpenCode | ターミナル向けに構築されたAIコーディングエージェント",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "を通じて75以上のLLMプロバイダーをサポート",
|
||||
"temp.screenshot.caption": "tokyonight テーマを使用した OpenCode TUI",
|
||||
"temp.screenshot.alt": "tokyonight テーマの OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencodeのロゴ(ライト)",
|
||||
"temp.logoDarkAlt": "opencodeのロゴ(ダーク)",
|
||||
|
||||
"home.banner.badge": "新着",
|
||||
"home.banner.text": "デスクトップアプリのベータ版が利用可能",
|
||||
@@ -248,25 +239,6 @@ export const dict = {
|
||||
"すべてのZenモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
||||
"zen.privacy.exceptionsLink": "以下の例外",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
|
||||
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
|
||||
"zen.api.error.modelFormatNotSupported": "フォーマット {{format}} ではモデル {{model}} はサポートされていません",
|
||||
"zen.api.error.noProviderAvailable": "利用可能なプロバイダーがありません",
|
||||
"zen.api.error.providerNotSupported": "プロバイダー {{provider}} はサポートされていません",
|
||||
"zen.api.error.missingApiKey": "APIキーがありません。",
|
||||
"zen.api.error.invalidApiKey": "無効なAPIキーです。",
|
||||
"zen.api.error.subscriptionQuotaExceeded":
|
||||
"サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"サブスクリプションの制限を超えました。無料モデルは引き続きご利用いただけます。",
|
||||
"zen.api.error.noPaymentMethod": "お支払い方法がありません。こちらからお支払い方法を追加してください: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "残高が不足しています。こちらから請求を管理してください: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"ワークスペースが月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "モデルが無効です",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 世界最高峰のコーディングモデルすべてにアクセス",
|
||||
"black.meta.description": "OpenCode Black サブスクリプションプランで、Claude、GPT、Gemini などにアクセス。",
|
||||
"black.hero.title": "世界最高峰のコーディングモデルすべてにアクセス",
|
||||
@@ -476,7 +448,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "支払い方法を更新して、もう一度お試しください。",
|
||||
"workspace.reload.retrying": "再試行中...",
|
||||
"workspace.reload.retry": "再試行",
|
||||
"workspace.reload.error.paymentFailed": "支払いに失敗しました。",
|
||||
|
||||
"workspace.payments.title": "支払い履歴",
|
||||
"workspace.payments.subtitle": "最近の支払い取引。",
|
||||
@@ -597,10 +568,6 @@ export const dict = {
|
||||
"enterprise.form.send": "送信",
|
||||
"enterprise.form.sending": "送信中...",
|
||||
"enterprise.form.success": "送信しました。まもなくご連絡いたします。",
|
||||
"enterprise.form.success.submitted": "フォームが正常に送信されました。",
|
||||
"enterprise.form.error.allFieldsRequired": "すべての項目は必須です。",
|
||||
"enterprise.form.error.invalidEmailFormat": "無効なメール形式です。",
|
||||
"enterprise.form.error.internalServer": "内部サーバーエラー。",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "OpenCode Enterpriseとは?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -633,7 +600,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "エージェント",
|
||||
"bench.list.table.model": "モデル",
|
||||
"bench.list.table.score": "スコア",
|
||||
"bench.submission.error.allFieldsRequired": "すべての項目は必須です。",
|
||||
|
||||
"bench.detail.title": "ベンチマーク - {{task}}",
|
||||
"bench.detail.notFound": "タスクが見つかりません",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "홈",
|
||||
"nav.openMenu": "메뉴 열기",
|
||||
"nav.getStartedFree": "무료로 시작하기",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "로고를 SVG로 복사",
|
||||
"nav.context.copyWordmark": "워드마크를 SVG로 복사",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "문서",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode 밝은 로고",
|
||||
"notFound.logoDarkAlt": "opencode 어두운 로고",
|
||||
|
||||
"user.logout": "로그아웃",
|
||||
|
||||
"auth.callback.error.codeMissing": "인증 코드를 찾을 수 없습니다.",
|
||||
|
||||
"workspace.select": "워크스페이스 선택",
|
||||
"workspace.createNew": "+ 새 워크스페이스 만들기",
|
||||
"workspace.modal.title": "새 워크스페이스 만들기",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "충전 금액은 최소 ${{amount}}이어야 합니다",
|
||||
"error.reloadTriggerMin": "잔액 트리거는 최소 ${{amount}}이어야 합니다",
|
||||
|
||||
"app.meta.description": "OpenCode - 오픈 소스 코딩 에이전트.",
|
||||
|
||||
"home.title": "OpenCode | 오픈 소스 AI 코딩 에이전트",
|
||||
|
||||
"temp.title": "OpenCode | 터미널을 위해 만들어진 AI 코딩 에이전트",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "를 통해 75개 이상의 LLM 제공자 지원",
|
||||
"temp.screenshot.caption": "tokyonight 테마가 적용된 OpenCode TUI",
|
||||
"temp.screenshot.alt": "tokyonight 테마가 적용된 OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencode 밝은 로고",
|
||||
"temp.logoDarkAlt": "opencode 어두운 로고",
|
||||
|
||||
"home.banner.badge": "신규",
|
||||
"home.banner.text": "데스크톱 앱 베타 버전 출시",
|
||||
@@ -245,24 +236,6 @@ export const dict = {
|
||||
"모든 Zen 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
||||
"zen.privacy.exceptionsLink": "다음 예외",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
|
||||
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
|
||||
"zen.api.error.modelFormatNotSupported": "{{model}} 모델은 {{format}} 형식에 대해 지원되지 않습니다",
|
||||
"zen.api.error.noProviderAvailable": "사용 가능한 제공자가 없습니다",
|
||||
"zen.api.error.providerNotSupported": "{{provider}} 제공자는 지원되지 않습니다",
|
||||
"zen.api.error.missingApiKey": "API 키가 누락되었습니다.",
|
||||
"zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"구독 할당량을 초과했습니다. 무료 모델은 계속 사용할 수 있습니다.",
|
||||
"zen.api.error.noPaymentMethod": "결제 수단이 없습니다. 결제 수단을 추가하세요: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "잔액이 부족합니다. 결제 관리를 여기서 하세요: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"워크스페이스의 월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "모델이 비활성화되었습니다",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 세계 최고의 코딩 모델에 액세스하세요",
|
||||
"black.meta.description": "OpenCode Black 구독 플랜으로 Claude, GPT, Gemini 등에 액세스하세요.",
|
||||
"black.hero.title": "세계 최고의 코딩 모델에 액세스하세요",
|
||||
@@ -472,7 +445,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "결제 수단을 업데이트하고 다시 시도해 주세요.",
|
||||
"workspace.reload.retrying": "재시도 중...",
|
||||
"workspace.reload.retry": "재시도",
|
||||
"workspace.reload.error.paymentFailed": "결제에 실패했습니다.",
|
||||
|
||||
"workspace.payments.title": "결제 내역",
|
||||
"workspace.payments.subtitle": "최근 결제 거래 내역입니다.",
|
||||
@@ -590,10 +562,6 @@ export const dict = {
|
||||
"enterprise.form.send": "전송",
|
||||
"enterprise.form.sending": "전송 중...",
|
||||
"enterprise.form.success": "메시지가 전송되었습니다. 곧 연락드리겠습니다.",
|
||||
"enterprise.form.success.submitted": "양식이 성공적으로 제출되었습니다.",
|
||||
"enterprise.form.error.allFieldsRequired": "모든 필드는 필수 항목입니다.",
|
||||
"enterprise.form.error.invalidEmailFormat": "유효하지 않은 이메일 형식입니다.",
|
||||
"enterprise.form.error.internalServer": "내부 서버 오류입니다.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "OpenCode 엔터프라이즈란 무엇인가요?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -626,7 +594,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "에이전트",
|
||||
"bench.list.table.model": "모델",
|
||||
"bench.list.table.score": "점수",
|
||||
"bench.submission.error.allFieldsRequired": "모든 필드는 필수 항목입니다.",
|
||||
|
||||
"bench.detail.title": "벤치마크 - {{task}}",
|
||||
"bench.detail.notFound": "태스크를 찾을 수 없음",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Hjem",
|
||||
"nav.openMenu": "Åpne meny",
|
||||
"nav.getStartedFree": "Kom i gang gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Kopier logo som SVG",
|
||||
"nav.context.copyWordmark": "Kopier wordmark som SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentasjon",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo lys",
|
||||
"notFound.logoDarkAlt": "opencode logo mørk",
|
||||
|
||||
"user.logout": "Logg ut",
|
||||
|
||||
"auth.callback.error.codeMissing": "Ingen autorisasjonskode funnet.",
|
||||
|
||||
"workspace.select": "Velg arbeidsområde",
|
||||
"workspace.createNew": "+ Opprett nytt arbeidsområde",
|
||||
"workspace.modal.title": "Opprett nytt arbeidsområde",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Påfyllingsbeløp må være minst ${{amount}}",
|
||||
"error.reloadTriggerMin": "Saldo-trigger må være minst ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - Den åpne kildekode kodingsagenten.",
|
||||
|
||||
"home.title": "OpenCode | Den åpne kildekode AI-kodingsagenten",
|
||||
|
||||
"temp.title": "opencode | AI-kodingsagent bygget for terminalen",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", inkludert lokale modeller",
|
||||
"temp.screenshot.caption": "opencode TUI med tokyonight-tema",
|
||||
"temp.screenshot.alt": "opencode TUI med tokyonight-tema",
|
||||
"temp.logoLightAlt": "opencode logo lys",
|
||||
"temp.logoDarkAlt": "opencode logo mørk",
|
||||
|
||||
"home.banner.badge": "Ny",
|
||||
"home.banner.text": "Desktop-app tilgjengelig i beta",
|
||||
@@ -249,24 +240,6 @@ export const dict = {
|
||||
"Alle Zen-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
|
||||
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",
|
||||
"zen.api.error.modelFormatNotSupported": "Modell {{model}} støttes ikke for format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Ingen leverandør tilgjengelig",
|
||||
"zen.api.error.providerNotSupported": "Leverandør {{provider}} støttes ikke",
|
||||
"zen.api.error.missingApiKey": "Mangler API-nøkkel.",
|
||||
"zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnementskvote overskredet. Du kan fortsette å bruke gratis modeller.",
|
||||
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Legg til en betalingsmetode her: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Utilstrekkelig saldo. Administrer faktureringen din her: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Arbeidsområdet ditt har nådd sin månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Modellen er deaktivert",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Få tilgang til verdens beste kodemodeller",
|
||||
"black.meta.description": "Få tilgang til Claude, GPT, Gemini og mer med OpenCode Black-abonnementer.",
|
||||
"black.hero.title": "Få tilgang til verdens beste kodemodeller",
|
||||
@@ -476,7 +449,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Vennligst oppdater betalingsmetoden din og prøv på nytt.",
|
||||
"workspace.reload.retrying": "Prøver på nytt...",
|
||||
"workspace.reload.retry": "Prøv på nytt",
|
||||
"workspace.reload.error.paymentFailed": "Betaling mislyktes.",
|
||||
|
||||
"workspace.payments.title": "Betalingshistorikk",
|
||||
"workspace.payments.subtitle": "Nylige betalingstransaksjoner.",
|
||||
@@ -595,10 +567,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Send",
|
||||
"enterprise.form.sending": "Sender...",
|
||||
"enterprise.form.success": "Melding sendt, vi tar kontakt snart.",
|
||||
"enterprise.form.success.submitted": "Skjemaet ble sendt inn.",
|
||||
"enterprise.form.error.allFieldsRequired": "Alle felt er obligatoriske.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Ugyldig e-postformat.",
|
||||
"enterprise.form.error.internalServer": "Intern serverfeil.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Hva er OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -631,7 +599,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Modell",
|
||||
"bench.list.table.score": "Poengsum",
|
||||
"bench.submission.error.allFieldsRequired": "Alle felt er obligatoriske.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Oppgave ikke funnet",
|
||||
|
||||
@@ -14,7 +14,6 @@ export const dict = {
|
||||
"nav.home": "Strona główna",
|
||||
"nav.openMenu": "Otwórz menu",
|
||||
"nav.getStartedFree": "Zacznij za darmo",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Skopiuj logo jako SVG",
|
||||
"nav.context.copyWordmark": "Skopiuj logotyp jako SVG",
|
||||
@@ -42,13 +41,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentacja",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "jasne logo opencode",
|
||||
"notFound.logoDarkAlt": "ciemne logo opencode",
|
||||
|
||||
"user.logout": "Wyloguj się",
|
||||
|
||||
"auth.callback.error.codeMissing": "Nie znaleziono kodu autoryzacji.",
|
||||
|
||||
"workspace.select": "Wybierz obszar roboczy",
|
||||
"workspace.createNew": "+ Utwórz nowy obszar roboczy",
|
||||
"workspace.modal.title": "Utwórz nowy obszar roboczy",
|
||||
@@ -80,8 +75,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Kwota doładowania musi wynosić co najmniej ${{amount}}",
|
||||
"error.reloadTriggerMin": "Próg salda musi wynosić co najmniej ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - Otwartoźródłowy agent programistyczny.",
|
||||
|
||||
"home.title": "OpenCode | Open source'owy agent AI do kodowania",
|
||||
|
||||
"temp.title": "opencode | Agent AI do kodowania zbudowany dla terminala",
|
||||
@@ -97,8 +90,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", w tym modele lokalne",
|
||||
"temp.screenshot.caption": "OpenCode TUI z motywem tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI z motywem tokyonight",
|
||||
"temp.logoLightAlt": "jasne logo opencode",
|
||||
"temp.logoDarkAlt": "ciemne logo opencode",
|
||||
|
||||
"home.banner.badge": "Nowość",
|
||||
"home.banner.text": "Aplikacja desktopowa dostępna w wersji beta",
|
||||
@@ -250,24 +241,6 @@ export const dict = {
|
||||
"Wszystkie modele Zen są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie wykorzystują Twoich danych do trenowania modeli, z",
|
||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} nie jest obsługiwany dla formatu {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Brak dostępnego dostawcy",
|
||||
"zen.api.error.providerNotSupported": "Dostawca {{provider}} nie jest obsługiwany",
|
||||
"zen.api.error.missingApiKey": "Brak klucza API.",
|
||||
"zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Przekroczono limit subskrypcji. Możesz kontynuować korzystanie z darmowych modeli.",
|
||||
"zen.api.error.noPaymentMethod": "Brak metody płatności. Dodaj metodę płatności tutaj: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Niewystarczające saldo. Zarządzaj swoimi płatnościami tutaj: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Twoja przestrzeń robocza osiągnęła miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Model jest wyłączony",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Dostęp do najlepszych na świecie modeli kodujących",
|
||||
"black.meta.description": "Uzyskaj dostęp do Claude, GPT, Gemini i innych dzięki planom subskrypcji OpenCode Black.",
|
||||
"black.hero.title": "Dostęp do najlepszych na świecie modeli kodujących",
|
||||
@@ -477,7 +450,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Zaktualizuj metodę płatności i spróbuj ponownie.",
|
||||
"workspace.reload.retrying": "Ponawianie...",
|
||||
"workspace.reload.retry": "Spróbuj ponownie",
|
||||
"workspace.reload.error.paymentFailed": "Płatność nie powiodła się.",
|
||||
|
||||
"workspace.payments.title": "Historia płatności",
|
||||
"workspace.payments.subtitle": "Ostatnie transakcje płatnicze.",
|
||||
@@ -598,10 +570,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Wyślij",
|
||||
"enterprise.form.sending": "Wysyłanie...",
|
||||
"enterprise.form.success": "Wiadomość wysłana, skontaktujemy się wkrótce.",
|
||||
"enterprise.form.success.submitted": "Formularz został pomyślnie wysłany.",
|
||||
"enterprise.form.error.allFieldsRequired": "Wszystkie pola są wymagane.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Nieprawidłowy format adresu e-mail.",
|
||||
"enterprise.form.error.internalServer": "Wewnętrzny błąd serwera.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Czym jest OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -634,7 +602,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Wynik",
|
||||
"bench.submission.error.allFieldsRequired": "Wszystkie pola są wymagane.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Nie znaleziono zadania",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Главная",
|
||||
"nav.openMenu": "Открыть меню",
|
||||
"nav.getStartedFree": "Начать бесплатно",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Скопировать логотип как SVG",
|
||||
"nav.context.copyWordmark": "Скопировать название как SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Документация",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "светлый логотип opencode",
|
||||
"notFound.logoDarkAlt": "темный логотип opencode",
|
||||
|
||||
"user.logout": "Выйти",
|
||||
|
||||
"auth.callback.error.codeMissing": "Код авторизации не найден.",
|
||||
|
||||
"workspace.select": "Выбрать рабочее пространство",
|
||||
"workspace.createNew": "+ Создать рабочее пространство",
|
||||
"workspace.modal.title": "Создать рабочее пространство",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Сумма пополнения должна быть не менее ${{amount}}",
|
||||
"error.reloadTriggerMin": "Порог баланса должен быть не менее ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - AI-агент с открытым кодом для программирования.",
|
||||
|
||||
"home.title": "OpenCode | AI-агент с открытым кодом для программирования",
|
||||
|
||||
"temp.title": "opencode | AI-агент для программирования в терминале",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", включая локальные модели",
|
||||
"temp.screenshot.caption": "OpenCode TUI с темой tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI с темой tokyonight",
|
||||
"temp.logoLightAlt": "светлый логотип opencode",
|
||||
"temp.logoDarkAlt": "темный логотип opencode",
|
||||
|
||||
"home.banner.badge": "Новое",
|
||||
"home.banner.text": "Доступно десктопное приложение (бета)",
|
||||
@@ -253,24 +244,6 @@ export const dict = {
|
||||
"Все модели Zen размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
|
||||
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
|
||||
"zen.api.error.modelFormatNotSupported": "Модель {{model}} не поддерживается для формата {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Нет доступных провайдеров",
|
||||
"zen.api.error.providerNotSupported": "Провайдер {{provider}} не поддерживается",
|
||||
"zen.api.error.missingApiKey": "Отсутствует API ключ.",
|
||||
"zen.api.error.invalidApiKey": "Неверный API ключ.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Квота подписки превышена. Вы можете продолжить использовать бесплатные модели.",
|
||||
"zen.api.error.noPaymentMethod": "Нет способа оплаты. Добавьте способ оплаты здесь: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Недостаточно средств. Управляйте оплатой здесь: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Ваше рабочее пространство достигло ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Модель отключена",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Доступ к лучшим моделям для кодинга в мире",
|
||||
"black.meta.description": "Получите доступ к Claude, GPT, Gemini и другим моделям с подпиской OpenCode Black.",
|
||||
"black.hero.title": "Доступ к лучшим моделям для кодинга в мире",
|
||||
@@ -482,7 +455,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Пожалуйста, обновите способ оплаты и попробуйте снова.",
|
||||
"workspace.reload.retrying": "Повторная попытка...",
|
||||
"workspace.reload.retry": "Повторить",
|
||||
"workspace.reload.error.paymentFailed": "Ошибка оплаты.",
|
||||
|
||||
"workspace.payments.title": "История платежей",
|
||||
"workspace.payments.subtitle": "Недавние транзакции.",
|
||||
@@ -602,10 +574,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Отправить",
|
||||
"enterprise.form.sending": "Отправка...",
|
||||
"enterprise.form.success": "Сообщение отправлено, мы скоро свяжемся с вами.",
|
||||
"enterprise.form.success.submitted": "Форма успешно отправлена.",
|
||||
"enterprise.form.error.allFieldsRequired": "Все поля обязательны.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Неверный формат email.",
|
||||
"enterprise.form.error.internalServer": "Внутренняя ошибка сервера.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Что такое OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -638,7 +606,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Агент",
|
||||
"bench.list.table.model": "Модель",
|
||||
"bench.list.table.score": "Оценка",
|
||||
"bench.submission.error.allFieldsRequired": "Все поля обязательны.",
|
||||
|
||||
"bench.detail.title": "Бенчмарк - {{task}}",
|
||||
"bench.detail.notFound": "Задача не найдена",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "หน้าหลัก",
|
||||
"nav.openMenu": "เปิดเมนู",
|
||||
"nav.getStartedFree": "เริ่มต้นฟรี",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "คัดลอกโลโก้เป็น SVG",
|
||||
"nav.context.copyWordmark": "คัดลอกตัวอักษรแบรนด์เป็น SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "เอกสาร",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "โลโก้ opencode แบบสว่าง",
|
||||
"notFound.logoDarkAlt": "โลโก้ opencode แบบมืด",
|
||||
|
||||
"user.logout": "ออกจากระบบ",
|
||||
|
||||
"auth.callback.error.codeMissing": "ไม่พบ authorization code",
|
||||
|
||||
"workspace.select": "เลือก Workspace",
|
||||
"workspace.createNew": "+ สร้าง Workspace ใหม่",
|
||||
"workspace.modal.title": "สร้าง Workspace ใหม่",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "จำนวนเงินที่โหลดซ้ำต้องมีอย่างน้อย ${{amount}}",
|
||||
"error.reloadTriggerMin": "ยอดคงเหลือที่กระตุ้นต้องมีอย่างน้อย ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - เอเจนต์เขียนโค้ดแบบโอเพนซอร์ส",
|
||||
|
||||
"home.title": "OpenCode | เอเจนต์เขียนโค้ดด้วย AI แบบโอเพนซอร์ส",
|
||||
|
||||
"temp.title": "OpenCode | เอเจนต์เขียนโค้ด AI ที่สร้างมาเพื่อเทอร์มินัล",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "รวมถึงโมเดล Local",
|
||||
"temp.screenshot.caption": "OpenCode TUI พร้อมธีม tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI พร้อมธีม tokyonight",
|
||||
"temp.logoLightAlt": "โลโก้ opencode แบบสว่าง",
|
||||
"temp.logoDarkAlt": "โลโก้ opencode แบบมืด",
|
||||
|
||||
"home.banner.badge": "ใหม่",
|
||||
"home.banner.text": "แอปเดสก์ท็อปพร้อมใช้งานในเวอร์ชันเบต้า",
|
||||
@@ -248,24 +239,6 @@ export const dict = {
|
||||
"โมเดล Zen ทั้งหมดโฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
|
||||
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "ไม่รองรับโมเดล {{model}} สำหรับรูปแบบ {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "ไม่มีผู้ให้บริการที่พร้อมใช้งาน",
|
||||
"zen.api.error.providerNotSupported": "ไม่รองรับผู้ให้บริการ {{provider}}",
|
||||
"zen.api.error.missingApiKey": "ไม่มี API key",
|
||||
"zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"โควต้าการสมัครสมาชิกเกินขีดจำกัด คุณสามารถดำเนินการต่อโดยใช้โมเดลฟรี",
|
||||
"zen.api.error.noPaymentMethod": "ไม่มีวิธีการชำระเงิน เพิ่มวิธีการชำระเงินที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "ยอดเงินคงเหลือไม่เพียงพอ จัดการการเรียกเก็บเงินของคุณที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Workspace ของคุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน",
|
||||
|
||||
"black.meta.title": "OpenCode Black | เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
|
||||
"black.meta.description": "เข้าถึง Claude, GPT, Gemini และอื่นๆ ด้วยแผนสมาชิก OpenCode Black",
|
||||
"black.hero.title": "เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
|
||||
@@ -475,7 +448,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "โปรดอัปเดตวิธีการชำระเงินของคุณแล้วลองอีกครั้ง",
|
||||
"workspace.reload.retrying": "กำลังลองอีกครั้ง...",
|
||||
"workspace.reload.retry": "ลองอีกครั้ง",
|
||||
"workspace.reload.error.paymentFailed": "การชำระเงินล้มเหลว",
|
||||
|
||||
"workspace.payments.title": "ประวัติการชำระเงิน",
|
||||
"workspace.payments.subtitle": "รายการธุรกรรมการชำระเงินล่าสุด",
|
||||
@@ -594,10 +566,6 @@ export const dict = {
|
||||
"enterprise.form.send": "ส่ง",
|
||||
"enterprise.form.sending": "กำลังส่ง...",
|
||||
"enterprise.form.success": "ส่งข้อความแล้ว เราจะติดต่อกลับเร็วๆ นี้",
|
||||
"enterprise.form.success.submitted": "ส่งแบบฟอร์มสำเร็จแล้ว",
|
||||
"enterprise.form.error.allFieldsRequired": "จำเป็นต้องกรอกทุกช่อง",
|
||||
"enterprise.form.error.invalidEmailFormat": "รูปแบบอีเมลไม่ถูกต้อง",
|
||||
"enterprise.form.error.internalServer": "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์",
|
||||
"enterprise.faq.title": "คำถามที่พบบ่อย",
|
||||
"enterprise.faq.q1": "OpenCode Enterprise คืออะไร?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -630,7 +598,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "เอเจนต์",
|
||||
"bench.list.table.model": "โมเดล",
|
||||
"bench.list.table.score": "คะแนน",
|
||||
"bench.submission.error.allFieldsRequired": "จำเป็นต้องกรอกทุกช่อง",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "ไม่พบงาน",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Ana sayfa",
|
||||
"nav.openMenu": "Menüyü aç",
|
||||
"nav.getStartedFree": "Ücretsiz başla",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Logoyu SVG olarak kopyala",
|
||||
"nav.context.copyWordmark": "Wordmark'ı SVG olarak kopyala",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokümantasyon",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode açık logo",
|
||||
"notFound.logoDarkAlt": "opencode koyu logo",
|
||||
|
||||
"user.logout": "Çıkış",
|
||||
|
||||
"auth.callback.error.codeMissing": "Yetkilendirme kodu bulunamadı.",
|
||||
|
||||
"workspace.select": "Çalışma alanı seç",
|
||||
"workspace.createNew": "+ Yeni çalışma alanı oluştur",
|
||||
"workspace.modal.title": "Yeni çalışma alanı oluştur",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Yükleme tutarı en az ${{amount}} olmalıdır",
|
||||
"error.reloadTriggerMin": "Bakiye tetikleyicisi en az ${{amount}} olmalıdır",
|
||||
|
||||
"app.meta.description": "OpenCode - Açık kaynaklı kodlama ajanı.",
|
||||
|
||||
"home.title": "OpenCode | Açık kaynaklı yapay zeka kodlama ajanı",
|
||||
|
||||
"temp.title": "opencode | Terminal için geliştirilmiş yapay zeka kodlama ajanı",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": " üzerinden destekler",
|
||||
"temp.screenshot.caption": "opencode TUI ve tokyonight teması",
|
||||
"temp.screenshot.alt": "tokyonight temalı opencode TUI",
|
||||
"temp.logoLightAlt": "opencode açık logo",
|
||||
"temp.logoDarkAlt": "opencode koyu logo",
|
||||
|
||||
"home.banner.badge": "Yeni",
|
||||
"home.banner.text": "Masaüstü uygulaması beta olarak kullanılabilir",
|
||||
@@ -251,24 +242,6 @@ export const dict = {
|
||||
"Tüm Zen modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
|
||||
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",
|
||||
"zen.api.error.modelFormatNotSupported": "{{model}} modeli {{format}} formatı için desteklenmiyor",
|
||||
"zen.api.error.noProviderAvailable": "Kullanılabilir sağlayıcı yok",
|
||||
"zen.api.error.providerNotSupported": "{{provider}} sağlayıcısı desteklenmiyor",
|
||||
"zen.api.error.missingApiKey": "API anahtarı eksik.",
|
||||
"zen.api.error.invalidApiKey": "Geçersiz API anahtarı.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonelik kotası aşıldı. {{retryIn}} içinde tekrar deneyin.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonelik kotası aşıldı. Ücretsiz modelleri kullanmaya devam edebilirsiniz.",
|
||||
"zen.api.error.noPaymentMethod": "Ödeme yöntemi bulunamadı. Buradan bir ödeme yöntemi ekleyin: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Yetersiz bakiye. Faturalandırmanızı buradan yönetin: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Çalışma alanınız aylık ${{amount}} harcama limitine ulaştı. Limitlerinizi buradan yönetin: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Model devre dışı",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Dünyanın en iyi kodlama modellerine erişin",
|
||||
"black.meta.description": "OpenCode Black abonelik planlarıyla Claude, GPT, Gemini ve daha fazlasına erişin.",
|
||||
"black.hero.title": "Dünyanın en iyi kodlama modellerine erişin",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Lütfen ödeme yönteminizi güncelleyin ve tekrar deneyin.",
|
||||
"workspace.reload.retrying": "Yeniden deneniyor...",
|
||||
"workspace.reload.retry": "Yeniden dene",
|
||||
"workspace.reload.error.paymentFailed": "Ödeme başarısız.",
|
||||
|
||||
"workspace.payments.title": "Ödeme Geçmişi",
|
||||
"workspace.payments.subtitle": "Son ödeme işlemleri.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Gönder",
|
||||
"enterprise.form.sending": "Gönderiliyor...",
|
||||
"enterprise.form.success": "Mesaj gönderildi, yakında size dönüş yapacağız.",
|
||||
"enterprise.form.success.submitted": "Form başarıyla gönderildi.",
|
||||
"enterprise.form.error.allFieldsRequired": "Tüm alanlar gereklidir.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Geçersiz e-posta formatı.",
|
||||
"enterprise.form.error.internalServer": "İç sunucu hatası.",
|
||||
"enterprise.faq.title": "SSS",
|
||||
"enterprise.faq.q1": "OpenCode Enterprise nedir?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Ajan",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Puan",
|
||||
"bench.submission.error.allFieldsRequired": "Tüm alanlar gereklidir.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Görev bulunamadı",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "首页",
|
||||
"nav.openMenu": "打开菜单",
|
||||
"nav.getStartedFree": "免费开始",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "复制 Logo (SVG)",
|
||||
"nav.context.copyWordmark": "复制商标 (SVG)",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "文档",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo 亮色",
|
||||
"notFound.logoDarkAlt": "opencode logo 暗色",
|
||||
|
||||
"user.logout": "退出登录",
|
||||
|
||||
"auth.callback.error.codeMissing": "未找到授权码。",
|
||||
|
||||
"workspace.select": "选择工作区",
|
||||
"workspace.createNew": "+ 新建工作区",
|
||||
"workspace.modal.title": "新建工作区",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "充值金额必须至少为 ${{amount}}",
|
||||
"error.reloadTriggerMin": "余额触发阈值必须至少为 ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - 开源编程代理。",
|
||||
|
||||
"home.title": "OpenCode | 开源 AI 编程代理",
|
||||
|
||||
"temp.title": "OpenCode | 专为终端打造的 AI 编程代理",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ",包括本地模型",
|
||||
"temp.screenshot.caption": "使用 Tokyonight 主题的 OpenCode TUI",
|
||||
"temp.screenshot.alt": "使用 Tokyonight 主题的 OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencode logo 亮色",
|
||||
"temp.logoDarkAlt": "opencode logo 暗色",
|
||||
|
||||
"home.banner.badge": "新",
|
||||
"home.banner.text": "桌面应用 Beta 版现已推出",
|
||||
@@ -238,22 +229,6 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
|
||||
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "格式 {{format}} 不支持模型 {{model}}",
|
||||
"zen.api.error.noProviderAvailable": "没有可用的提供商",
|
||||
"zen.api.error.providerNotSupported": "不支持提供商 {{provider}}",
|
||||
"zen.api.error.missingApiKey": "缺少 API 密钥。",
|
||||
"zen.api.error.invalidApiKey": "无效的 API 密钥。",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出订阅配额。您可以继续使用免费模型。",
|
||||
"zen.api.error.noPaymentMethod": "没有付款方式。请在此处添加付款方式:{{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "余额不足。请在此处管理您的计费:{{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "模型已禁用",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 访问全球顶尖编程模型",
|
||||
"black.meta.description": "通过 OpenCode Black 订阅计划使用 Claude, GPT, Gemini 等模型。",
|
||||
"black.hero.title": "访问全球顶尖编程模型",
|
||||
@@ -461,7 +436,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
|
||||
"workspace.reload.retrying": "正在重试...",
|
||||
"workspace.reload.retry": "重试",
|
||||
"workspace.reload.error.paymentFailed": "支付失败。",
|
||||
|
||||
"workspace.payments.title": "支付历史",
|
||||
"workspace.payments.subtitle": "近期支付交易。",
|
||||
@@ -578,10 +552,6 @@ export const dict = {
|
||||
"enterprise.form.send": "发送",
|
||||
"enterprise.form.sending": "正在发送...",
|
||||
"enterprise.form.success": "消息已发送,我们会尽快与您联系。",
|
||||
"enterprise.form.success.submitted": "表单提交成功。",
|
||||
"enterprise.form.error.allFieldsRequired": "所有字段均为必填项。",
|
||||
"enterprise.form.error.invalidEmailFormat": "邮箱格式无效。",
|
||||
"enterprise.form.error.internalServer": "内部服务器错误。",
|
||||
"enterprise.faq.title": "常见问题",
|
||||
"enterprise.faq.q1": "什么是 OpenCode 企业版?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -614,7 +584,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "代理",
|
||||
"bench.list.table.model": "模型",
|
||||
"bench.list.table.score": "分数",
|
||||
"bench.submission.error.allFieldsRequired": "所有字段均为必填项。",
|
||||
|
||||
"bench.detail.title": "基准测试 - {{task}}",
|
||||
"bench.detail.notFound": "未找到任务",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "首頁",
|
||||
"nav.openMenu": "開啟選單",
|
||||
"nav.getStartedFree": "免費開始使用",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "複製標誌(SVG)",
|
||||
"nav.context.copyWordmark": "複製字標(SVG)",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "文件",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode 淺色標誌",
|
||||
"notFound.logoDarkAlt": "opencode 深色標誌",
|
||||
|
||||
"user.logout": "登出",
|
||||
|
||||
"auth.callback.error.codeMissing": "找不到授權碼。",
|
||||
|
||||
"workspace.select": "選取工作區",
|
||||
"workspace.createNew": "+ 建立新工作區",
|
||||
"workspace.modal.title": "建立新工作區",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "儲值金額必須至少為 ${{amount}}",
|
||||
"error.reloadTriggerMin": "餘額觸發門檻必須至少為 ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - 開源編碼代理。",
|
||||
|
||||
"home.title": "OpenCode | 開源 AI 編碼代理",
|
||||
|
||||
"temp.title": "OpenCode | 專為終端打造的 AI 編碼代理",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "支援 75+ 家 LLM 供應商,包括本地模型",
|
||||
"temp.screenshot.caption": "使用 tokyonight 主題的 OpenCode TUI",
|
||||
"temp.screenshot.alt": "使用 tokyonight 主題的 OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencode 淺色標誌",
|
||||
"temp.logoDarkAlt": "opencode 深色標誌",
|
||||
|
||||
"home.banner.badge": "新",
|
||||
"home.banner.text": "桌面應用已推出 Beta",
|
||||
@@ -238,22 +229,6 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均在美國託管。供應商遵循零留存政策,不會將你的資料用於模型訓練,並且有",
|
||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
|
||||
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "模型 {{model}} 不支援格式 {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "無可用的供應商",
|
||||
"zen.api.error.providerNotSupported": "不支援供應商 {{provider}}",
|
||||
"zen.api.error.missingApiKey": "缺少 API 金鑰。",
|
||||
"zen.api.error.invalidApiKey": "無效的 API 金鑰。",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出訂閱配額。你可以繼續使用免費模型。",
|
||||
"zen.api.error.noPaymentMethod": "無付款方式。請在此處新增付款方式:{{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "餘額不足。請在此處管理你的帳務:{{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "模型已停用",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 存取全球最佳編碼模型",
|
||||
"black.meta.description": "透過 OpenCode Black 訂閱方案存取 Claude、GPT、Gemini 等模型。",
|
||||
"black.hero.title": "存取全球最佳編碼模型",
|
||||
@@ -461,7 +436,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "請更新你的付款方式並重試。",
|
||||
"workspace.reload.retrying": "重試中...",
|
||||
"workspace.reload.retry": "重試",
|
||||
"workspace.reload.error.paymentFailed": "付款失敗。",
|
||||
|
||||
"workspace.payments.title": "付款紀錄",
|
||||
"workspace.payments.subtitle": "最近的付款交易。",
|
||||
@@ -577,10 +551,6 @@ export const dict = {
|
||||
"enterprise.form.send": "傳送",
|
||||
"enterprise.form.sending": "傳送中...",
|
||||
"enterprise.form.success": "訊息已傳送,我們會盡快與你聯絡。",
|
||||
"enterprise.form.success.submitted": "表單已成功送出。",
|
||||
"enterprise.form.error.allFieldsRequired": "所有欄位均為必填。",
|
||||
"enterprise.form.error.invalidEmailFormat": "無效的電子郵件格式。",
|
||||
"enterprise.form.error.internalServer": "內部伺服器錯誤。",
|
||||
"enterprise.faq.title": "常見問題",
|
||||
"enterprise.faq.q1": "什麼是 OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -613,7 +583,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "代理",
|
||||
"bench.list.table.model": "模型",
|
||||
"bench.list.table.score": "分數",
|
||||
"bench.submission.error.allFieldsRequired": "所有欄位均為必填。",
|
||||
|
||||
"bench.detail.title": "評測 - {{task}}",
|
||||
"bench.detail.notFound": "找不到任務",
|
||||
|
||||
@@ -48,9 +48,6 @@ const map = {
|
||||
"Provider is required": "error.providerRequired",
|
||||
"API key is required": "error.apiKeyRequired",
|
||||
"Model is required": "error.modelRequired",
|
||||
"workspace.reload.error.paymentFailed": "workspace.reload.error.paymentFailed",
|
||||
"Payment failed": "workspace.reload.error.paymentFailed",
|
||||
"Payment failed.": "workspace.reload.error.paymentFailed",
|
||||
} as const satisfies Record<string, Key>
|
||||
|
||||
export function formErrorReloadAmountMin(amount: number) {
|
||||
|
||||
@@ -16,8 +16,8 @@ export default function NotFound() {
|
||||
<div data-component="content">
|
||||
<section data-component="top">
|
||||
<a href={language.route("/")} data-slot="logo-link">
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("notFound.logoLightAlt")} />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("notFound.logoDarkAlt")} />
|
||||
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
|
||||
</a>
|
||||
<h1 data-slot="title">{i18n.t("notFound.heading")}</h1>
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { AWS } from "@opencode-ai/console-core/aws.js"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
interface EnterpriseFormData {
|
||||
name: string
|
||||
@@ -11,19 +9,18 @@ interface EnterpriseFormData {
|
||||
}
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const dict = i18n(localeFromRequest(event.request))
|
||||
try {
|
||||
const body = (await event.request.json()) as EnterpriseFormData
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.role || !body.email || !body.message) {
|
||||
return Response.json({ error: dict["enterprise.form.error.allFieldsRequired"] }, { status: 400 })
|
||||
return Response.json({ error: "All fields are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(body.email)) {
|
||||
return Response.json({ error: dict["enterprise.form.error.invalidEmailFormat"] }, { status: 400 })
|
||||
return Response.json({ error: "Invalid email format" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create email content
|
||||
@@ -42,9 +39,9 @@ ${body.email}`.trim()
|
||||
replyTo: body.email,
|
||||
})
|
||||
|
||||
return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
|
||||
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error processing enterprise form:", error)
|
||||
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
||||
return Response.json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,15 @@ import { redirect } from "@solidjs/router"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { AuthClient } from "~/context/auth"
|
||||
import { useAuthSession } from "~/context/auth"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest, route } from "~/lib/language"
|
||||
|
||||
export async function GET(input: APIEvent) {
|
||||
const url = new URL(input.request.url)
|
||||
const locale = localeFromRequest(input.request)
|
||||
const dict = i18n(locale)
|
||||
|
||||
try {
|
||||
const code = url.searchParams.get("code")
|
||||
if (!code) throw new Error(dict["auth.callback.error.codeMissing"])
|
||||
if (!code) throw new Error("No code found")
|
||||
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
|
||||
if (result.err) throw new Error(result.err.message)
|
||||
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||
import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
interface SubmissionBody {
|
||||
model: string
|
||||
@@ -12,11 +10,10 @@ interface SubmissionBody {
|
||||
}
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const dict = i18n(localeFromRequest(event.request))
|
||||
const body = (await event.request.json()) as SubmissionBody
|
||||
|
||||
if (!body.model || !body.agent || !body.result) {
|
||||
return Response.json({ error: dict["bench.submission.error.allFieldsRequired"] }, { status: 400 })
|
||||
return Response.json({ error: "All fields are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
await Database.use((tx) =>
|
||||
|
||||
@@ -33,7 +33,6 @@ const brandAssets = "/opencode-brand-assets.zip"
|
||||
|
||||
export default function Brand() {
|
||||
const i18n = useI18n()
|
||||
const alt = i18n.t("brand.meta.description")
|
||||
const downloadFile = async (url: string, filename: string) => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
@@ -89,7 +88,7 @@ export default function Brand() {
|
||||
|
||||
<div data-component="brand-grid">
|
||||
<div>
|
||||
<img src={previewLogoLight} alt={alt} />
|
||||
<img src={previewLogoLight} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
|
||||
PNG
|
||||
@@ -116,7 +115,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoDark} alt={alt} />
|
||||
<img src={previewLogoDark} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
|
||||
PNG
|
||||
@@ -143,7 +142,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoLightSquare} alt={alt} />
|
||||
<img src={previewLogoLightSquare} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoLightSquarePng, "opencode-logo-light-square.png")}>
|
||||
PNG
|
||||
@@ -170,7 +169,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoDarkSquare} alt={alt} />
|
||||
<img src={previewLogoDarkSquare} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoDarkSquarePng, "opencode-logo-dark-square.png")}>
|
||||
PNG
|
||||
@@ -197,7 +196,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkLight} alt={alt} />
|
||||
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}>
|
||||
PNG
|
||||
@@ -224,7 +223,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkDark} alt={alt} />
|
||||
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}>
|
||||
PNG
|
||||
@@ -251,7 +250,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkSimpleLight} alt={alt} />
|
||||
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}>
|
||||
PNG
|
||||
@@ -278,7 +277,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkSimpleDark} alt={alt} />
|
||||
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")}>
|
||||
PNG
|
||||
|
||||
@@ -19,7 +19,7 @@ const downloadNames: Record<string, string> = {
|
||||
|
||||
export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||
const assetName = assetNames[platform]
|
||||
if (!assetName) return new Response(null, { status: 404 })
|
||||
if (!assetName) return new Response("Not Found", { status: 404 })
|
||||
|
||||
const resp = await fetch(
|
||||
`https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`,
|
||||
|
||||
@@ -306,7 +306,7 @@ export async function POST(input: APIEvent) {
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
reload: false,
|
||||
reloadError: errorMessage ?? "workspace.reload.error.paymentFailed",
|
||||
reloadError: errorMessage ?? "Payment failed.",
|
||||
timeReloadError: sql`now()`,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, Actor.workspace())),
|
||||
|
||||
@@ -47,8 +47,8 @@ export default function Home() {
|
||||
|
||||
<div data-component="content">
|
||||
<section data-component="top">
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("temp.logoLightAlt")} />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("temp.logoDarkAlt")} />
|
||||
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
|
||||
<h1 data-slot="title">{i18n.t("temp.hero.title")}</h1>
|
||||
<div data-slot="login">
|
||||
<a href="/auth">{i18n.t("temp.zen")}</a>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { queryBillingInfo } from "../../common"
|
||||
import styles from "./lite-section.module.css"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { formError } from "~/lib/form-error"
|
||||
|
||||
const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
@@ -115,7 +114,7 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
||||
const setLiteUseBalance = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
|
||||
return json(
|
||||
|
||||
@@ -202,8 +202,7 @@ export function ReloadSection() {
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
. {i18n.t("workspace.reload.reason")}{" "}
|
||||
{localizeError(i18n.t, billingInfo()?.reloadError ?? undefined).replace(/\.$/, "")}.{" "}
|
||||
. {i18n.t("workspace.reload.reason")} {billingInfo()?.reloadError?.replace(/\.$/, "")}.{" "}
|
||||
{i18n.t("workspace.reload.updatePaymentMethod")}
|
||||
</p>
|
||||
<form action={reload} method="post" data-slot="create-form">
|
||||
|
||||
@@ -35,8 +35,6 @@ import { createTrialLimiter } from "./trialLimiter"
|
||||
import { createStickyTracker } from "./stickyProviderTracker"
|
||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { i18n, type Key } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||
type RetryOptions = {
|
||||
@@ -45,15 +43,6 @@ type RetryOptions = {
|
||||
}
|
||||
type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "lite" | "balance"
|
||||
|
||||
function resolve(text: string, params?: Record<string, string | number>) {
|
||||
if (!params) return text
|
||||
return text.replace(/\{\{(\w+)\}\}/g, (raw, key) => {
|
||||
const value = params[key]
|
||||
if (value === undefined || value === null) return raw
|
||||
return String(value)
|
||||
})
|
||||
}
|
||||
|
||||
export async function handler(
|
||||
input: APIEvent,
|
||||
opts: {
|
||||
@@ -71,8 +60,6 @@ export async function handler(
|
||||
|
||||
const MAX_FAILOVER_RETRIES = 3
|
||||
const MAX_429_RETRIES = 3
|
||||
const dict = i18n(localeFromRequest(input.request))
|
||||
const t = (key: Key, params?: Record<string, string | number>) => resolve(dict[key], params)
|
||||
const ADMIN_WORKSPACES = [
|
||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
|
||||
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
|
||||
@@ -99,7 +86,7 @@ export async function handler(
|
||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request)
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request.headers)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
@@ -372,20 +359,14 @@ export async function handler(
|
||||
}
|
||||
|
||||
function validateModel(zenData: ZenData, reqModel: string) {
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel }))
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
|
||||
|
||||
const modelId = reqModel as keyof typeof zenData.models
|
||||
const modelData = Array.isArray(zenData.models[modelId])
|
||||
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
|
||||
: zenData.models[modelId]
|
||||
|
||||
if (!modelData)
|
||||
throw new ModelError(
|
||||
t("zen.api.error.modelFormatNotSupported", {
|
||||
model: reqModel,
|
||||
format: opts.format,
|
||||
}),
|
||||
)
|
||||
if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
|
||||
|
||||
logger.metric({ model: modelId })
|
||||
|
||||
@@ -437,9 +418,8 @@ export async function handler(
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
})()
|
||||
|
||||
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
|
||||
if (!(modelProvider.id in zenData.providers))
|
||||
throw new ModelError(t("zen.api.error.providerNotSupported", { provider: modelProvider.id }))
|
||||
if (!modelProvider) throw new ModelError("No provider available")
|
||||
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
|
||||
|
||||
return {
|
||||
...modelProvider,
|
||||
@@ -459,7 +439,7 @@ export async function handler(
|
||||
const apiKey = opts.parseApiKey(input.request.headers)
|
||||
if (!apiKey || apiKey === "public") {
|
||||
if (modelInfo.allowAnonymous) return
|
||||
throw new AuthError(t("zen.api.error.missingApiKey"))
|
||||
throw new AuthError("Missing API key.")
|
||||
}
|
||||
|
||||
const data = await Database.use((tx) =>
|
||||
@@ -540,13 +520,13 @@ export async function handler(
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!data) throw new AuthError(t("zen.api.error.invalidApiKey"))
|
||||
if (!data) throw new AuthError("Invalid API key.")
|
||||
if (
|
||||
modelInfo.id.startsWith("alpha-") &&
|
||||
Resource.App.stage === "production" &&
|
||||
!ADMIN_WORKSPACES.includes(data.workspaceID)
|
||||
)
|
||||
throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id }))
|
||||
throw new AuthError(`Model ${modelInfo.id} not supported`)
|
||||
|
||||
logger.metric({
|
||||
api_key: data.apiKey,
|
||||
@@ -610,9 +590,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -628,9 +606,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -656,7 +632,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -671,7 +647,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -686,7 +662,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -699,10 +675,14 @@ export async function handler(
|
||||
|
||||
// Validate pay as you go billing
|
||||
const billing = authInfo.billing
|
||||
const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing`
|
||||
const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members`
|
||||
if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
|
||||
if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl }))
|
||||
if (!billing.paymentMethodID)
|
||||
throw new CreditsError(
|
||||
`No payment method. Add a payment method here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
|
||||
)
|
||||
if (billing.balance <= 0)
|
||||
throw new CreditsError(
|
||||
`Insufficient balance. Manage your billing here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
|
||||
)
|
||||
|
||||
const now = new Date()
|
||||
const currentYear = now.getUTCFullYear()
|
||||
@@ -716,10 +696,7 @@ export async function handler(
|
||||
currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
)
|
||||
throw new MonthlyLimitError(
|
||||
t("zen.api.error.workspaceMonthlyLimitReached", {
|
||||
amount: billing.monthlyLimit,
|
||||
billingUrl,
|
||||
}),
|
||||
`Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -731,10 +708,7 @@ export async function handler(
|
||||
currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
)
|
||||
throw new UserLimitError(
|
||||
t("zen.api.error.userMonthlyLimitReached", {
|
||||
amount: authInfo.user.monthlyLimit,
|
||||
membersUrl,
|
||||
}),
|
||||
`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
|
||||
)
|
||||
|
||||
return "balance"
|
||||
@@ -742,7 +716,7 @@ export async function handler(
|
||||
|
||||
function validateModelSettings(authInfo: AuthInfo) {
|
||||
if (!authInfo) return
|
||||
if (authInfo.isDisabled) throw new ModelError(t("zen.api.error.modelDisabled"))
|
||||
if (authInfo.isDisabled) throw new ModelError("Model is disabled")
|
||||
}
|
||||
|
||||
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
|
||||
|
||||
@@ -3,14 +3,11 @@ import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { FreeUsageLimitError } from "./error"
|
||||
import { logger } from "./logger"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, request: Request) {
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) {
|
||||
if (!limit) return
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const limitValue = limit.checkHeader && !request.headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
@@ -39,7 +36,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
|
||||
logger.debug(`rate limit total: ${total}`)
|
||||
if (total >= limitValue)
|
||||
throw new FreeUsageLimitError(
|
||||
dict["zen.api.error.rateLimitExceeded"],
|
||||
`Rate limit exceeded. Please try again later.`,
|
||||
limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { FileSSR } from "@opencode-ai/ui/file-ssr"
|
||||
import { clientOnly } from "@solidjs/start"
|
||||
import { type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { Base64 } from "js-base64"
|
||||
|
||||
@@ -267,9 +268,10 @@ export default function () {
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Show when={provider()}>
|
||||
<ProviderIcon id={provider()!} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||
</Show>
|
||||
<ProviderIcon
|
||||
id={provider() as IconName}
|
||||
class="size-3.5 shrink-0 text-icon-strong-base"
|
||||
/>
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE `workspace` (
|
||||
`id` text PRIMARY KEY,
|
||||
`branch` text,
|
||||
`project_id` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
@@ -1,959 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40",
|
||||
"prevIds": ["d2736e43-700f-4e9e-8151-9f2f0d967bc8"],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "control_account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "branch",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "config",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "commands",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_workspace_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["email", "url"],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE `session` ADD `workspace_id` text;--> statement-breakpoint
|
||||
CREATE INDEX `session_workspace_idx` ON `session` (`workspace_id`);
|
||||
@@ -1,983 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "572fb732-56f4-4b1e-b981-77152c9980dd",
|
||||
"prevIds": ["1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40"],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "control_account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "branch",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "config",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "commands",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "workspace_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_workspace_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["email", "url"],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "workspace_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_workspace_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -2,9 +2,6 @@ import { Server } from "../../server/server"
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Workspace } from "../../control-plane/workspace"
|
||||
import { Project } from "../../project/project"
|
||||
import { Installation } from "../../installation"
|
||||
|
||||
export const ServeCommand = cmd({
|
||||
command: "serve",
|
||||
@@ -17,15 +14,7 @@ export const ServeCommand = cmd({
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = Server.listen(opts)
|
||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||
|
||||
let workspaceSync: Array<ReturnType<typeof Workspace.startSyncing>> = []
|
||||
// Only available in development right now
|
||||
if (Installation.isLocal()) {
|
||||
workspaceSync = Project.list().map((project) => Workspace.startSyncing(project))
|
||||
}
|
||||
|
||||
await new Promise(() => {})
|
||||
await server.stop()
|
||||
await Promise.all(workspaceSync.map((item) => item.stop()))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
For,
|
||||
Match,
|
||||
on,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
useContext,
|
||||
@@ -1324,8 +1323,6 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
return props.message.time.completed - user.time.created
|
||||
})
|
||||
|
||||
const keybind = useKeybind()
|
||||
|
||||
return (
|
||||
<>
|
||||
<For each={props.parts}>
|
||||
@@ -1343,14 +1340,6 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={props.parts.some((x) => x.type === "tool" && x.tool === "task")}>
|
||||
<box paddingTop={1} paddingLeft={3}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("session_child_first")}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
|
||||
<box
|
||||
border={["left"]}
|
||||
@@ -1620,7 +1609,6 @@ function InlineTool(props: {
|
||||
iconColor?: RGBA
|
||||
complete: any
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
children: JSX.Element
|
||||
part: ToolPart
|
||||
}) {
|
||||
@@ -1677,18 +1665,11 @@ function InlineTool(props: {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={props.spinner}>
|
||||
<Spinner color={fg()} children={props.children} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
||||
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
||||
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
|
||||
</Show>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
||||
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
||||
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
|
||||
</Show>
|
||||
</text>
|
||||
<Show when={error() && !denied()}>
|
||||
<text fg={theme.error}>{error()}</text>
|
||||
</Show>
|
||||
@@ -1855,7 +1836,6 @@ function Glob(props: ToolProps<typeof GlobTool>) {
|
||||
|
||||
function Read(props: ToolProps<typeof ReadTool>) {
|
||||
const { theme } = useTheme()
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const loaded = createMemo(() => {
|
||||
if (props.part.state.status !== "completed") return []
|
||||
if (props.part.state.time.compacted) return []
|
||||
@@ -1865,13 +1845,7 @@ function Read(props: ToolProps<typeof ReadTool>) {
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<InlineTool
|
||||
icon="→"
|
||||
pending="Reading file..."
|
||||
complete={props.input.filePath}
|
||||
spinner={isRunning()}
|
||||
part={props.part}
|
||||
>
|
||||
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
||||
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
@@ -1947,60 +1921,62 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
const local = useLocal()
|
||||
const sync = useSync()
|
||||
|
||||
onMount(() => {
|
||||
if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length)
|
||||
sync.session.sync(props.metadata.sessionId)
|
||||
})
|
||||
|
||||
const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? [])
|
||||
|
||||
const tools = createMemo(() => {
|
||||
return messages().flatMap((msg) =>
|
||||
const sessionID = props.metadata.sessionId
|
||||
const msgs = sync.data.message[sessionID ?? ""] ?? []
|
||||
return msgs.flatMap((msg) =>
|
||||
(sync.data.part[msg.id] ?? [])
|
||||
.filter((part): part is ToolPart => part.type === "tool")
|
||||
.map((part) => ({ tool: part.tool, state: part.state })),
|
||||
)
|
||||
})
|
||||
|
||||
const current = createMemo(() => tools().findLast((x) => (x.state as any).title))
|
||||
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
|
||||
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
|
||||
const duration = createMemo(() => {
|
||||
const first = messages().find((x) => x.role === "user")?.time.created
|
||||
const assistant = messages().findLast((x) => x.role === "assistant")?.time.completed
|
||||
if (!first || !assistant) return 0
|
||||
return assistant - first
|
||||
})
|
||||
|
||||
return (
|
||||
<InlineTool
|
||||
icon="≡"
|
||||
spinner={isRunning()}
|
||||
complete={props.input.description}
|
||||
pending="Delegating..."
|
||||
part={props.part}
|
||||
>
|
||||
{props.input.description}
|
||||
<Show when={isRunning() && tools().length > 0}>
|
||||
{" "}
|
||||
· {tools().length} toolcalls
|
||||
<Show fallback={"\n└ Running..."} when={current()}>
|
||||
{(item) => {
|
||||
const title = createMemo(() => (item().state as any).title)
|
||||
return (
|
||||
<>
|
||||
{"\n"}└ {Locale.titlecase(item().tool)} {title()}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={duration() && props.part.state.status === "completed"}>
|
||||
{"\n "}
|
||||
{tools().length} toolcalls · {Locale.duration(duration())}
|
||||
</Show>
|
||||
</InlineTool>
|
||||
<Switch>
|
||||
<Match when={props.input.description || props.input.subagent_type}>
|
||||
<BlockTool
|
||||
title={"# " + Locale.titlecase(props.input.subagent_type ?? "unknown") + " Task"}
|
||||
onClick={
|
||||
props.metadata.sessionId
|
||||
? () => navigate({ type: "session", sessionID: props.metadata.sessionId! })
|
||||
: undefined
|
||||
}
|
||||
part={props.part}
|
||||
spinner={isRunning()}
|
||||
>
|
||||
<box>
|
||||
<text style={{ fg: theme.textMuted }}>
|
||||
{props.input.description} ({tools().length} toolcalls)
|
||||
</text>
|
||||
<Show when={current()}>
|
||||
{(item) => {
|
||||
const title = item().state.status === "completed" ? (item().state as any).title : ""
|
||||
return (
|
||||
<text style={{ fg: item().state.status === "error" ? theme.error : theme.textMuted }}>
|
||||
└ {Locale.titlecase(item().tool)} {title}
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={props.metadata.sessionId}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("session_child_first")}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
</text>
|
||||
</Show>
|
||||
</BlockTool>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="#" pending="Delegating..." complete={props.input.subagent_type} part={props.part}>
|
||||
{props.input.subagent_type} Task {props.input.description}
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,59 @@
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { WorkspaceServer } from "../../control-plane/workspace-server/server"
|
||||
import { Installation } from "../../installation"
|
||||
|
||||
export const WorkspaceServeCommand = cmd({
|
||||
command: "workspace-serve",
|
||||
builder: (yargs) => withNetworkOptions(yargs),
|
||||
describe: "starts a remote workspace event server",
|
||||
describe: "starts a remote workspace websocket server",
|
||||
handler: async (args) => {
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = WorkspaceServer.Listen(opts)
|
||||
console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`)
|
||||
const server = Bun.serve<{ id: string }>({
|
||||
hostname: opts.hostname,
|
||||
port: opts.port,
|
||||
fetch(req, server) {
|
||||
const url = new URL(req.url)
|
||||
if (url.pathname === "/ws") {
|
||||
const id = Bun.randomUUIDv7()
|
||||
if (server.upgrade(req, { data: { id } })) return
|
||||
return new Response("Upgrade failed", { status: 400 })
|
||||
}
|
||||
|
||||
if (url.pathname === "/health") {
|
||||
return new Response("ok", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/plain; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
service: "workspace-server",
|
||||
ws: `ws://${server.hostname}:${server.port}/ws`,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
websocket: {
|
||||
open(ws) {
|
||||
ws.send(JSON.stringify({ type: "ready", id: ws.data.id }))
|
||||
},
|
||||
message(ws, msg) {
|
||||
const text = typeof msg === "string" ? msg : msg.toString()
|
||||
ws.send(JSON.stringify({ type: "message", id: ws.data.id, text }))
|
||||
},
|
||||
close() {},
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`workspace websocket server listening on ws://${server.hostname}:${server.port}/ws`)
|
||||
await new Promise(() => {})
|
||||
await server.stop()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { WorktreeAdaptor } from "./worktree"
|
||||
import type { Config } from "../config"
|
||||
import type { Adaptor } from "./types"
|
||||
|
||||
export function getAdaptor(config: Config): Adaptor {
|
||||
switch (config.type) {
|
||||
case "worktree":
|
||||
return WorktreeAdaptor
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { Config } from "../config"
|
||||
|
||||
export type Adaptor<T extends Config = Config> = {
|
||||
create(from: T, branch?: string | null): Promise<{ config: T; init: () => Promise<void> }>
|
||||
remove(from: T): Promise<void>
|
||||
request(from: T, method: string, url: string, data?: BodyInit, signal?: AbortSignal): Promise<Response | undefined>
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Worktree } from "@/worktree"
|
||||
import type { Config } from "../config"
|
||||
import type { Adaptor } from "./types"
|
||||
|
||||
type WorktreeConfig = Extract<Config, { type: "worktree" }>
|
||||
|
||||
export const WorktreeAdaptor: Adaptor<WorktreeConfig> = {
|
||||
async create(_from: WorktreeConfig, _branch: string) {
|
||||
const next = await Worktree.create(undefined)
|
||||
return {
|
||||
config: {
|
||||
type: "worktree",
|
||||
directory: next.directory,
|
||||
},
|
||||
// Hack for now: `Worktree.create` puts all its async code in a
|
||||
// `setTimeout` so it doesn't use this, but we should change that
|
||||
init: async () => {},
|
||||
}
|
||||
},
|
||||
async remove(config: WorktreeConfig) {
|
||||
await Worktree.remove({ directory: config.directory })
|
||||
},
|
||||
async request(_from: WorktreeConfig, _method: string, _url: string, _data?: BodyInit, _signal?: AbortSignal) {
|
||||
throw new Error("worktree does not support request")
|
||||
},
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import z from "zod"
|
||||
|
||||
export const Config = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
directory: z.string(),
|
||||
type: z.literal("worktree"),
|
||||
}),
|
||||
])
|
||||
|
||||
export type Config = z.infer<typeof Config>
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Instance } from "@/project/instance"
|
||||
import type { MiddlewareHandler } from "hono"
|
||||
import { Installation } from "../installation"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { Workspace } from "./workspace"
|
||||
|
||||
// This middleware forwards all non-GET requests if the workspace is a
|
||||
// remote. The remote workspace needs to handle session mutations
|
||||
async function proxySessionRequest(req: Request) {
|
||||
if (req.method === "GET") return
|
||||
if (!Instance.directory.startsWith("wrk_")) return
|
||||
|
||||
const workspace = await Workspace.get(Instance.directory)
|
||||
if (!workspace) {
|
||||
return new Response(`Workspace not found: ${Instance.directory}`, {
|
||||
status: 500,
|
||||
headers: {
|
||||
"content-type": "text/plain; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
if (workspace.config.type === "worktree") return
|
||||
|
||||
const url = new URL(req.url)
|
||||
const body = req.method === "HEAD" ? undefined : await req.arrayBuffer()
|
||||
return getAdaptor(workspace.config).request(
|
||||
workspace.config,
|
||||
req.method,
|
||||
`${url.pathname}${url.search}`,
|
||||
body,
|
||||
req.signal,
|
||||
)
|
||||
}
|
||||
|
||||
export const SessionProxyMiddleware: MiddlewareHandler = async (c, next) => {
|
||||
// Only available in development for now
|
||||
if (!Installation.isLocal()) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const response = await proxySessionRequest(c.req.raw)
|
||||
if (response) {
|
||||
return response
|
||||
}
|
||||
return next()
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
export async function parseSSE(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
signal: AbortSignal,
|
||||
onEvent: (event: unknown) => void,
|
||||
) {
|
||||
const reader = body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buf = ""
|
||||
let last = ""
|
||||
let retry = 1000
|
||||
|
||||
const abort = () => {
|
||||
void reader.cancel().catch(() => undefined)
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abort)
|
||||
|
||||
try {
|
||||
while (!signal.aborted) {
|
||||
const chunk = await reader.read().catch(() => ({ done: true, value: undefined as Uint8Array | undefined }))
|
||||
if (chunk.done) break
|
||||
|
||||
buf += decoder.decode(chunk.value, { stream: true })
|
||||
buf = buf.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
||||
|
||||
const chunks = buf.split("\n\n")
|
||||
buf = chunks.pop() ?? ""
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
const data: string[] = []
|
||||
chunk.split("\n").forEach((line) => {
|
||||
if (line.startsWith("data:")) {
|
||||
data.push(line.replace(/^data:\s*/, ""))
|
||||
return
|
||||
}
|
||||
if (line.startsWith("id:")) {
|
||||
last = line.replace(/^id:\s*/, "")
|
||||
return
|
||||
}
|
||||
if (line.startsWith("retry:")) {
|
||||
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10)
|
||||
if (!Number.isNaN(parsed)) retry = parsed
|
||||
}
|
||||
})
|
||||
|
||||
if (!data.length) return
|
||||
const raw = data.join("\n")
|
||||
try {
|
||||
onEvent(JSON.parse(raw))
|
||||
} catch {
|
||||
onEvent({
|
||||
type: "sse.message",
|
||||
properties: {
|
||||
data: raw,
|
||||
id: last || undefined,
|
||||
retry,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener("abort", abort)
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Context } from "../util/context"
|
||||
|
||||
interface Context {
|
||||
workspaceID?: string
|
||||
}
|
||||
|
||||
const context = Context.create<Context>("workspace")
|
||||
|
||||
export const WorkspaceContext = {
|
||||
async provide<R>(input: { workspaceID?: string; fn: () => R }): Promise<R> {
|
||||
return context.provide({ workspaceID: input.workspaceID }, async () => {
|
||||
return input.fn()
|
||||
})
|
||||
},
|
||||
|
||||
get workspaceID() {
|
||||
try {
|
||||
return context.use().workspaceID
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { GlobalBus } from "../../bus/global"
|
||||
import { Hono } from "hono"
|
||||
import { streamSSE } from "hono/streaming"
|
||||
|
||||
export function WorkspaceServerRoutes() {
|
||||
return new Hono().get("/event", async (c) => {
|
||||
c.header("X-Accel-Buffering", "no")
|
||||
c.header("X-Content-Type-Options", "nosniff")
|
||||
return streamSSE(c, async (stream) => {
|
||||
const send = async (event: unknown) => {
|
||||
await stream.writeSSE({
|
||||
data: JSON.stringify(event),
|
||||
})
|
||||
}
|
||||
const handler = async (event: { directory?: string; payload: unknown }) => {
|
||||
await send(event.payload)
|
||||
}
|
||||
GlobalBus.on("event", handler)
|
||||
await send({ type: "server.connected", properties: {} })
|
||||
const heartbeat = setInterval(() => {
|
||||
void send({ type: "server.heartbeat", properties: {} })
|
||||
}, 10_000)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
stream.onAbort(() => {
|
||||
clearInterval(heartbeat)
|
||||
GlobalBus.off("event", handler)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Hono } from "hono"
|
||||
import { SessionRoutes } from "../../server/routes/session"
|
||||
import { WorkspaceServerRoutes } from "./routes"
|
||||
|
||||
export namespace WorkspaceServer {
|
||||
export function App() {
|
||||
const session = new Hono()
|
||||
.use("*", async (c, next) => {
|
||||
if (c.req.method === "GET") return c.notFound()
|
||||
await next()
|
||||
})
|
||||
.route("/", SessionRoutes())
|
||||
|
||||
return new Hono().route("/session", session).route("/", WorkspaceServerRoutes())
|
||||
}
|
||||
|
||||
export function Listen(opts: { hostname: string; port: number }) {
|
||||
return Bun.serve({
|
||||
hostname: opts.hostname,
|
||||
port: opts.port,
|
||||
fetch: App().fetch,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "@/project/project.sql"
|
||||
import type { Config } from "./config"
|
||||
|
||||
export const WorkspaceTable = sqliteTable("workspace", {
|
||||
id: text().primaryKey(),
|
||||
branch: text(),
|
||||
project_id: text()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
config: text({ mode: "json" }).notNull().$type<Config>(),
|
||||
})
|
||||
@@ -1,160 +0,0 @@
|
||||
import z from "zod"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { Project } from "@/project/project"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Log } from "@/util/log"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
import { Config } from "./config"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { parseSSE } from "./sse"
|
||||
|
||||
export namespace Workspace {
|
||||
export const Event = {
|
||||
Ready: BusEvent.define(
|
||||
"workspace.ready",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
Failed: BusEvent.define(
|
||||
"workspace.failed",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: Identifier.schema("workspace"),
|
||||
branch: z.string().nullable(),
|
||||
projectID: z.string(),
|
||||
config: Config,
|
||||
})
|
||||
.meta({
|
||||
ref: "Workspace",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
||||
return {
|
||||
id: row.id,
|
||||
branch: row.branch,
|
||||
projectID: row.project_id,
|
||||
config: row.config,
|
||||
}
|
||||
}
|
||||
|
||||
export const create = fn(
|
||||
z.object({
|
||||
id: Identifier.schema("workspace").optional(),
|
||||
projectID: Info.shape.projectID,
|
||||
branch: Info.shape.branch,
|
||||
config: Info.shape.config,
|
||||
}),
|
||||
async (input) => {
|
||||
const id = Identifier.ascending("workspace", input.id)
|
||||
|
||||
const { config, init } = await getAdaptor(input.config).create(input.config, input.branch)
|
||||
|
||||
const info: Info = {
|
||||
id,
|
||||
projectID: input.projectID,
|
||||
branch: input.branch,
|
||||
config,
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await init()
|
||||
|
||||
Database.use((db) => {
|
||||
db.insert(WorkspaceTable)
|
||||
.values({
|
||||
id: info.id,
|
||||
branch: info.branch,
|
||||
project_id: info.projectID,
|
||||
config: info.config,
|
||||
})
|
||||
.run()
|
||||
})
|
||||
|
||||
GlobalBus.emit("event", {
|
||||
directory: id,
|
||||
payload: {
|
||||
type: Event.Ready.type,
|
||||
properties: {},
|
||||
},
|
||||
})
|
||||
}, 0)
|
||||
|
||||
return info
|
||||
},
|
||||
)
|
||||
|
||||
export function list(project: Project.Info) {
|
||||
const rows = Database.use((db) =>
|
||||
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
|
||||
)
|
||||
return rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
|
||||
export const get = fn(Identifier.schema("workspace"), async (id) => {
|
||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||
if (!row) return
|
||||
return fromRow(row)
|
||||
})
|
||||
|
||||
export const remove = fn(Identifier.schema("workspace"), async (id) => {
|
||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||
if (row) {
|
||||
const info = fromRow(row)
|
||||
await getAdaptor(info.config).remove(info.config)
|
||||
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
|
||||
return info
|
||||
}
|
||||
})
|
||||
const log = Log.create({ service: "workspace-sync" })
|
||||
|
||||
async function workspaceEventLoop(space: Info, stop: AbortSignal) {
|
||||
while (!stop.aborted) {
|
||||
const res = await getAdaptor(space.config)
|
||||
.request(space.config, "GET", "/event", undefined, stop)
|
||||
.catch(() => undefined)
|
||||
if (!res || !res.ok || !res.body) {
|
||||
await Bun.sleep(1000)
|
||||
continue
|
||||
}
|
||||
await parseSSE(res.body, stop, (event) => {
|
||||
GlobalBus.emit("event", {
|
||||
directory: space.id,
|
||||
payload: event,
|
||||
})
|
||||
})
|
||||
// Wait 250ms and retry if SSE connection fails
|
||||
await Bun.sleep(250)
|
||||
}
|
||||
}
|
||||
|
||||
export function startSyncing(project: Project.Info) {
|
||||
const stop = new AbortController()
|
||||
const spaces = list(project).filter((space) => space.config.type !== "worktree")
|
||||
|
||||
spaces.forEach((space) => {
|
||||
void workspaceEventLoop(space, stop.signal).catch((error) => {
|
||||
log.warn("workspace sync listener failed", {
|
||||
workspaceID: space.id,
|
||||
error,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
async stop() {
|
||||
stop.abort()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ export namespace Identifier {
|
||||
part: "prt",
|
||||
pty: "pty",
|
||||
tool: "tool",
|
||||
workspace: "wrk",
|
||||
} as const
|
||||
|
||||
export function schema(prefix: keyof typeof prefixes) {
|
||||
|
||||
@@ -76,7 +76,6 @@ let cli = yargs(hideBin(process.argv))
|
||||
|
||||
process.env.AGENT = "1"
|
||||
process.env.OPENCODE = "1"
|
||||
process.env.OPENCODE_PID = String(process.pid)
|
||||
|
||||
Log.Default.info("opencode", {
|
||||
version: Installation.VERSION,
|
||||
|
||||
@@ -160,28 +160,6 @@ export namespace MCP {
|
||||
return typeof entry === "object" && entry !== null && "type" in entry
|
||||
}
|
||||
|
||||
async function descendants(pid: number): Promise<number[]> {
|
||||
if (process.platform === "win32") return []
|
||||
const pids: number[] = []
|
||||
const queue = [pid]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
const proc = Bun.spawn(["pgrep", "-P", String(current)], { stdout: "pipe", stderr: "pipe" })
|
||||
const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch(
|
||||
() => [-1, ""] as const,
|
||||
)
|
||||
if (code !== 0) continue
|
||||
for (const tok of out.trim().split(/\s+/)) {
|
||||
const cpid = parseInt(tok, 10)
|
||||
if (!isNaN(cpid) && pids.indexOf(cpid) === -1) {
|
||||
pids.push(cpid)
|
||||
queue.push(cpid)
|
||||
}
|
||||
}
|
||||
}
|
||||
return pids
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const cfg = await Config.get()
|
||||
@@ -218,21 +196,6 @@ export namespace MCP {
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
// The MCP SDK only signals the direct child process on close.
|
||||
// Servers like chrome-devtools-mcp spawn grandchild processes
|
||||
// (e.g. Chrome) that the SDK never reaches, leaving them orphaned.
|
||||
// Kill the full descendant tree first so the server exits promptly
|
||||
// and no processes are left behind.
|
||||
for (const client of Object.values(state.clients)) {
|
||||
const pid = (client.transport as any)?.pid
|
||||
if (typeof pid !== "number") continue
|
||||
for (const dpid of await descendants(pid)) {
|
||||
try {
|
||||
process.kill(dpid, "SIGTERM")
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.values(state.clients).map((client) =>
|
||||
client.close().catch((error) => {
|
||||
|
||||
@@ -76,18 +76,6 @@ export namespace ProviderError {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// If responseBody is HTML (e.g. from a gateway or proxy error page),
|
||||
// provide a human-readable message instead of dumping raw markup
|
||||
if (/^\s*<!doctype|^\s*<html/i.test(e.responseBody)) {
|
||||
if (e.statusCode === 401) {
|
||||
return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` to re-authenticate."
|
||||
}
|
||||
if (e.statusCode === 403) {
|
||||
return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings."
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
return `${msg}: ${e.responseBody}`
|
||||
}).trim()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Session } from "../../session"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { WorkspaceRoutes } from "./workspace"
|
||||
|
||||
export const ExperimentalRoutes = lazy(() =>
|
||||
new Hono()
|
||||
@@ -113,7 +112,6 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
return c.json(worktree)
|
||||
},
|
||||
)
|
||||
.route("/workspace", WorkspaceRoutes())
|
||||
.get(
|
||||
"/worktree",
|
||||
describeRoute({
|
||||
|
||||
@@ -16,13 +16,11 @@ import { Log } from "../../util/log"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { SessionProxyMiddleware } from "../../control-plane/session-proxy-middleware"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export const SessionRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.use(SessionProxyMiddleware)
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { Workspace } from "../../control-plane/workspace"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
|
||||
export const WorkspaceRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.post(
|
||||
"/:id",
|
||||
describeRoute({
|
||||
summary: "Create workspace",
|
||||
description: "Create a workspace for the current project.",
|
||||
operationId: "experimental.workspace.create",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Workspace created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Workspace.Info.shape.id,
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
branch: Workspace.Info.shape.branch,
|
||||
config: Workspace.Info.shape.config,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { id } = c.req.valid("param")
|
||||
const body = c.req.valid("json")
|
||||
const workspace = await Workspace.create({
|
||||
id,
|
||||
projectID: Instance.project.id,
|
||||
branch: body.branch,
|
||||
config: body.config,
|
||||
})
|
||||
return c.json(workspace)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
summary: "List workspaces",
|
||||
description: "List all workspaces.",
|
||||
operationId: "experimental.workspace.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Workspaces",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(Workspace.Info)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(Workspace.list(Instance.project))
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/:id",
|
||||
describeRoute({
|
||||
summary: "Remove workspace",
|
||||
description: "Remove an existing workspace.",
|
||||
operationId: "experimental.workspace.remove",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Workspace removed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info.optional()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Workspace.Info.shape.id,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { id } = c.req.valid("param")
|
||||
return c.json(await Workspace.remove(id))
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -21,7 +21,6 @@ import { Auth } from "../auth"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Command } from "../command"
|
||||
import { Global } from "../global"
|
||||
import { WorkspaceContext } from "../control-plane/workspace-context"
|
||||
import { ProjectRoutes } from "./routes/project"
|
||||
import { SessionRoutes } from "./routes/session"
|
||||
import { PtyRoutes } from "./routes/pty"
|
||||
@@ -195,7 +194,6 @@ export namespace Server {
|
||||
)
|
||||
.use(async (c, next) => {
|
||||
if (c.req.path === "/log") return next()
|
||||
const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
|
||||
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
||||
const directory = (() => {
|
||||
try {
|
||||
@@ -204,17 +202,11 @@ export namespace Server {
|
||||
return raw
|
||||
}
|
||||
})()
|
||||
|
||||
return WorkspaceContext.provide({
|
||||
workspaceID,
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
})
|
||||
return next()
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -231,15 +223,7 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
)
|
||||
.use(
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
directory: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.use(validator("query", z.object({ directory: z.string().optional() })))
|
||||
.route("/project", ProjectRoutes())
|
||||
.route("/pty", PtyRoutes())
|
||||
.route("/config", ConfigRoutes())
|
||||
|
||||
@@ -22,7 +22,6 @@ import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { WorkspaceContext } from "../control-plane/workspace-context"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
@@ -64,7 +63,6 @@ export namespace Session {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
projectID: row.project_id,
|
||||
workspaceID: row.workspace_id ?? undefined,
|
||||
directory: row.directory,
|
||||
parentID: row.parent_id ?? undefined,
|
||||
title: row.title,
|
||||
@@ -86,7 +84,6 @@ export namespace Session {
|
||||
return {
|
||||
id: info.id,
|
||||
project_id: info.projectID,
|
||||
workspace_id: info.workspaceID,
|
||||
parent_id: info.parentID,
|
||||
slug: info.slug,
|
||||
directory: info.directory,
|
||||
@@ -121,7 +118,6 @@ export namespace Session {
|
||||
id: Identifier.schema("session"),
|
||||
slug: z.string(),
|
||||
projectID: z.string(),
|
||||
workspaceID: z.string().optional(),
|
||||
directory: z.string(),
|
||||
parentID: Identifier.schema("session").optional(),
|
||||
summary: z
|
||||
@@ -301,7 +297,6 @@ export namespace Session {
|
||||
version: Installation.VERSION,
|
||||
projectID: Instance.project.id,
|
||||
directory: input.directory,
|
||||
workspaceID: WorkspaceContext.workspaceID,
|
||||
parentID: input.parentID,
|
||||
title: input.title ?? createDefaultTitle(!!input.parentID),
|
||||
permission: input.permission,
|
||||
@@ -532,7 +527,6 @@ export namespace Session {
|
||||
|
||||
export function* list(input?: {
|
||||
directory?: string
|
||||
workspaceID?: string
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
@@ -541,9 +535,6 @@ export namespace Session {
|
||||
const project = Instance.project
|
||||
const conditions = [eq(SessionTable.project_id, project.id)]
|
||||
|
||||
if (WorkspaceContext.workspaceID) {
|
||||
conditions.push(eq(SessionTable.workspace_id, WorkspaceContext.workspaceID))
|
||||
}
|
||||
if (input?.directory) {
|
||||
conditions.push(eq(SessionTable.directory, input.directory))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export const SessionTable = sqliteTable(
|
||||
project_id: text()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
workspace_id: text(),
|
||||
parent_id: text(),
|
||||
slug: text().notNull(),
|
||||
directory: text().notNull(),
|
||||
@@ -32,11 +31,7 @@ export const SessionTable = sqliteTable(
|
||||
time_compacting: integer(),
|
||||
time_archived: integer(),
|
||||
},
|
||||
(table) => [
|
||||
index("session_project_idx").on(table.project_id),
|
||||
index("session_workspace_idx").on(table.workspace_id),
|
||||
index("session_parent_idx").on(table.parent_id),
|
||||
],
|
||||
(table) => [index("session_project_idx").on(table.project_id), index("session_parent_idx").on(table.parent_id)],
|
||||
)
|
||||
|
||||
export const MessageTable = sqliteTable(
|
||||
|
||||
@@ -2,4 +2,3 @@ export { ControlAccountTable } from "../control/control.sql"
|
||||
export { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
|
||||
export { SessionShareTable } from "../share/share.sql"
|
||||
export { ProjectTable } from "../project/project.sql"
|
||||
export { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { Hono } from "hono"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Database } from "../../src/storage/db"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
type State = {
|
||||
workspace?: "first" | "second"
|
||||
calls: Array<{ method: string; url: string; body?: string }>
|
||||
}
|
||||
|
||||
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
|
||||
|
||||
async function setup(state: State) {
|
||||
mock.module("../../src/control-plane/adaptors", () => ({
|
||||
getAdaptor: () => ({
|
||||
request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
|
||||
const body = data ? await new Response(data).text() : undefined
|
||||
state.calls.push({ method, url, body })
|
||||
return new Response("proxied", { status: 202 })
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const { project } = await Project.fromDirectory(tmp.path)
|
||||
|
||||
const id1 = Identifier.descending("workspace")
|
||||
const id2 = Identifier.descending("workspace")
|
||||
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(WorkspaceTable)
|
||||
.values([
|
||||
{
|
||||
id: id1,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: remote,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: { type: "worktree", directory: tmp.path },
|
||||
},
|
||||
])
|
||||
.run(),
|
||||
)
|
||||
|
||||
const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
|
||||
const app = new Hono().use(SessionProxyMiddleware)
|
||||
|
||||
return {
|
||||
id1,
|
||||
id2,
|
||||
app,
|
||||
async request(input: RequestInfo | URL, init?: RequestInit) {
|
||||
return Instance.provide({
|
||||
directory: state.workspace === "first" ? id1 : id2,
|
||||
fn: async () => app.request(input, init),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("control-plane/session-proxy-middleware", () => {
|
||||
test("forwards non-GET session requests for remote workspaces", async () => {
|
||||
const state: State = {
|
||||
workspace: "first",
|
||||
calls: [],
|
||||
}
|
||||
|
||||
const ctx = await setup(state)
|
||||
|
||||
ctx.app.post("/session/foo", (c) => c.text("local", 200))
|
||||
const response = await ctx.request("http://workspace.test/session/foo?x=1", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ hello: "world" }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.status).toBe(202)
|
||||
expect(await response.text()).toBe("proxied")
|
||||
expect(state.calls).toEqual([
|
||||
{
|
||||
method: "POST",
|
||||
url: "/session/foo?x=1",
|
||||
body: '{"hello":"world"}',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("does not forward GET requests", async () => {
|
||||
const state: State = {
|
||||
workspace: "first",
|
||||
calls: [],
|
||||
}
|
||||
|
||||
const ctx = await setup(state)
|
||||
|
||||
ctx.app.get("/session/foo", (c) => c.text("local", 200))
|
||||
const response = await ctx.request("http://workspace.test/session/foo?x=1")
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.text()).toBe("local")
|
||||
expect(state.calls).toEqual([])
|
||||
})
|
||||
|
||||
test("does not forward GET or POST requests for worktree workspaces", async () => {
|
||||
const state: State = {
|
||||
workspace: "second",
|
||||
calls: [],
|
||||
}
|
||||
|
||||
const ctx = await setup(state)
|
||||
|
||||
ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
|
||||
ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
|
||||
|
||||
const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
|
||||
const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ hello: "world" }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
expect(getResponse.status).toBe(200)
|
||||
expect(await getResponse.text()).toBe("local-get")
|
||||
expect(postResponse.status).toBe(200)
|
||||
expect(await postResponse.text()).toBe("local-post")
|
||||
expect(state.calls).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { parseSSE } from "../../src/control-plane/sse"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
function stream(chunks: string[]) {
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
chunks.forEach((chunk) => controller.enqueue(encoder.encode(chunk)))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe("control-plane/sse", () => {
|
||||
test("parses JSON events with CRLF and multiline data blocks", async () => {
|
||||
const events: unknown[] = []
|
||||
const stop = new AbortController()
|
||||
|
||||
await parseSSE(
|
||||
stream([
|
||||
'data: {"type":"one","properties":{"ok":true}}\r\n\r\n',
|
||||
'data: {"type":"two",\r\ndata: "properties":{"n":2}}\r\n\r\n',
|
||||
]),
|
||||
stop.signal,
|
||||
(event) => events.push(event),
|
||||
)
|
||||
|
||||
expect(events).toEqual([
|
||||
{ type: "one", properties: { ok: true } },
|
||||
{ type: "two", properties: { n: 2 } },
|
||||
])
|
||||
})
|
||||
|
||||
test("falls back to sse.message for non-json payload", async () => {
|
||||
const events: unknown[] = []
|
||||
const stop = new AbortController()
|
||||
|
||||
await parseSSE(stream(["id: abc\nretry: 1500\ndata: hello world\n\n"]), stop.signal, (event) => events.push(event))
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "sse.message",
|
||||
properties: {
|
||||
data: "hello world",
|
||||
id: "abc",
|
||||
retry: 1500,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,65 +0,0 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { WorkspaceServer } from "../../src/control-plane/workspace-server/server"
|
||||
import { parseSSE } from "../../src/control-plane/sse"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
describe("control-plane/workspace-server SSE", () => {
|
||||
test("streams GlobalBus events and parseSSE reads them", async () => {
|
||||
const app = WorkspaceServer.App()
|
||||
const stop = new AbortController()
|
||||
const seen: unknown[] = []
|
||||
|
||||
try {
|
||||
const response = await app.request("/event", {
|
||||
signal: stop.signal,
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toBeDefined()
|
||||
|
||||
const done = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("timed out waiting for workspace.test event"))
|
||||
}, 3000)
|
||||
|
||||
void parseSSE(response.body!, stop.signal, (event) => {
|
||||
seen.push(event)
|
||||
const next = event as { type?: string }
|
||||
if (next.type === "server.connected") {
|
||||
GlobalBus.emit("event", {
|
||||
payload: {
|
||||
type: "workspace.test",
|
||||
properties: { ok: true },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (next.type !== "workspace.test") return
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
}).catch((error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
await done
|
||||
|
||||
expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true)
|
||||
expect(seen).toContainEqual({
|
||||
type: "workspace.test",
|
||||
properties: { ok: true },
|
||||
})
|
||||
} finally {
|
||||
stop.abort()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,97 +0,0 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { Database } from "../../src/storage/db"
|
||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const seen: string[] = []
|
||||
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
|
||||
|
||||
mock.module("../../src/control-plane/adaptors", () => ({
|
||||
getAdaptor: (config: { type: string }) => {
|
||||
seen.push(config.type)
|
||||
return {
|
||||
async create() {
|
||||
throw new Error("not used")
|
||||
},
|
||||
async remove() {},
|
||||
async request() {
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
describe("control-plane/workspace.startSyncing", () => {
|
||||
test("syncs only remote workspaces and emits remote SSE events", async () => {
|
||||
const { Workspace } = await import("../../src/control-plane/workspace")
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const { project } = await Project.fromDirectory(tmp.path)
|
||||
|
||||
const id1 = Identifier.descending("workspace")
|
||||
const id2 = Identifier.descending("workspace")
|
||||
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(WorkspaceTable)
|
||||
.values([
|
||||
{
|
||||
id: id1,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: remote,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: { type: "worktree", directory: tmp.path },
|
||||
},
|
||||
])
|
||||
.run(),
|
||||
)
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const listener = (event: { directory?: string; payload: { type: string } }) => {
|
||||
if (event.directory !== id1) return
|
||||
if (event.payload.type !== "remote.ready") return
|
||||
GlobalBus.off("event", listener)
|
||||
resolve()
|
||||
}
|
||||
GlobalBus.on("event", listener)
|
||||
})
|
||||
|
||||
const sync = Workspace.startSyncing(project)
|
||||
await Promise.race([
|
||||
done,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("timed out waiting for sync event")), 2000)),
|
||||
])
|
||||
|
||||
await sync.stop()
|
||||
expect(seen).toContain("testing")
|
||||
expect(seen).not.toContain("worktree")
|
||||
})
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
import { rm } from "fs/promises"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Database } from "../../src/storage/db"
|
||||
|
||||
export async function resetDatabase() {
|
||||
await Instance.disposeAll().catch(() => undefined)
|
||||
Database.close()
|
||||
await rm(Database.Path, { force: true }).catch(() => undefined)
|
||||
await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
|
||||
await rm(`${Database.Path}-shm`, { force: true }).catch(() => undefined)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -808,7 +808,6 @@ export type Session = {
|
||||
id: string
|
||||
slug: string
|
||||
projectID: string
|
||||
workspaceID?: string
|
||||
directory: string
|
||||
parentID?: string
|
||||
summary?: {
|
||||
@@ -888,35 +887,6 @@ export type EventVcsBranchUpdated = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeReady = {
|
||||
type: "worktree.ready"
|
||||
properties: {
|
||||
name: string
|
||||
branch: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeFailed = {
|
||||
type: "worktree.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceReady = {
|
||||
type: "workspace.ready"
|
||||
properties: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceFailed = {
|
||||
type: "workspace.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Pty = {
|
||||
id: string
|
||||
title: string
|
||||
@@ -956,6 +926,21 @@ export type EventPtyDeleted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeReady = {
|
||||
type: "worktree.ready"
|
||||
properties: {
|
||||
name: string
|
||||
branch: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeFailed = {
|
||||
type: "worktree.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
@@ -994,14 +979,12 @@ export type Event =
|
||||
| EventSessionDiff
|
||||
| EventSessionError
|
||||
| EventVcsBranchUpdated
|
||||
| EventWorktreeReady
|
||||
| EventWorktreeFailed
|
||||
| EventWorkspaceReady
|
||||
| EventWorkspaceFailed
|
||||
| EventPtyCreated
|
||||
| EventPtyUpdated
|
||||
| EventPtyExited
|
||||
| EventPtyDeleted
|
||||
| EventWorktreeReady
|
||||
| EventWorktreeFailed
|
||||
|
||||
export type GlobalEvent = {
|
||||
directory: string
|
||||
@@ -1644,16 +1627,6 @@ export type WorktreeCreateInput = {
|
||||
startCommand?: string
|
||||
}
|
||||
|
||||
export type Workspace = {
|
||||
id: string
|
||||
branch: string | null
|
||||
projectID: string
|
||||
config: {
|
||||
directory: string
|
||||
type: "worktree"
|
||||
}
|
||||
}
|
||||
|
||||
export type WorktreeRemoveInput = {
|
||||
directory: string
|
||||
}
|
||||
@@ -1672,7 +1645,6 @@ export type GlobalSession = {
|
||||
id: string
|
||||
slug: string
|
||||
projectID: string
|
||||
workspaceID?: string
|
||||
directory: string
|
||||
parentID?: string
|
||||
summary?: {
|
||||
@@ -2053,7 +2025,6 @@ export type ProjectListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project"
|
||||
}
|
||||
@@ -2072,7 +2043,6 @@ export type ProjectCurrentData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/current"
|
||||
}
|
||||
@@ -2106,7 +2076,6 @@ export type ProjectUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/{projectID}"
|
||||
}
|
||||
@@ -2138,7 +2107,6 @@ export type PtyListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty"
|
||||
}
|
||||
@@ -2165,7 +2133,6 @@ export type PtyCreateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty"
|
||||
}
|
||||
@@ -2195,7 +2162,6 @@ export type PtyRemoveData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}"
|
||||
}
|
||||
@@ -2225,7 +2191,6 @@ export type PtyGetData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}"
|
||||
}
|
||||
@@ -2261,7 +2226,6 @@ export type PtyUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}"
|
||||
}
|
||||
@@ -2291,7 +2255,6 @@ export type PtyConnectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}/connect"
|
||||
}
|
||||
@@ -2319,7 +2282,6 @@ export type ConfigGetData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/config"
|
||||
}
|
||||
@@ -2338,7 +2300,6 @@ export type ConfigUpdateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/config"
|
||||
}
|
||||
@@ -2366,7 +2327,6 @@ export type ConfigProvidersData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/config/providers"
|
||||
}
|
||||
@@ -2390,7 +2350,6 @@ export type ToolIdsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/tool/ids"
|
||||
}
|
||||
@@ -2418,7 +2377,6 @@ export type ToolListData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
@@ -2448,7 +2406,6 @@ export type WorktreeRemoveData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree"
|
||||
}
|
||||
@@ -2476,7 +2433,6 @@ export type WorktreeListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree"
|
||||
}
|
||||
@@ -2495,7 +2451,6 @@ export type WorktreeCreateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree"
|
||||
}
|
||||
@@ -2518,102 +2473,11 @@ export type WorktreeCreateResponses = {
|
||||
|
||||
export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
|
||||
|
||||
export type ExperimentalWorkspaceRemoveData = {
|
||||
body?: never
|
||||
path: {
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/workspace/{id}"
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceRemoveErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceRemoveError =
|
||||
ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors]
|
||||
|
||||
export type ExperimentalWorkspaceRemoveResponses = {
|
||||
/**
|
||||
* Workspace removed
|
||||
*/
|
||||
200: Workspace
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceRemoveResponse =
|
||||
ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses]
|
||||
|
||||
export type ExperimentalWorkspaceCreateData = {
|
||||
body?: {
|
||||
branch: string | null
|
||||
config: {
|
||||
directory: string
|
||||
type: "worktree"
|
||||
}
|
||||
}
|
||||
path: {
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/workspace/{id}"
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceCreateErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceCreateError =
|
||||
ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
|
||||
|
||||
export type ExperimentalWorkspaceCreateResponses = {
|
||||
/**
|
||||
* Workspace created
|
||||
*/
|
||||
200: Workspace
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceCreateResponse =
|
||||
ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
|
||||
|
||||
export type ExperimentalWorkspaceListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/workspace"
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceListResponses = {
|
||||
/**
|
||||
* Workspaces
|
||||
*/
|
||||
200: Array<Workspace>
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceListResponse =
|
||||
ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
|
||||
|
||||
export type WorktreeResetData = {
|
||||
body?: WorktreeResetInput
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree/reset"
|
||||
}
|
||||
@@ -2644,7 +2508,6 @@ export type ExperimentalSessionListData = {
|
||||
* Filter sessions by project directory
|
||||
*/
|
||||
directory?: string
|
||||
workspace?: string
|
||||
/**
|
||||
* Only return root sessions (no parentID)
|
||||
*/
|
||||
@@ -2687,7 +2550,6 @@ export type ExperimentalResourceListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/resource"
|
||||
}
|
||||
@@ -2712,7 +2574,6 @@ export type SessionListData = {
|
||||
* Filter sessions by project directory
|
||||
*/
|
||||
directory?: string
|
||||
workspace?: string
|
||||
/**
|
||||
* Only return root sessions (no parentID)
|
||||
*/
|
||||
@@ -2751,7 +2612,6 @@ export type SessionCreateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session"
|
||||
}
|
||||
@@ -2779,7 +2639,6 @@ export type SessionStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/status"
|
||||
}
|
||||
@@ -2811,7 +2670,6 @@ export type SessionDeleteData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}"
|
||||
}
|
||||
@@ -2845,7 +2703,6 @@ export type SessionGetData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}"
|
||||
}
|
||||
@@ -2884,7 +2741,6 @@ export type SessionUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}"
|
||||
}
|
||||
@@ -2918,7 +2774,6 @@ export type SessionChildrenData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/children"
|
||||
}
|
||||
@@ -2955,7 +2810,6 @@ export type SessionTodoData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/todo"
|
||||
}
|
||||
@@ -2996,7 +2850,6 @@ export type SessionInitData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/init"
|
||||
}
|
||||
@@ -3032,7 +2885,6 @@ export type SessionForkData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/fork"
|
||||
}
|
||||
@@ -3053,7 +2905,6 @@ export type SessionAbortData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/abort"
|
||||
}
|
||||
@@ -3087,7 +2938,6 @@ export type SessionUnshareData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/share"
|
||||
}
|
||||
@@ -3121,7 +2971,6 @@ export type SessionShareData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/share"
|
||||
}
|
||||
@@ -3155,7 +3004,6 @@ export type SessionDiffData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
messageID?: string
|
||||
}
|
||||
url: "/session/{sessionID}/diff"
|
||||
@@ -3184,7 +3032,6 @@ export type SessionSummarizeData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/summarize"
|
||||
}
|
||||
@@ -3221,7 +3068,6 @@ export type SessionMessagesData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
limit?: number
|
||||
}
|
||||
url: "/session/{sessionID}/message"
|
||||
@@ -3280,7 +3126,6 @@ export type SessionPromptData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message"
|
||||
}
|
||||
@@ -3324,7 +3169,6 @@ export type SessionDeleteMessageData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}"
|
||||
}
|
||||
@@ -3365,7 +3209,6 @@ export type SessionMessageData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}"
|
||||
}
|
||||
@@ -3413,7 +3256,6 @@ export type PartDeleteData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}/part/{partID}"
|
||||
}
|
||||
@@ -3458,7 +3300,6 @@ export type PartUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}/part/{partID}"
|
||||
}
|
||||
@@ -3513,7 +3354,6 @@ export type SessionPromptAsyncData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/prompt_async"
|
||||
}
|
||||
@@ -3565,7 +3405,6 @@ export type SessionCommandData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/command"
|
||||
}
|
||||
@@ -3612,7 +3451,6 @@ export type SessionShellData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/shell"
|
||||
}
|
||||
@@ -3649,7 +3487,6 @@ export type SessionRevertData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/revert"
|
||||
}
|
||||
@@ -3683,7 +3520,6 @@ export type SessionUnrevertData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/unrevert"
|
||||
}
|
||||
@@ -3720,7 +3556,6 @@ export type PermissionRespondData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/permissions/{permissionID}"
|
||||
}
|
||||
@@ -3757,7 +3592,6 @@ export type PermissionReplyData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/permission/{requestID}/reply"
|
||||
}
|
||||
@@ -3789,7 +3623,6 @@ export type PermissionListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/permission"
|
||||
}
|
||||
@@ -3808,7 +3641,6 @@ export type QuestionListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/question"
|
||||
}
|
||||
@@ -3834,7 +3666,6 @@ export type QuestionReplyData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/question/{requestID}/reply"
|
||||
}
|
||||
@@ -3868,7 +3699,6 @@ export type QuestionRejectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/question/{requestID}/reject"
|
||||
}
|
||||
@@ -3900,7 +3730,6 @@ export type ProviderListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider"
|
||||
}
|
||||
@@ -3986,7 +3815,6 @@ export type ProviderAuthData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider/auth"
|
||||
}
|
||||
@@ -4017,7 +3845,6 @@ export type ProviderOauthAuthorizeData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider/{providerID}/oauth/authorize"
|
||||
}
|
||||
@@ -4059,7 +3886,6 @@ export type ProviderOauthCallbackData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider/{providerID}/oauth/callback"
|
||||
}
|
||||
@@ -4087,7 +3913,6 @@ export type FindTextData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
pattern: string
|
||||
}
|
||||
url: "/find"
|
||||
@@ -4123,7 +3948,6 @@ export type FindFilesData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
query: string
|
||||
dirs?: "true" | "false"
|
||||
type?: "file" | "directory"
|
||||
@@ -4146,7 +3970,6 @@ export type FindSymbolsData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
query: string
|
||||
}
|
||||
url: "/find/symbol"
|
||||
@@ -4166,7 +3989,6 @@ export type FileListData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
path: string
|
||||
}
|
||||
url: "/file"
|
||||
@@ -4186,7 +4008,6 @@ export type FileReadData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
path: string
|
||||
}
|
||||
url: "/file/content"
|
||||
@@ -4206,7 +4027,6 @@ export type FileStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/file/status"
|
||||
}
|
||||
@@ -4225,7 +4045,6 @@ export type McpStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp"
|
||||
}
|
||||
@@ -4249,7 +4068,6 @@ export type McpAddData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp"
|
||||
}
|
||||
@@ -4281,7 +4099,6 @@ export type McpAuthRemoveData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth"
|
||||
}
|
||||
@@ -4313,7 +4130,6 @@ export type McpAuthStartData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth"
|
||||
}
|
||||
@@ -4357,7 +4173,6 @@ export type McpAuthCallbackData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth/callback"
|
||||
}
|
||||
@@ -4391,7 +4206,6 @@ export type McpAuthAuthenticateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth/authenticate"
|
||||
}
|
||||
@@ -4425,7 +4239,6 @@ export type McpConnectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/connect"
|
||||
}
|
||||
@@ -4446,7 +4259,6 @@ export type McpDisconnectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/disconnect"
|
||||
}
|
||||
@@ -4467,7 +4279,6 @@ export type TuiAppendPromptData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/append-prompt"
|
||||
}
|
||||
@@ -4495,7 +4306,6 @@ export type TuiOpenHelpData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-help"
|
||||
}
|
||||
@@ -4514,7 +4324,6 @@ export type TuiOpenSessionsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-sessions"
|
||||
}
|
||||
@@ -4533,7 +4342,6 @@ export type TuiOpenThemesData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-themes"
|
||||
}
|
||||
@@ -4552,7 +4360,6 @@ export type TuiOpenModelsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-models"
|
||||
}
|
||||
@@ -4571,7 +4378,6 @@ export type TuiSubmitPromptData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/submit-prompt"
|
||||
}
|
||||
@@ -4590,7 +4396,6 @@ export type TuiClearPromptData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/clear-prompt"
|
||||
}
|
||||
@@ -4611,7 +4416,6 @@ export type TuiExecuteCommandData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/execute-command"
|
||||
}
|
||||
@@ -4647,7 +4451,6 @@ export type TuiShowToastData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/show-toast"
|
||||
}
|
||||
@@ -4666,7 +4469,6 @@ export type TuiPublishData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/publish"
|
||||
}
|
||||
@@ -4699,7 +4501,6 @@ export type TuiSelectSessionData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/select-session"
|
||||
}
|
||||
@@ -4731,7 +4532,6 @@ export type TuiControlNextData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/control/next"
|
||||
}
|
||||
@@ -4753,7 +4553,6 @@ export type TuiControlResponseData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/control/response"
|
||||
}
|
||||
@@ -4772,7 +4571,6 @@ export type InstanceDisposeData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/instance/dispose"
|
||||
}
|
||||
@@ -4791,7 +4589,6 @@ export type PathGetData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/path"
|
||||
}
|
||||
@@ -4810,7 +4607,6 @@ export type VcsGetData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/vcs"
|
||||
}
|
||||
@@ -4829,7 +4625,6 @@ export type CommandListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/command"
|
||||
}
|
||||
@@ -4867,7 +4662,6 @@ export type AppLogData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/log"
|
||||
}
|
||||
@@ -4895,7 +4689,6 @@ export type AppAgentsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/agent"
|
||||
}
|
||||
@@ -4914,7 +4707,6 @@ export type AppSkillsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/skill"
|
||||
}
|
||||
@@ -4938,7 +4730,6 @@ export type LspStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/lsp"
|
||||
}
|
||||
@@ -4957,7 +4748,6 @@ export type FormatterStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/formatter"
|
||||
}
|
||||
@@ -4976,7 +4766,6 @@ export type EventSubscribeData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/event"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user