mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8e5b12f41 | |||
| c45d1db9a0 | |||
| ba2ba770d5 | |||
| 5ca19947d9 | |||
| 3b4aaafd41 | |||
| 56a37c3640 | |||
| f9dac262ff | |||
| 3730125f8f | |||
| 9bb5370205 | |||
| 25c6abc31a | |||
| f4afb2c0a5 | |||
| bba74985ab | |||
| e9ee2129ea | |||
| 68260ea6c9 | |||
| 49ea8a9455 | |||
| 142c5c11d9 | |||
| 4b83f5d8f0 | |||
| 121decfd15 | |||
| 24b0132bc5 | |||
| 858f35f1b3 | |||
| 20750a3295 | |||
| 1ddb75dd3e | |||
| ea5dd9d940 | |||
| 31b58b4704 | |||
| 42bb793574 | |||
| 17f312d537 | |||
| 116ac93ddb | |||
| 2684293291 | |||
| e8e83afbce | |||
| 2e909334c1 | |||
| 69f3b633b8 | |||
| db7c28b3ee | |||
| e45f55d922 | |||
| 04c1730c90 | |||
| 1ef0fd5d01 | |||
| 36501a81c3 |
@@ -325,6 +325,7 @@ jobs:
|
|||||||
run: bun run build
|
run: bun run build
|
||||||
working-directory: packages/desktop
|
working-directory: packages/desktop
|
||||||
env:
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ on:
|
|||||||
- "bun.lock"
|
- "bun.lock"
|
||||||
- "packages/storybook/**"
|
- "packages/storybook/**"
|
||||||
- "packages/ui/**"
|
- "packages/ui/**"
|
||||||
|
- "packages/session-ui/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [dev]
|
branches: [dev]
|
||||||
paths:
|
paths:
|
||||||
@@ -17,6 +18,7 @@ on:
|
|||||||
- "bun.lock"
|
- "bun.lock"
|
||||||
- "packages/storybook/**"
|
- "packages/storybook/**"
|
||||||
- "packages/ui/**"
|
- "packages/ui/**"
|
||||||
|
- "packages/session-ui/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributi
|
|||||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||||
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||||
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
||||||
|
- In Effect generators, bind services to named variables before calling methods. Do not use nested service yields such as `yield* (yield* Foo.Service).bar()`.
|
||||||
|
|
||||||
Reduce total variable count by inlining when a value is only used once.
|
Reduce total variable count by inlining when a value is only used once.
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,6 @@ The bounded projection of a Core-executed tool result persisted in Session histo
|
|||||||
**Managed Tool Output File**:
|
**Managed Tool Output File**:
|
||||||
A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history.
|
A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history.
|
||||||
|
|
||||||
**Model Request Options**:
|
|
||||||
Provider-semantic model settings selected from the Catalog and active Session variant before the LLM protocol adapter encodes them for a provider request.
|
|
||||||
_Avoid_: Request body, wire options
|
|
||||||
|
|
||||||
**Generation Controls**:
|
|
||||||
Provider-neutral sampling and output controls, partitioned from provider semantics and compatibility wire fields when model metadata enters the Catalog.
|
|
||||||
|
|
||||||
**PTY Environment**:
|
**PTY Environment**:
|
||||||
The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory.
|
The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory.
|
||||||
|
|
||||||
@@ -114,8 +107,6 @@ The host-supplied environment overlay applied by the server when creating a PTY,
|
|||||||
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
|
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
|
||||||
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
|
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
|
||||||
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
|
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
|
||||||
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
|
|
||||||
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
|
|
||||||
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
|
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
|
||||||
- A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter.
|
- A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter.
|
||||||
- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
|
- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
|
||||||
|
|||||||
@@ -29,11 +29,12 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
"@opencode-ai/session-ui": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@pierre/trees": "1.0.0-beta.4",
|
"@pierre/trees": "1.0.0-beta.4",
|
||||||
"@sentry/solid": "catalog:",
|
"@sentry/solid": "catalog:",
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs",
|
"lildax": "./bin/lildax.cjs",
|
||||||
},
|
},
|
||||||
@@ -111,7 +112,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -174,7 +175,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -196,7 +197,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -220,7 +221,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -240,7 +241,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -333,7 +334,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -387,7 +388,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -401,7 +402,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -413,10 +414,11 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/session-ui": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
@@ -444,7 +446,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -460,7 +462,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.83",
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
@@ -479,7 +481,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
@@ -498,7 +500,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -537,6 +539,8 @@
|
|||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
@@ -626,7 +630,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -653,6 +657,18 @@
|
|||||||
"@opentui/solid",
|
"@opentui/solid",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"packages/protocol": {
|
||||||
|
"name": "@opencode-ai/protocol",
|
||||||
|
"dependencies": {
|
||||||
|
"@opencode-ai/schema": "workspace:*",
|
||||||
|
"effect": "catalog:",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/schema": {
|
"packages/schema": {
|
||||||
"name": "@opencode-ai/schema",
|
"name": "@opencode-ai/schema",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -676,7 +692,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -691,9 +707,10 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -703,9 +720,53 @@
|
|||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/session-ui": {
|
||||||
|
"name": "@opencode-ai/session-ui",
|
||||||
|
"version": "1.17.10",
|
||||||
|
"dependencies": {
|
||||||
|
"@kobalte/core": "catalog:",
|
||||||
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@pierre/diffs": "catalog:",
|
||||||
|
"@shikijs/stream": "catalog:",
|
||||||
|
"@shikijs/transformers": "3.9.2",
|
||||||
|
"@solid-primitives/bounds": "0.1.3",
|
||||||
|
"@solid-primitives/event-listener": "2.4.5",
|
||||||
|
"@solid-primitives/media": "2.3.3",
|
||||||
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
|
"@solidjs/meta": "catalog:",
|
||||||
|
"@solidjs/router": "catalog:",
|
||||||
|
"diff": "catalog:",
|
||||||
|
"dompurify": "3.3.1",
|
||||||
|
"fuzzysort": "catalog:",
|
||||||
|
"katex": "0.16.27",
|
||||||
|
"luxon": "catalog:",
|
||||||
|
"marked": "catalog:",
|
||||||
|
"marked-katex-extension": "5.1.6",
|
||||||
|
"marked-shiki": "catalog:",
|
||||||
|
"morphdom": "2.7.8",
|
||||||
|
"motion": "12.34.5",
|
||||||
|
"remeda": "catalog:",
|
||||||
|
"remend": "catalog:",
|
||||||
|
"shiki": "catalog:",
|
||||||
|
"solid-js": "catalog:",
|
||||||
|
"solid-list": "catalog:",
|
||||||
|
"strip-ansi": "7.1.2",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node22": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@types/katex": "0.16.7",
|
||||||
|
"@types/luxon": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"vite": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -718,7 +779,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
"@opencode-ai/stats-core": "workspace:*",
|
||||||
@@ -751,7 +812,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
@@ -770,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -789,6 +850,7 @@
|
|||||||
"packages/storybook": {
|
"packages/storybook": {
|
||||||
"name": "@opencode-ai/storybook",
|
"name": "@opencode-ai/storybook",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@opencode-ai/session-ui": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@storybook/addon-a11y": "^10.2.13",
|
"@storybook/addon-a11y": "^10.2.13",
|
||||||
@@ -810,7 +872,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
@@ -837,11 +899,9 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@shikijs/stream": "catalog:",
|
"@shikijs/stream": "catalog:",
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
@@ -886,7 +946,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1817,6 +1877,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||||
|
|
||||||
|
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
||||||
|
|
||||||
"@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"],
|
"@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"],
|
||||||
|
|
||||||
"@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"],
|
"@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"],
|
||||||
@@ -1825,6 +1887,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
|
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
|
||||||
|
|
||||||
|
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
|
||||||
|
|
||||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||||
|
|
||||||
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
|
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
|
||||||
@@ -5843,6 +5907,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
"@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||||
|
|
||||||
|
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
||||||
|
|
||||||
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
|
|||||||
+2
-1
@@ -42,11 +42,12 @@ const inferenceEventTable = new aws.s3tables.Table(
|
|||||||
{ name: "request", type: "string", required: false },
|
{ name: "request", type: "string", required: false },
|
||||||
{ name: "client", type: "string", required: false },
|
{ name: "client", type: "string", required: false },
|
||||||
{ name: "user_agent", type: "string", required: false },
|
{ name: "user_agent", type: "string", required: false },
|
||||||
|
{ name: "model", type: "string", required: false },
|
||||||
|
{ name: "model_tier", type: "string", required: false },
|
||||||
{ name: "model_variant", type: "string", required: false },
|
{ name: "model_variant", type: "string", required: false },
|
||||||
{ name: "source", type: "string", required: false },
|
{ name: "source", type: "string", required: false },
|
||||||
{ name: "provider", type: "string", required: false },
|
{ name: "provider", type: "string", required: false },
|
||||||
{ name: "provider_model", type: "string", required: false },
|
{ name: "provider_model", type: "string", required: false },
|
||||||
{ name: "model", type: "string", required: false },
|
|
||||||
{ name: "llm_error_code", type: "int", required: false },
|
{ name: "llm_error_code", type: "int", required: false },
|
||||||
{ name: "llm_error_message", type: "string", required: false },
|
{ name: "llm_error_message", type: "string", required: false },
|
||||||
{ name: "error_response", type: "string", required: false },
|
{ name: "error_response", type: "string", required: false },
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-JJnJVqP2+NiiRVoTKXjGD09lPRyfz8YMfx0m3jBaUzU=",
|
"x86_64-linux": "sha256-Q0CW2wMhVtRM3uILV1kvKAzpQJqBxTp4MLsGlsSoT1s=",
|
||||||
"aarch64-linux": "sha256-tY3/IA+iZbOQQGxrcHMvQ1xqByskZyVI+4LZkU0wVjs=",
|
"aarch64-linux": "sha256-UHpoDqqV+umG47LENB7DLxbCqvwjTCDjHWIFiv6Vmk8=",
|
||||||
"aarch64-darwin": "sha256-9qMRrQKKOgQuOcAgAq8oZacVFD7G4nbAfZawmCRuJdY=",
|
"aarch64-darwin": "sha256-rqlFositEiB1wzx6Jcp5yVQlp8LLYBW+79KXmRpXh2Q=",
|
||||||
"x86_64-darwin": "sha256-dR2VGxDLoAPEmk8SsEF4dhlPBcb9ubztAuVfwxuAD4M="
|
"x86_64-darwin": "sha256-BKr4YQio3eP0VLZ0z+oyaM93H8dAbSwmlzxT8PH6W8M="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ export async function setupTimelineBenchmark(page: Page, options: { historyTurns
|
|||||||
editToolPartsExpanded: true,
|
editToolPartsExpanded: true,
|
||||||
shellToolPartsExpanded: true,
|
shellToolPartsExpanded: true,
|
||||||
showReasoningSummaries: true,
|
showReasoningSummaries: true,
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export async function installTimelineSettings(page: Page) {
|
|||||||
editToolPartsExpanded: true,
|
editToolPartsExpanded: true,
|
||||||
shellToolPartsExpanded: true,
|
shellToolPartsExpanded: true,
|
||||||
showReasoningSummaries: true,
|
showReasoningSummaries: true,
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ async function configurePage(page: Page) {
|
|||||||
editToolPartsExpanded: true,
|
editToolPartsExpanded: true,
|
||||||
shellToolPartsExpanded: true,
|
shellToolPartsExpanded: true,
|
||||||
showReasoningSummaries: true,
|
showReasoningSummaries: true,
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ async function configurePage(page: Page) {
|
|||||||
editToolPartsExpanded: true,
|
editToolPartsExpanded: true,
|
||||||
shellToolPartsExpanded: true,
|
shellToolPartsExpanded: true,
|
||||||
showReasoningSummaries: true,
|
showReasoningSummaries: true,
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -362,7 +362,6 @@ async function configureSmokePage(page: Page, directory: string) {
|
|||||||
editToolPartsExpanded: true,
|
editToolPartsExpanded: true,
|
||||||
shellToolPartsExpanded: true,
|
shellToolPartsExpanded: true,
|
||||||
showReasoningSummaries: true,
|
showReasoningSummaries: true,
|
||||||
showSessionProgressBar: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
"@opencode-ai/session-ui": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@pierre/trees": "1.0.0-beta.4",
|
"@pierre/trees": "1.0.0-beta.4",
|
||||||
"@sentry/solid": "catalog:",
|
"@sentry/solid": "catalog:",
|
||||||
|
|||||||
+14
-25
@@ -4,7 +4,7 @@ import { I18nProvider } from "@opencode-ai/ui/context"
|
|||||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||||
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
||||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||||
import { File } from "@opencode-ai/ui/file"
|
import { File } from "@opencode-ai/session-ui/file"
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||||
@@ -31,8 +31,8 @@ import { CommandProvider } from "@/context/command"
|
|||||||
import { CommentsProvider } from "@/context/comments"
|
import { CommentsProvider } from "@/context/comments"
|
||||||
import { FileProvider } from "@/context/file"
|
import { FileProvider } from "@/context/file"
|
||||||
import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
|
import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
|
||||||
import { ServerSyncProvider } from "@/context/server-sync"
|
import { ServerSyncProvider, useServerSync } from "@/context/server-sync"
|
||||||
import { GlobalProvider, useGlobal } from "@/context/global"
|
import { GlobalProvider } from "@/context/global"
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { LayoutProvider } from "@/context/layout"
|
||||||
@@ -51,7 +51,7 @@ import LegacyLayout from "@/pages/layout"
|
|||||||
import NewLayout from "@/pages/layout-new"
|
import NewLayout from "@/pages/layout-new"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { useCheckServerHealth } from "./utils/server-health"
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
import { legacySessionHref, requireServerKey, rootSession, sessionHref } from "./utils/session-route"
|
import { legacySessionHref, requireServerKey, sessionHref } from "./utils/session-route"
|
||||||
|
|
||||||
import Session from "@/pages/session"
|
import Session from "@/pages/session"
|
||||||
import { NewHome, LegacyHome } from "@/pages/home"
|
import { NewHome, LegacyHome } from "@/pages/home"
|
||||||
@@ -109,37 +109,26 @@ function ResolvedTargetSessionRoute() {
|
|||||||
const params = useParams<{ serverKey: string; id: string }>()
|
const params = useParams<{ serverKey: string; id: string }>()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const global = useGlobal()
|
const sync = useServerSync()
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const serverKey = createMemo(() => requireServerKey(params.serverKey))
|
const serverKey = createMemo(() => requireServerKey(params.serverKey))
|
||||||
const placement = createMemo(() => global.sessionPlacement.get(serverKey(), params.id))
|
const cached = createMemo(() => sync().session.lineage.peek(params.id))
|
||||||
const [resolved] = createResource(
|
const [resolved] = createResource(
|
||||||
() => {
|
() => {
|
||||||
if (placement()) return
|
if (cached()) return
|
||||||
return { id: params.id, sdk: serverSDK() }
|
return { id: params.id, sync: sync() }
|
||||||
},
|
|
||||||
async ({ id, sdk }) => {
|
|
||||||
const session = (await sdk.client.session.get({ sessionID: id })).data!
|
|
||||||
const root = await rootSession(session, (sessionID) =>
|
|
||||||
sdk.client.session.get({ sessionID }).then((result) => result.data!),
|
|
||||||
)
|
|
||||||
return global.sessionPlacement.set({
|
|
||||||
server: serverKey(),
|
|
||||||
leafID: session.id,
|
|
||||||
rootID: root.id,
|
|
||||||
directory: session.directory,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
({ id, sync }) => sync.session.lineage.resolve(id),
|
||||||
)
|
)
|
||||||
const directory = createMemo(() => placement()?.directory ?? resolved()?.directory)
|
const current = createMemo(() => cached() ?? resolved())
|
||||||
|
const directory = createMemo(() => current()?.session.directory)
|
||||||
const targetDirectory = () => directory()!
|
const targetDirectory = () => directory()!
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const current = placement() ?? resolved()
|
const session = current()
|
||||||
if (!current) return
|
if (!session) return
|
||||||
tabs.addSessionTab({
|
tabs.addSessionTab({
|
||||||
server: serverKey(),
|
server: serverKey(),
|
||||||
sessionId: current.rootID,
|
sessionId: session.root.id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,10 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
mock.module("@/context/server-sync", () => ({
|
mock.module("@/context/server-sync", () => ({
|
||||||
useServerSync: () => () => ({
|
useServerSync: () => () => ({
|
||||||
|
session: {
|
||||||
|
remember: () => undefined,
|
||||||
|
set: () => undefined,
|
||||||
|
},
|
||||||
child: (directory: string) => {
|
child: (directory: string) => {
|
||||||
syncedDirectories.push(directory)
|
syncedDirectories.push(directory)
|
||||||
storedSessions[directory] ??= []
|
storedSessions[directory] ??= []
|
||||||
|
|||||||
@@ -56,16 +56,14 @@ const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttac
|
|||||||
export async function sendFollowupDraft(input: FollowupSendInput) {
|
export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||||
const text = draftText(input.draft.prompt)
|
const text = draftText(input.draft.prompt)
|
||||||
const images = draftImages(input.draft.prompt)
|
const images = draftImages(input.draft.prompt)
|
||||||
const [, setStore] = input.serverSync.child(input.draft.sessionDirectory)
|
|
||||||
|
|
||||||
const setBusy = () => {
|
const setBusy = () => {
|
||||||
if (!input.optimisticBusy) return
|
if (!input.optimisticBusy) return
|
||||||
setStore("session_status", input.draft.sessionID, { type: "busy" })
|
input.serverSync.session.set("session_status", input.draft.sessionID, { type: "busy" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const setIdle = () => {
|
const setIdle = () => {
|
||||||
if (!input.optimisticBusy) return
|
if (!input.optimisticBusy) return
|
||||||
setStore("session_status", input.draft.sessionID, { type: "idle" })
|
input.serverSync.session.set("session_status", input.draft.sessionID, { type: "idle" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const wait = async () => {
|
const wait = async () => {
|
||||||
@@ -234,9 +232,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return Promise.resolve()
|
if (!sessionID) return Promise.resolve()
|
||||||
|
|
||||||
serverSync().todo.set(sessionID, [])
|
serverSync().session.set("todo", sessionID, [])
|
||||||
const [, setStore] = serverSync().child(sdk().directory)
|
|
||||||
setStore("todo", sessionID, [])
|
|
||||||
|
|
||||||
input.onAbort?.()
|
input.onAbort?.()
|
||||||
|
|
||||||
@@ -282,6 +278,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seed = (dir: string, info: Session) => {
|
const seed = (dir: string, info: Session) => {
|
||||||
|
serverSync().session.remember(info)
|
||||||
const [, setStore] = serverSync().child(dir)
|
const [, setStore] = serverSync().child(dir)
|
||||||
setStore("session", (list: Session[]) => {
|
setStore("session", (list: Session[]) => {
|
||||||
const result = Binary.search(list, info.id, (item) => item.id)
|
const result = Binary.search(list, info.id, (item) => item.id)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { same } from "@/utils/same"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||||
import { File } from "@opencode-ai/ui/file"
|
import { File } from "@opencode-ai/session-ui/file"
|
||||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
import { Markdown } from "@opencode-ai/session-ui/markdown"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|||||||
@@ -335,18 +335,6 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.showSessionProgressBar.title")}
|
|
||||||
description={language.t("settings.general.row.showSessionProgressBar.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-session-progress-bar">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showSessionProgressBar()}
|
|
||||||
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.newLayoutDesigns.title")}
|
title={language.t("settings.general.row.newLayoutDesigns.title")}
|
||||||
description={language.t("settings.general.row.newLayoutDesigns.description")}
|
description={language.t("settings.general.row.newLayoutDesigns.description")}
|
||||||
|
|||||||
@@ -318,18 +318,6 @@ export const SettingsGeneralV2: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SettingsRowV2>
|
</SettingsRowV2>
|
||||||
|
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.general.row.showSessionProgressBar.title")}
|
|
||||||
description={language.t("settings.general.row.showSessionProgressBar.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-show-session-progress-bar">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showSessionProgressBar()}
|
|
||||||
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRowV2>
|
|
||||||
|
|
||||||
<SettingsRowV2
|
<SettingsRowV2
|
||||||
title={language.t("settings.general.row.newLayoutDesigns.title")}
|
title={language.t("settings.general.row.newLayoutDesigns.title")}
|
||||||
description={language.t("settings.general.row.newLayoutDesigns.description")}
|
description={language.t("settings.general.row.newLayoutDesigns.description")}
|
||||||
|
|||||||
@@ -512,38 +512,15 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
return conn ? global.ensureServerCtx(conn) : undefined
|
return conn ? global.ensureServerCtx(conn) : undefined
|
||||||
})
|
})
|
||||||
const sdk = createMemo(() => serverCtx()?.sdk ?? null)
|
const sdk = createMemo(() => serverCtx()?.sdk ?? null)
|
||||||
const cachedSession = createMemo(() => {
|
const cachedSession = createMemo(() => serverCtx()?.sync.session.peek(tab.sessionId))
|
||||||
const placement = global.sessionPlacement.get(tab.server, tab.sessionId)
|
|
||||||
const ctx = serverCtx()
|
|
||||||
if (!placement || !ctx) return
|
|
||||||
return ctx.sync
|
|
||||||
.child(placement.directory, { bootstrap: false })[0]
|
|
||||||
.session.find((session) => session.id === tab.sessionId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [loadedSession] = createResource(
|
const [loadedSession] = createResource(
|
||||||
() => {
|
() => {
|
||||||
if (cachedSession()) return null
|
|
||||||
const id = tab.sessionId
|
const id = tab.sessionId
|
||||||
const ctx = serverCtx()
|
const ctx = serverCtx()
|
||||||
return ctx ? { id, ctx } : null
|
return ctx ? { id, ctx } : null
|
||||||
},
|
},
|
||||||
({ id, ctx }) =>
|
({ id, ctx }) => ctx.sync.session.resolve(id).catch(() => undefined),
|
||||||
ctx.sdk.client.session
|
|
||||||
.get({ sessionID: id })
|
|
||||||
.then((x) => {
|
|
||||||
const session = x.data
|
|
||||||
if (!session) return
|
|
||||||
if (!session.parentID)
|
|
||||||
global.sessionPlacement.set({
|
|
||||||
server: tab.server,
|
|
||||||
leafID: session.id,
|
|
||||||
rootID: session.id,
|
|
||||||
directory: session.directory,
|
|
||||||
})
|
|
||||||
return session
|
|
||||||
})
|
|
||||||
.catch(() => undefined),
|
|
||||||
)
|
)
|
||||||
const session = createMemo(() => cachedSession() ?? loadedSession())
|
const session = createMemo(() => cachedSession() ?? loadedSession())
|
||||||
let prefetched = false
|
let prefetched = false
|
||||||
|
|||||||
@@ -1,175 +1,23 @@
|
|||||||
import { batch, createMemo } from "solid-js"
|
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
|
||||||
import {
|
|
||||||
clearSessionPrefetch,
|
|
||||||
getSessionPrefetch,
|
|
||||||
getSessionPrefetchPromise,
|
|
||||||
setSessionPrefetch,
|
|
||||||
} from "./global-sync/session-prefetch"
|
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
import { createMemo } from "solid-js"
|
||||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
import { produce, reconcile, type SetStoreFunction } from "solid-js/store"
|
||||||
import { type createServerSdkContext } from "./server-sdk"
|
import type { createServerSdkContext } from "./server-sdk"
|
||||||
import { type createServerSyncContextInner } from "./server-sync"
|
import type { createServerSyncContextInner } from "./server-sync"
|
||||||
|
import type { State } from "./global-sync/types"
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
|
||||||
|
|
||||||
function sortParts(parts: Part[]) {
|
|
||||||
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
function runInflight(map: Map<string, Promise<void>>, key: string, task: () => Promise<void>) {
|
|
||||||
const pending = map.get(key)
|
|
||||||
if (pending) return pending
|
|
||||||
const promise = task().finally(() => {
|
|
||||||
map.delete(key)
|
|
||||||
})
|
|
||||||
map.set(key, promise)
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
|
||||||
|
|
||||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
const sessionFields = new Set([
|
||||||
const isNotFound = (error: unknown) =>
|
"session_status",
|
||||||
error instanceof Error &&
|
"session_working",
|
||||||
typeof error.cause === "object" &&
|
"session_diff",
|
||||||
error.cause !== null &&
|
"todo",
|
||||||
(error.cause as { status?: unknown }).status === 404
|
"permission",
|
||||||
|
"question",
|
||||||
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
"message",
|
||||||
const map = new Map(a.map((item) => [item.id, item] as const))
|
"part",
|
||||||
for (const item of b) map.set(item.id, item)
|
"part_text_accum_delta",
|
||||||
return [...map.values()].sort((x, y) => cmp(x.id, y.id))
|
])
|
||||||
}
|
|
||||||
|
|
||||||
type OptimisticStore = {
|
|
||||||
message: Record<string, Message[] | undefined>
|
|
||||||
part: Record<string, Part[] | undefined>
|
|
||||||
}
|
|
||||||
|
|
||||||
type OptimisticAddInput = {
|
|
||||||
sessionID: string
|
|
||||||
message: Message
|
|
||||||
parts: Part[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type OptimisticRemoveInput = {
|
|
||||||
sessionID: string
|
|
||||||
messageID: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type OptimisticItem = {
|
|
||||||
message: Message
|
|
||||||
parts: Part[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessagePage = {
|
|
||||||
session: Message[]
|
|
||||||
part: { id: string; part: Part[] }[]
|
|
||||||
cursor?: string
|
|
||||||
complete: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasParts = (parts: Part[] | undefined, want: Part[]) => {
|
|
||||||
if (!parts) return want.length === 0
|
|
||||||
return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeParts = (parts: Part[] | undefined, want: Part[]) => {
|
|
||||||
if (!parts) return sortParts(want)
|
|
||||||
const next = [...parts]
|
|
||||||
let changed = false
|
|
||||||
for (const part of want) {
|
|
||||||
const result = Binary.search(next, part.id, (item) => item.id)
|
|
||||||
if (result.found) continue
|
|
||||||
next.splice(result.index, 0, part)
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if (!changed) return parts
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
|
||||||
if (items.length === 0) return { ...page, confirmed: [] as string[] }
|
|
||||||
|
|
||||||
const session = [...page.session]
|
|
||||||
const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)]))
|
|
||||||
const confirmed: string[] = []
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
|
||||||
const found = result.found
|
|
||||||
if (!found) session.splice(result.index, 0, item.message)
|
|
||||||
|
|
||||||
const current = part.get(item.message.id)
|
|
||||||
if (found && hasParts(current, item.parts)) {
|
|
||||||
confirmed.push(item.message.id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
part.set(item.message.id, mergeParts(current, item.parts))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
cursor: page.cursor,
|
|
||||||
complete: page.complete,
|
|
||||||
session,
|
|
||||||
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })),
|
|
||||||
confirmed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
|
||||||
const messages = draft.message[input.sessionID]
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
|
|
||||||
const messages = draft.message[input.sessionID]
|
|
||||||
if (messages) {
|
|
||||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
|
||||||
if (result.found) messages.splice(result.index, 1)
|
|
||||||
}
|
|
||||||
delete draft.part[input.messageID]
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) {
|
|
||||||
setStore("message", input.sessionID, (messages: Message[] | undefined) => {
|
|
||||||
if (!messages) return [input.message]
|
|
||||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
|
||||||
const next = [...messages]
|
|
||||||
next.splice(result.index, 0, input.message)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
setStore("part", input.message.id, sortParts(input.parts))
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) {
|
|
||||||
setStore("message", input.sessionID, (messages: Message[] | undefined) => {
|
|
||||||
if (!messages) return messages
|
|
||||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
|
||||||
if (!result.found) return messages
|
|
||||||
const next = [...messages]
|
|
||||||
next.splice(result.index, 1)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
setStore("part", (part: Record<string, Part[] | undefined>) => {
|
|
||||||
if (!(input.messageID in part)) return part
|
|
||||||
const next = { ...part }
|
|
||||||
delete next[input.messageID]
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createDirSyncContext = (
|
export const createDirSyncContext = (
|
||||||
directory: string,
|
directory: string,
|
||||||
@@ -177,210 +25,42 @@ export const createDirSyncContext = (
|
|||||||
serverSDK: ReturnType<typeof createServerSdkContext>,
|
serverSDK: ReturnType<typeof createServerSdkContext>,
|
||||||
) => {
|
) => {
|
||||||
const client = serverSDK.createClient({ directory, throwOnError: true })
|
const client = serverSDK.createClient({ directory, throwOnError: true })
|
||||||
|
|
||||||
type Child = ReturnType<(typeof serverSync)["child"]>
|
|
||||||
type Setter = Child[1]
|
|
||||||
|
|
||||||
const current = createMemo(() => serverSync.child(directory, { mcp: true }))
|
const current = createMemo(() => serverSync.child(directory, { mcp: true }))
|
||||||
const target = (directory?: string) => {
|
|
||||||
if (!directory || directory === directory) return current()
|
|
||||||
return serverSync.child(directory)
|
|
||||||
}
|
|
||||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||||
const initialMessagePageSize = 2
|
const data = new Proxy({} as State, {
|
||||||
const historyMessagePageSize = 200
|
get(_, property: keyof State) {
|
||||||
const inflight = new Map<string, Promise<void>>()
|
if (property === "session_working") return serverSync.session.data.session_working.bind(serverSync.session.data)
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
if (sessionFields.has(property)) return serverSync.session.data[property as keyof typeof serverSync.session.data]
|
||||||
const inflightTodo = new Map<string, Promise<void>>()
|
return current()[0][property]
|
||||||
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
},
|
||||||
const maxDirs = 30
|
|
||||||
const seen = new Map<string, Set<string>>()
|
|
||||||
const [meta, setMeta] = createStore({
|
|
||||||
limit: {} as Record<string, number>,
|
|
||||||
cursor: {} as Record<string, string | undefined>,
|
|
||||||
complete: {} as Record<string, boolean>,
|
|
||||||
loading: {} as Record<string, boolean>,
|
|
||||||
})
|
})
|
||||||
|
const set = ((...input: unknown[]) => {
|
||||||
|
if (typeof input[0] === "string" && sessionFields.has(input[0])) {
|
||||||
|
return (serverSync.session.set as (...args: unknown[]) => unknown)(...input)
|
||||||
|
}
|
||||||
|
const result = (current()[1] as (...args: unknown[]) => unknown)(...input)
|
||||||
|
if (input[0] === "session") current()[0].session.forEach(serverSync.session.remember)
|
||||||
|
return result
|
||||||
|
}) as SetStoreFunction<State>
|
||||||
|
|
||||||
const getSession = (sessionID: string) => {
|
const index = (sessionID: string) => {
|
||||||
const store = current()[0]
|
const session = serverSync.session.get(sessionID)
|
||||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
if (!session || session.directory !== directory) return
|
||||||
if (match.found) return store.session[match.index]
|
const [store, setStore] = current()
|
||||||
return undefined
|
const result = Binary.search(store.session, session.id, (item) => item.id)
|
||||||
}
|
if (result.found) {
|
||||||
|
setStore("session", result.index, reconcile(session))
|
||||||
const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
const list = optimistic.get(key)
|
|
||||||
if (list) {
|
|
||||||
list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]]))
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => {
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
if (!messageID) {
|
|
||||||
optimistic.delete(key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = optimistic.get(key)
|
|
||||||
if (!list) return
|
|
||||||
list.delete(messageID)
|
|
||||||
if (list.size === 0) optimistic.delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOptimistic = (directory: string, sessionID: string) => [
|
|
||||||
...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []),
|
|
||||||
]
|
|
||||||
|
|
||||||
const seenFor = (directory: string) => {
|
|
||||||
const existing = seen.get(directory)
|
|
||||||
if (existing) {
|
|
||||||
seen.delete(directory)
|
|
||||||
seen.set(directory, existing)
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
const created = new Set<string>()
|
|
||||||
seen.set(directory, created)
|
|
||||||
while (seen.size > maxDirs) {
|
|
||||||
const first = seen.keys().next().value
|
|
||||||
if (!first) break
|
|
||||||
const stale = [...(seen.get(first) ?? [])]
|
|
||||||
seen.delete(first)
|
|
||||||
const [, setStore] = serverSync.child(first, { bootstrap: false })
|
|
||||||
evict(first, setStore, stale)
|
|
||||||
}
|
|
||||||
return created
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
|
||||||
if (sessionIDs.length === 0) return
|
|
||||||
for (const sessionID of sessionIDs) {
|
|
||||||
clearOptimistic(directory, sessionID)
|
|
||||||
}
|
|
||||||
setMeta(
|
|
||||||
produce((draft) => {
|
|
||||||
for (const sessionID of sessionIDs) {
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
delete draft.limit[key]
|
|
||||||
delete draft.cursor[key]
|
|
||||||
delete draft.complete[key]
|
|
||||||
delete draft.loading[key]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
|
|
||||||
if (sessionIDs.length === 0) return
|
|
||||||
clearSessionPrefetch(serverSDK.scope, directory, sessionIDs)
|
|
||||||
for (const sessionID of sessionIDs) {
|
|
||||||
serverSync.todo.set(sessionID, undefined)
|
|
||||||
}
|
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
"session",
|
||||||
dropSessionCaches(draft, sessionIDs)
|
produce((draft) => void draft.splice(result.index, 0, session)),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
clearMeta(directory, sessionIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
const touch = (directory: string, setStore: Setter, sessionID: string) => {
|
|
||||||
const stale = pickSessionCacheEvictions({
|
|
||||||
seen: seenFor(directory),
|
|
||||||
keep: sessionID,
|
|
||||||
limit: SESSION_CACHE_LIMIT,
|
|
||||||
})
|
|
||||||
evict(directory, setStore, stale)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchMessages = async (input: { client: typeof client; sessionID: string; limit: number; before?: string }) => {
|
|
||||||
const messages = await retry(() =>
|
|
||||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
|
|
||||||
)
|
|
||||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
|
||||||
const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id))
|
|
||||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
|
||||||
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
|
||||||
return {
|
|
||||||
session,
|
|
||||||
part,
|
|
||||||
cursor,
|
|
||||||
complete: !cursor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false
|
|
||||||
|
|
||||||
const loadMessages = async (input: {
|
|
||||||
directory: string
|
|
||||||
client: typeof client
|
|
||||||
setStore: Setter
|
|
||||||
sessionID: string
|
|
||||||
limit: number
|
|
||||||
before?: string
|
|
||||||
mode?: "replace" | "prepend"
|
|
||||||
}) => {
|
|
||||||
const key = keyFor(input.directory, input.sessionID)
|
|
||||||
if (meta.loading[key]) return
|
|
||||||
|
|
||||||
setMeta("loading", key, true)
|
|
||||||
await fetchMessages(input)
|
|
||||||
.then((page) => {
|
|
||||||
if (!tracked(input.directory, input.sessionID)) return
|
|
||||||
const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID))
|
|
||||||
for (const messageID of next.confirmed) {
|
|
||||||
clearOptimistic(input.directory, input.sessionID, messageID)
|
|
||||||
}
|
|
||||||
const [store] = serverSync.child(input.directory, { bootstrap: false })
|
|
||||||
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
|
||||||
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
|
||||||
batch(() => {
|
|
||||||
input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
|
|
||||||
for (const p of next.part) {
|
|
||||||
const filtered = p.part.filter((x) => !SKIP_PARTS.has(x.type))
|
|
||||||
if (filtered.length) input.setStore("part", p.id, filtered)
|
|
||||||
}
|
|
||||||
setMeta("limit", key, message.length)
|
|
||||||
setMeta("cursor", key, next.cursor)
|
|
||||||
setMeta("complete", key, next.complete)
|
|
||||||
setSessionPrefetch({
|
|
||||||
scope: serverSDK.scope,
|
|
||||||
directory: input.directory,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
limit: message.length,
|
|
||||||
cursor: next.cursor,
|
|
||||||
complete: next.complete,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (isNotFound(error) && !tracked(input.directory, input.sessionID)) return
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setMeta(
|
|
||||||
produce((draft) => {
|
|
||||||
if (!tracked(input.directory, input.sessionID)) {
|
|
||||||
delete draft.loading[key]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
draft.loading[key] = false
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get data() {
|
data,
|
||||||
return current()[0]
|
set,
|
||||||
},
|
|
||||||
get set(): Setter {
|
|
||||||
return current()[1]
|
|
||||||
},
|
|
||||||
get status() {
|
get status() {
|
||||||
return current()[0].status
|
return current()[0].status
|
||||||
},
|
},
|
||||||
@@ -389,24 +69,20 @@ export const createDirSyncContext = (
|
|||||||
},
|
},
|
||||||
get project() {
|
get project() {
|
||||||
const store = current()[0]
|
const store = current()[0]
|
||||||
const match = Binary.search(serverSync.data.project, store.project, (p) => p.id)
|
const match = Binary.search(serverSync.data.project, store.project, (project) => project.id)
|
||||||
if (match.found) return serverSync.data.project[match.index]
|
if (match.found) return serverSync.data.project[match.index]
|
||||||
return undefined
|
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
get: getSession,
|
get(sessionID: string) {
|
||||||
|
const session = serverSync.session.get(sessionID)
|
||||||
|
if (session?.directory === directory) return session
|
||||||
|
},
|
||||||
optimistic: {
|
optimistic: {
|
||||||
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
||||||
const _directory = input.directory ?? directory
|
serverSync.session.optimistic.add(input)
|
||||||
const [, setStore] = target(input.directory)
|
|
||||||
setOptimistic(_directory, input.sessionID, { message: input.message, parts: input.parts })
|
|
||||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
|
||||||
},
|
},
|
||||||
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
||||||
const _directory = input.directory ?? directory
|
serverSync.session.optimistic.remove(input)
|
||||||
const [, setStore] = target(input.directory)
|
|
||||||
clearOptimistic(_directory, input.sessionID, input.messageID)
|
|
||||||
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
addOptimisticMessage(input: {
|
addOptimisticMessage(input: {
|
||||||
@@ -417,194 +93,48 @@ export const createDirSyncContext = (
|
|||||||
model: { providerID: string; modelID: string }
|
model: { providerID: string; modelID: string }
|
||||||
variant?: string
|
variant?: string
|
||||||
}) {
|
}) {
|
||||||
const message: Message = {
|
serverSync.session.optimistic.add({
|
||||||
id: input.messageID,
|
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
role: "user",
|
message: {
|
||||||
time: { created: Date.now() },
|
id: input.messageID,
|
||||||
agent: input.agent,
|
sessionID: input.sessionID,
|
||||||
model: { ...input.model, variant: input.variant },
|
role: "user",
|
||||||
}
|
time: { created: Date.now() },
|
||||||
const [, setStore] = target()
|
agent: input.agent,
|
||||||
setOptimistic(directory, input.sessionID, { message, parts: input.parts })
|
model: { ...input.model, variant: input.variant },
|
||||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
},
|
||||||
sessionID: input.sessionID,
|
|
||||||
message,
|
|
||||||
parts: input.parts,
|
parts: input.parts,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async sync(sessionID: string, opts?: { force?: boolean }) {
|
async sync(sessionID: string, options?: { force?: boolean }) {
|
||||||
const [store, setStore] = serverSync.child(directory)
|
await serverSync.session.sync(sessionID, options)
|
||||||
const key = keyFor(directory, sessionID)
|
index(sessionID)
|
||||||
|
|
||||||
touch(directory, setStore, sessionID)
|
|
||||||
|
|
||||||
const seeded = getSessionPrefetch(serverSDK.scope, directory, sessionID)
|
|
||||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
|
||||||
batch(() => {
|
|
||||||
setMeta("limit", key, seeded.limit)
|
|
||||||
setMeta("cursor", key, seeded.cursor)
|
|
||||||
setMeta("complete", key, seeded.complete)
|
|
||||||
setMeta("loading", key, false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return runInflight(inflight, key, async () => {
|
|
||||||
const pending = getSessionPrefetchPromise(serverSDK.scope, directory, sessionID)
|
|
||||||
if (pending) {
|
|
||||||
await pending
|
|
||||||
const seeded = getSessionPrefetch(serverSDK.scope, directory, sessionID)
|
|
||||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
|
||||||
batch(() => {
|
|
||||||
setMeta("limit", key, seeded.limit)
|
|
||||||
setMeta("cursor", key, seeded.cursor)
|
|
||||||
setMeta("complete", key, seeded.complete)
|
|
||||||
setMeta("loading", key, false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
|
||||||
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
|
|
||||||
if (cached && hasSession && !opts?.force) return
|
|
||||||
|
|
||||||
const limit = meta.limit[key] ?? initialMessagePageSize
|
|
||||||
const sessionReq =
|
|
||||||
hasSession && !opts?.force
|
|
||||||
? Promise.resolve()
|
|
||||||
: retry(() => client.session.get({ sessionID }))
|
|
||||||
.then((session) => {
|
|
||||||
if (!tracked(directory, sessionID)) return
|
|
||||||
const data = session.data
|
|
||||||
if (!data) return
|
|
||||||
setStore(
|
|
||||||
"session",
|
|
||||||
produce((draft) => {
|
|
||||||
const match = Binary.search(draft, sessionID, (s) => s.id)
|
|
||||||
if (match.found) {
|
|
||||||
draft[match.index] = data
|
|
||||||
return
|
|
||||||
}
|
|
||||||
draft.splice(match.index, 0, data)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (isNotFound(error) && !tracked(directory, sessionID)) return
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
|
|
||||||
const messagesReq =
|
|
||||||
cached && !opts?.force
|
|
||||||
? Promise.resolve()
|
|
||||||
: loadMessages({
|
|
||||||
directory,
|
|
||||||
client,
|
|
||||||
setStore,
|
|
||||||
sessionID,
|
|
||||||
limit,
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all([sessionReq, messagesReq])
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
async diff(sessionID: string, opts?: { force?: boolean }) {
|
diff: serverSync.session.diff,
|
||||||
const [store, setStore] = serverSync.child(directory)
|
todo: serverSync.session.todo,
|
||||||
touch(directory, setStore, sessionID)
|
history: serverSync.session.history,
|
||||||
if (store.session_diff[sessionID] !== undefined && !opts?.force) return
|
evict(sessionID: string) {
|
||||||
|
serverSync.session.evict(sessionID)
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
return runInflight(inflightDiff, key, () =>
|
|
||||||
retry(() => client.session.diff({ sessionID })).then((diff) => {
|
|
||||||
if (!tracked(directory, sessionID)) return
|
|
||||||
setStore("session_diff", sessionID, reconcile(list(diff.data), { key: "file" }))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
async todo(sessionID: string, opts?: { force?: boolean }) {
|
|
||||||
const [store, setStore] = serverSync.child(directory)
|
|
||||||
touch(directory, setStore, sessionID)
|
|
||||||
const existing = store.todo[sessionID]
|
|
||||||
const cached = serverSync.data.session_todo[sessionID]
|
|
||||||
if (existing !== undefined) {
|
|
||||||
if (cached === undefined) {
|
|
||||||
serverSync.todo.set(sessionID, existing)
|
|
||||||
}
|
|
||||||
if (!opts?.force) return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cached !== undefined) {
|
|
||||||
setStore("todo", sessionID, reconcile(cached, { key: "id" }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
return runInflight(inflightTodo, key, () =>
|
|
||||||
retry(() => client.session.todo({ sessionID })).then((todo) => {
|
|
||||||
if (!tracked(directory, sessionID)) return
|
|
||||||
const list = todo.data ?? []
|
|
||||||
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
|
||||||
serverSync.todo.set(sessionID, list)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
history: {
|
|
||||||
more(sessionID: string) {
|
|
||||||
const store = current()[0]
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
if (store.message[sessionID] === undefined) return false
|
|
||||||
if (meta.limit[key] === undefined) return false
|
|
||||||
if (meta.complete[key]) return false
|
|
||||||
return !!meta.cursor[key]
|
|
||||||
},
|
|
||||||
loading(sessionID: string) {
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
return meta.loading[key] ?? false
|
|
||||||
},
|
|
||||||
async loadMore(sessionID: string, count?: number) {
|
|
||||||
const [, setStore] = serverSync.child(directory)
|
|
||||||
touch(directory, setStore, sessionID)
|
|
||||||
const key = keyFor(directory, sessionID)
|
|
||||||
const step = count ?? historyMessagePageSize
|
|
||||||
if (meta.loading[key]) return
|
|
||||||
if (meta.complete[key]) return
|
|
||||||
const before = meta.cursor[key]
|
|
||||||
if (!before) return
|
|
||||||
|
|
||||||
await loadMessages({
|
|
||||||
directory,
|
|
||||||
client,
|
|
||||||
setStore,
|
|
||||||
sessionID,
|
|
||||||
limit: step,
|
|
||||||
before,
|
|
||||||
mode: "prepend",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
evict(sessionID: string, _directory = directory) {
|
|
||||||
const [, setStore] = serverSync.child(_directory)
|
|
||||||
seenFor(_directory).delete(sessionID)
|
|
||||||
evict(_directory, setStore, [sessionID])
|
|
||||||
},
|
},
|
||||||
fetch: async (count = 10) => {
|
fetch: async (count = 10) => {
|
||||||
const [store, setStore] = serverSync.child(directory)
|
const [store, setStore] = current()
|
||||||
setStore("limit", (x) => x + count)
|
setStore("limit", (value) => value + count)
|
||||||
await client.session.list().then((x) => {
|
const response = await client.session.list()
|
||||||
const sessions = (x.data ?? [])
|
const sessions = (response.data ?? [])
|
||||||
.filter((s) => !!s?.id)
|
.filter((session) => !!session?.id)
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
.slice(0, store.limit)
|
.slice(0, store.limit)
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
sessions.forEach(serverSync.session.remember)
|
||||||
})
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
},
|
},
|
||||||
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
||||||
archive: async (sessionID: string) => {
|
archive: async (sessionID: string) => {
|
||||||
const [, setStore] = serverSync.child(directory)
|
await serverSDK.client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||||
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
current()[1](
|
||||||
setStore(
|
"session",
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
const match = Binary.search(draft, sessionID, (session) => session.id)
|
||||||
if (match.found) draft.session.splice(match.index, 1)
|
if (match.found) draft.splice(match.index, 1)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { QueryClient } from "@tanstack/solid-query"
|
import { QueryClient } from "@tanstack/solid-query"
|
||||||
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
|
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
|
||||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
import { ServerScope } from "@/utils/server-scope"
|
||||||
|
|||||||
@@ -7,28 +7,25 @@ import type {
|
|||||||
ProviderAuthResponse,
|
ProviderAuthResponse,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
Session,
|
Session,
|
||||||
Todo,
|
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
|
import type { ServerSession } from "../server-session"
|
||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||||
import { loadMcpQuery } from "../server-sync"
|
import { loadMcpQuery } from "../server-sync"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
path: Path
|
path: Path
|
||||||
project: Project[]
|
project: Project[]
|
||||||
session_todo: {
|
|
||||||
[sessionID: string]: Todo[]
|
|
||||||
}
|
|
||||||
provider: NormalizedProviderListResponse
|
provider: NormalizedProviderListResponse
|
||||||
provider_auth: ProviderAuthResponse
|
provider_auth: ProviderAuthResponse
|
||||||
config: Config
|
config: Config
|
||||||
@@ -215,6 +212,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
provider: NormalizedProviderListResponse
|
provider: NormalizedProviderListResponse
|
||||||
}
|
}
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
|
session?: ServerSession
|
||||||
}) {
|
}) {
|
||||||
const loading = input.store.status !== "complete"
|
const loading = input.store.status !== "complete"
|
||||||
const seededProject = projectID(input.directory, input.global.project)
|
const seededProject = projectID(input.directory, input.global.project)
|
||||||
@@ -238,7 +236,30 @@ export async function bootstrapDirectory(input: {
|
|||||||
.then((data) => input.setStore("agent", data)),
|
.then((data) => input.setStore("agent", data)),
|
||||||
() =>
|
() =>
|
||||||
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.session.status().then(async (x) => {
|
||||||
|
if (input.session) {
|
||||||
|
const statuses = x.data ?? {}
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(statuses).map((sessionID) => input.session!.resolve(sessionID).catch(() => undefined)),
|
||||||
|
)
|
||||||
|
input.session.set(
|
||||||
|
"session_status",
|
||||||
|
produce((draft) => {
|
||||||
|
for (const sessionID of Object.keys(draft)) {
|
||||||
|
if (statuses[sessionID]) continue
|
||||||
|
if (input.session?.get(sessionID)?.directory === input.directory) delete draft[sessionID]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
for (const [sessionID, status] of Object.entries(statuses)) {
|
||||||
|
input.session.set("session_status", sessionID, reconcile(status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!input.session) input.setStore("session_status", x.data!)
|
||||||
|
}),
|
||||||
|
),
|
||||||
!seededProject &&
|
!seededProject &&
|
||||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
||||||
!seededPath &&
|
!seededPath &&
|
||||||
@@ -263,21 +284,25 @@ export async function bootstrapDirectory(input: {
|
|||||||
const grouped = groupBySession(
|
const grouped = groupBySession(
|
||||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||||
)
|
)
|
||||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
const warm = input.session
|
||||||
|
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
|
||||||
|
: warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk })
|
||||||
|
return warm.then(() =>
|
||||||
batch(() => {
|
batch(() => {
|
||||||
for (const sessionID of Object.keys(input.store.permission)) {
|
const current = input.session?.data.permission ?? input.store.permission
|
||||||
|
for (const sessionID of Object.keys(current)) {
|
||||||
if (grouped[sessionID]) continue
|
if (grouped[sessionID]) continue
|
||||||
input.setStore("permission", sessionID, [])
|
if (input.session?.get(sessionID)?.directory !== input.directory) continue
|
||||||
|
if (input.session) input.session.set("permission", sessionID, [])
|
||||||
|
if (!input.session) input.setStore("permission", sessionID, [])
|
||||||
}
|
}
|
||||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||||
input.setStore(
|
const value = reconcile(
|
||||||
"permission",
|
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
sessionID,
|
{ key: "id" },
|
||||||
reconcile(
|
|
||||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
if (input.session) input.session.set("permission", sessionID, value)
|
||||||
|
if (!input.session) input.setStore("permission", sessionID, value)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -288,21 +313,25 @@ export async function bootstrapDirectory(input: {
|
|||||||
input.sdk.question.list().then((x) => {
|
input.sdk.question.list().then((x) => {
|
||||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
const warm = input.session
|
||||||
|
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
|
||||||
|
: warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk })
|
||||||
|
return warm.then(() =>
|
||||||
batch(() => {
|
batch(() => {
|
||||||
for (const sessionID of Object.keys(input.store.question)) {
|
const current = input.session?.data.question ?? input.store.question
|
||||||
|
for (const sessionID of Object.keys(current)) {
|
||||||
if (grouped[sessionID]) continue
|
if (grouped[sessionID]) continue
|
||||||
input.setStore("question", sessionID, [])
|
if (input.session?.get(sessionID)?.directory !== input.directory) continue
|
||||||
|
if (input.session) input.session.set("question", sessionID, [])
|
||||||
|
if (!input.session) input.setStore("question", sessionID, [])
|
||||||
}
|
}
|
||||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||||
input.setStore(
|
const value = reconcile(
|
||||||
"question",
|
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
sessionID,
|
{ key: "id" },
|
||||||
reconcile(
|
|
||||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
if (input.session) input.session.set("question", sessionID, value)
|
||||||
|
if (!input.session) input.setStore("question", sessionID, value)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||||
import { createRoot, getOwner, type Owner } from "solid-js"
|
import { createRoot, getOwner, type Owner } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import type { State } from "./types"
|
import type { State } from "./types"
|
||||||
import type { QueryOptionsApi } from "../server-sync"
|
import type { QueryOptionsApi } from "../server-sync"
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
import { ServerScope } from "@/utils/server-scope"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
|||||||
import { useQuery } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { QueryOptionsApi } from "../server-sync"
|
import { QueryOptionsApi } from "../server-sync"
|
||||||
import { directoryKey, type DirectoryKey } from "./utils"
|
import { directoryKey, type DirectoryKey } from "./utils"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import type { ServerScope } from "@/utils/server-scope"
|
import type { ServerScope } from "@/utils/server-scope"
|
||||||
|
|
||||||
export function createChildStoreManager(input: {
|
export function createChildStoreManager(input: {
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ import { dropSessionCaches } from "./session-cache"
|
|||||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||||
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
const SESSION_CONTENT_EVENTS = new Set([
|
||||||
|
"session.diff",
|
||||||
|
"todo.updated",
|
||||||
|
"session.status",
|
||||||
|
"message.updated",
|
||||||
|
"message.removed",
|
||||||
|
"message.part.updated",
|
||||||
|
"message.part.removed",
|
||||||
|
"message.part.delta",
|
||||||
|
"permission.asked",
|
||||||
|
"permission.replied",
|
||||||
|
"question.asked",
|
||||||
|
"question.replied",
|
||||||
|
"question.rejected",
|
||||||
|
])
|
||||||
|
|
||||||
export function applyGlobalEvent(input: {
|
export function applyGlobalEvent(input: {
|
||||||
event: { type: string; properties?: unknown }
|
event: { type: string; properties?: unknown }
|
||||||
@@ -100,8 +115,11 @@ export function applyDirectoryEvent(input: {
|
|||||||
vcsCache?: VcsCache
|
vcsCache?: VcsCache
|
||||||
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
|
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
|
||||||
retainedLimit?: number
|
retainedLimit?: number
|
||||||
|
sessionContent?: boolean
|
||||||
|
permission?: State["permission"]
|
||||||
}) {
|
}) {
|
||||||
const event = input.event
|
const event = input.event
|
||||||
|
if (input.sessionContent === false && SESSION_CONTENT_EVENTS.has(event.type)) return
|
||||||
const limit = Math.max(input.store.limit, input.retainedLimit ?? 0)
|
const limit = Math.max(input.store.limit, input.retainedLimit ?? 0)
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "server.instance.disposed": {
|
case "server.instance.disposed": {
|
||||||
@@ -117,7 +135,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
}
|
}
|
||||||
const next = input.store.session.slice()
|
const next = input.store.session.slice()
|
||||||
next.splice(result.index, 0, info)
|
next.splice(result.index, 0, info)
|
||||||
const trimmed = trimSessions(next, { limit, permission: input.store.permission })
|
const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission })
|
||||||
input.setStore("session", reconcile(trimmed, { key: "id" }))
|
input.setStore("session", reconcile(trimmed, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
|
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
|
||||||
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
|
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
|
||||||
@@ -147,7 +165,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
}
|
}
|
||||||
const next = input.store.session.slice()
|
const next = input.store.session.slice()
|
||||||
next.splice(result.index, 0, info)
|
next.splice(result.index, 0, info)
|
||||||
const trimmed = trimSessions(next, { limit, permission: input.store.permission })
|
const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission })
|
||||||
input.setStore("session", reconcile(trimmed, { key: "id" }))
|
input.setStore("session", reconcile(trimmed, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
|
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import {
|
|
||||||
clearSessionPrefetch,
|
|
||||||
clearSessionPrefetchDirectory,
|
|
||||||
getSessionPrefetch,
|
|
||||||
runSessionPrefetch,
|
|
||||||
setSessionPrefetch,
|
|
||||||
shouldSkipSessionPrefetch,
|
|
||||||
} from "./session-prefetch"
|
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
|
||||||
|
|
||||||
const scope = ServerScope.local
|
|
||||||
|
|
||||||
describe("session prefetch", () => {
|
|
||||||
test("stores and clears message metadata by directory", () => {
|
|
||||||
clearSessionPrefetch(scope, "/tmp/a", ["ses_1"])
|
|
||||||
clearSessionPrefetch(scope, "/tmp/b", ["ses_1"])
|
|
||||||
|
|
||||||
setSessionPrefetch({
|
|
||||||
directory: "/tmp/a",
|
|
||||||
scope,
|
|
||||||
sessionID: "ses_1",
|
|
||||||
limit: 200,
|
|
||||||
cursor: "abc",
|
|
||||||
complete: false,
|
|
||||||
at: 123,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(getSessionPrefetch(scope, "/tmp/a", "ses_1")).toEqual({
|
|
||||||
limit: 200,
|
|
||||||
cursor: "abc",
|
|
||||||
complete: false,
|
|
||||||
at: 123,
|
|
||||||
})
|
|
||||||
expect(getSessionPrefetch(scope, "/tmp/b", "ses_1")).toBeUndefined()
|
|
||||||
|
|
||||||
clearSessionPrefetch(scope, "/tmp/a", ["ses_1"])
|
|
||||||
|
|
||||||
expect(getSessionPrefetch(scope, "/tmp/a", "ses_1")).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("dedupes inflight work", async () => {
|
|
||||||
clearSessionPrefetch(scope, "/tmp/c", ["ses_2"])
|
|
||||||
|
|
||||||
let calls = 0
|
|
||||||
const run = () =>
|
|
||||||
runSessionPrefetch({
|
|
||||||
directory: "/tmp/c",
|
|
||||||
scope,
|
|
||||||
sessionID: "ses_2",
|
|
||||||
task: async () => {
|
|
||||||
calls += 1
|
|
||||||
return { limit: 100, cursor: "next", complete: true, at: 456 }
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const [a, b] = await Promise.all([run(), run()])
|
|
||||||
|
|
||||||
expect(calls).toBe(1)
|
|
||||||
expect(a).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 })
|
|
||||||
expect(b).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("clears a whole directory", () => {
|
|
||||||
setSessionPrefetch({
|
|
||||||
scope,
|
|
||||||
directory: "/tmp/d",
|
|
||||||
sessionID: "ses_1",
|
|
||||||
limit: 10,
|
|
||||||
cursor: "a",
|
|
||||||
complete: true,
|
|
||||||
at: 1,
|
|
||||||
})
|
|
||||||
setSessionPrefetch({
|
|
||||||
scope,
|
|
||||||
directory: "/tmp/d",
|
|
||||||
sessionID: "ses_2",
|
|
||||||
limit: 20,
|
|
||||||
cursor: "b",
|
|
||||||
complete: false,
|
|
||||||
at: 2,
|
|
||||||
})
|
|
||||||
setSessionPrefetch({
|
|
||||||
scope,
|
|
||||||
directory: "/tmp/e",
|
|
||||||
sessionID: "ses_1",
|
|
||||||
limit: 30,
|
|
||||||
cursor: "c",
|
|
||||||
complete: true,
|
|
||||||
at: 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
clearSessionPrefetchDirectory(scope, "/tmp/d")
|
|
||||||
|
|
||||||
expect(getSessionPrefetch(scope, "/tmp/d", "ses_1")).toBeUndefined()
|
|
||||||
expect(getSessionPrefetch(scope, "/tmp/d", "ses_2")).toBeUndefined()
|
|
||||||
expect(getSessionPrefetch(scope, "/tmp/e", "ses_1")).toEqual({ limit: 30, cursor: "c", complete: true, at: 3 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isolates identical directories and sessions by server scope", () => {
|
|
||||||
const remote = "https://debian.example" as ServerScope
|
|
||||||
setSessionPrefetch({ scope, directory: "/repo", sessionID: "ses_1", limit: 10, complete: true, at: 1 })
|
|
||||||
setSessionPrefetch({ scope: remote, directory: "/repo", sessionID: "ses_1", limit: 20, complete: true, at: 2 })
|
|
||||||
|
|
||||||
expect(getSessionPrefetch(scope, "/repo", "ses_1")?.limit).toBe(10)
|
|
||||||
expect(getSessionPrefetch(remote, "/repo", "ses_1")?.limit).toBe(20)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("refreshes stale first-page prefetched history", () => {
|
|
||||||
expect(
|
|
||||||
shouldSkipSessionPrefetch({
|
|
||||||
message: true,
|
|
||||||
info: { limit: 200, cursor: "x", complete: false, at: 1 },
|
|
||||||
chunk: 200,
|
|
||||||
now: 1 + 15_001,
|
|
||||||
}),
|
|
||||||
).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("keeps deeper or complete history cached", () => {
|
|
||||||
expect(
|
|
||||||
shouldSkipSessionPrefetch({
|
|
||||||
message: true,
|
|
||||||
info: { limit: 400, cursor: "x", complete: false, at: 1 },
|
|
||||||
chunk: 200,
|
|
||||||
now: 1 + 15_001,
|
|
||||||
}),
|
|
||||||
).toBe(true)
|
|
||||||
|
|
||||||
expect(
|
|
||||||
shouldSkipSessionPrefetch({
|
|
||||||
message: true,
|
|
||||||
info: { limit: 120, complete: true, at: 1 },
|
|
||||||
chunk: 200,
|
|
||||||
now: 1 + 15_001,
|
|
||||||
}),
|
|
||||||
).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
|
||||||
|
|
||||||
const key = (scope: ServerScope, directory: string, sessionID: string) => ScopedKey.from(scope, directory, sessionID)
|
|
||||||
|
|
||||||
export const SESSION_PREFETCH_TTL = 15_000
|
|
||||||
|
|
||||||
type Meta = {
|
|
||||||
limit: number
|
|
||||||
cursor?: string
|
|
||||||
complete: boolean
|
|
||||||
at: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldSkipSessionPrefetch(input: { message: boolean; info?: Meta; chunk: number; now?: number }) {
|
|
||||||
if (input.message) {
|
|
||||||
if (!input.info) return true
|
|
||||||
if (input.info.complete) return true
|
|
||||||
if (input.info.limit > input.chunk) return true
|
|
||||||
} else {
|
|
||||||
if (!input.info) return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return (input.now ?? Date.now()) - input.info.at < SESSION_PREFETCH_TTL
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = new Map<string, Meta>()
|
|
||||||
const inflight = new Map<string, Promise<Meta | undefined>>()
|
|
||||||
const rev = new Map<string, number>()
|
|
||||||
|
|
||||||
const version = (id: string) => rev.get(id) ?? 0
|
|
||||||
|
|
||||||
export function getSessionPrefetch(scope: ServerScope, directory: string, sessionID: string) {
|
|
||||||
return cache.get(key(scope, directory, sessionID))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSessionPrefetchPromise(scope: ServerScope, directory: string, sessionID: string) {
|
|
||||||
return inflight.get(key(scope, directory, sessionID))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSessionPrefetchInflight(scope: ServerScope) {
|
|
||||||
const prefix = ScopedKey.prefix(scope)
|
|
||||||
for (const id of inflight.keys()) {
|
|
||||||
if (id.startsWith(prefix)) inflight.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSessionPrefetchCurrent(scope: ServerScope, directory: string, sessionID: string, value: number) {
|
|
||||||
return version(key(scope, directory, sessionID)) === value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runSessionPrefetch(input: {
|
|
||||||
directory: string
|
|
||||||
scope: ServerScope
|
|
||||||
sessionID: string
|
|
||||||
task: (value: number) => Promise<Meta | undefined>
|
|
||||||
}) {
|
|
||||||
const id = key(input.scope, input.directory, input.sessionID)
|
|
||||||
const pending = inflight.get(id)
|
|
||||||
if (pending) return pending
|
|
||||||
|
|
||||||
const value = version(id)
|
|
||||||
|
|
||||||
const promise = input.task(value).finally(() => {
|
|
||||||
if (inflight.get(id) === promise) inflight.delete(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
inflight.set(id, promise)
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSessionPrefetch(input: {
|
|
||||||
directory: string
|
|
||||||
scope: ServerScope
|
|
||||||
sessionID: string
|
|
||||||
limit: number
|
|
||||||
cursor?: string
|
|
||||||
complete: boolean
|
|
||||||
at?: number
|
|
||||||
}) {
|
|
||||||
cache.set(key(input.scope, input.directory, input.sessionID), {
|
|
||||||
limit: input.limit,
|
|
||||||
cursor: input.cursor,
|
|
||||||
complete: input.complete,
|
|
||||||
at: input.at ?? Date.now(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSessionPrefetch(scope: ServerScope, directory: string, sessionIDs: Iterable<string>) {
|
|
||||||
for (const sessionID of sessionIDs) {
|
|
||||||
if (!sessionID) continue
|
|
||||||
const id = key(scope, directory, sessionID)
|
|
||||||
rev.set(id, version(id) + 1)
|
|
||||||
cache.delete(id)
|
|
||||||
inflight.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSessionPrefetchDirectory(scope: ServerScope, directory: string) {
|
|
||||||
const prefix = ScopedKey.prefix(scope, directory)
|
|
||||||
const keys = new Set([...cache.keys(), ...inflight.keys()])
|
|
||||||
for (const id of keys) {
|
|
||||||
if (!id.startsWith(prefix)) continue
|
|
||||||
rev.set(id, version(id) + 1)
|
|
||||||
cache.delete(id)
|
|
||||||
inflight.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
Todo,
|
Todo,
|
||||||
VcsInfo,
|
VcsInfo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import type { Accessor } from "solid-js"
|
import type { Accessor } from "solid-js"
|
||||||
import type { SetStoreFunction, Store } from "solid-js/store"
|
import type { SetStoreFunction, Store } from "solid-js/store"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
|
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
|
||||||
|
|
||||||
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ import { createServerSyncContext } from "./server-sync"
|
|||||||
import { getOwner } from "solid-js/web"
|
import { getOwner } from "solid-js/web"
|
||||||
import { QueryClient } from "@tanstack/solid-query"
|
import { QueryClient } from "@tanstack/solid-query"
|
||||||
import type { ServerScope } from "@/utils/server-scope"
|
import type { ServerScope } from "@/utils/server-scope"
|
||||||
import { createSessionPlacementStore } from "@/utils/session-placement"
|
|
||||||
|
|
||||||
export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({
|
export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({
|
||||||
name: "Global",
|
name: "Global",
|
||||||
init: () => {
|
init: () => {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const sessionPlacement = createSessionPlacementStore()
|
|
||||||
const serverHealth = useServerHealth(
|
const serverHealth = useServerHealth(
|
||||||
() => server.list,
|
() => server.list,
|
||||||
() => true,
|
() => true,
|
||||||
@@ -87,7 +85,6 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sessionPlacement,
|
|
||||||
ensureServerCtx(conn: ServerConnection.Any) {
|
ensureServerCtx(conn: ServerConnection.Any) {
|
||||||
return ensureServerCtx(conn)
|
return ensureServerCtx(conn)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { OpencodeClient, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { createServerSession } from "./server-session"
|
||||||
|
|
||||||
|
const session = (id: string, parentID?: string): Session => ({
|
||||||
|
id,
|
||||||
|
slug: id,
|
||||||
|
projectID: "project",
|
||||||
|
directory: "/repo",
|
||||||
|
title: id,
|
||||||
|
version: "1",
|
||||||
|
parentID,
|
||||||
|
time: { created: 1, updated: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
function setup(sessions: Record<string, Session>) {
|
||||||
|
const get: unknown[] = []
|
||||||
|
const messages: unknown[] = []
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
get: async (input: unknown) => {
|
||||||
|
get.push(input)
|
||||||
|
const id = (input as { sessionID: string }).sessionID
|
||||||
|
return { data: sessions[id] }
|
||||||
|
},
|
||||||
|
messages: async (input: unknown) => {
|
||||||
|
messages.push(input)
|
||||||
|
return { data: [], response: { headers: new Headers() } }
|
||||||
|
},
|
||||||
|
diff: async () => ({ data: [] }),
|
||||||
|
todo: async () => ({ data: [] }),
|
||||||
|
},
|
||||||
|
} as unknown as OpencodeClient
|
||||||
|
return { get, messages, store: createServerSession(client) }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("server session", () => {
|
||||||
|
test("resolves lineage by session ID without directory", async () => {
|
||||||
|
const ctx = setup({ child: session("child", "root"), root: session("root") })
|
||||||
|
|
||||||
|
const result = await ctx.store.lineage.resolve("child")
|
||||||
|
|
||||||
|
expect(result.root.id).toBe("root")
|
||||||
|
expect(ctx.get).toEqual([{ sessionID: "child" }, { sessionID: "root" }])
|
||||||
|
expect(ctx.store.lineage.peek("child")).toEqual(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loads session content through the server client", async () => {
|
||||||
|
const ctx = setup({ root: session("root") })
|
||||||
|
|
||||||
|
await ctx.store.sync("root")
|
||||||
|
|
||||||
|
expect(ctx.get).toEqual([{ sessionID: "root" }])
|
||||||
|
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 2, before: undefined }])
|
||||||
|
expect(ctx.store.data.message.root).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("applies events without a directory store", () => {
|
||||||
|
const ctx = setup({})
|
||||||
|
ctx.store.apply({ type: "session.created", properties: { info: session("root") } })
|
||||||
|
ctx.store.apply({ type: "session.status", properties: { sessionID: "root", status: { type: "busy" } } })
|
||||||
|
|
||||||
|
expect(ctx.store.get("root")?.directory).toBe("/repo")
|
||||||
|
expect(ctx.store.data.session_working("root")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves pinned session content under server-wide cache pressure", () => {
|
||||||
|
const ctx = setup({})
|
||||||
|
ctx.store.pin("active")
|
||||||
|
ctx.store.optimistic.add({
|
||||||
|
sessionID: "active",
|
||||||
|
message: {
|
||||||
|
id: "message",
|
||||||
|
sessionID: "active",
|
||||||
|
role: "assistant",
|
||||||
|
time: { created: 1 },
|
||||||
|
parentID: "parent",
|
||||||
|
modelID: "model",
|
||||||
|
providerID: "provider",
|
||||||
|
mode: "build",
|
||||||
|
agent: "agent",
|
||||||
|
path: { cwd: "/repo", root: "/repo" },
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
},
|
||||||
|
parts: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let index = 0; index < 50; index++) {
|
||||||
|
ctx.store.apply({
|
||||||
|
type: "session.status",
|
||||||
|
properties: { sessionID: `session-${index}`, status: { type: "busy" } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ctx.store.data.message.active?.map((message) => message.id)).toEqual(["message"])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,626 @@
|
|||||||
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
|
import type {
|
||||||
|
Message,
|
||||||
|
OpencodeClient,
|
||||||
|
Part,
|
||||||
|
PermissionRequest,
|
||||||
|
QuestionRequest,
|
||||||
|
Session,
|
||||||
|
SessionStatus,
|
||||||
|
SnapshotFileDiff,
|
||||||
|
Todo,
|
||||||
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { batch } from "solid-js"
|
||||||
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
|
import { diffs as cleanDiffs, message as cleanMessage } from "@/utils/diffs"
|
||||||
|
import { rootSession } from "@/utils/session-route"
|
||||||
|
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
||||||
|
|
||||||
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
const initialMessagePageSize = 2
|
||||||
|
const historyMessagePageSize = 200
|
||||||
|
const sessionInfoLimit = 2_048
|
||||||
|
|
||||||
|
type OptimisticItem = {
|
||||||
|
message: Message
|
||||||
|
parts: Part[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||||
|
if (!parts) return want.length === 0
|
||||||
|
return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeOptimisticPage(
|
||||||
|
page: { session: Message[]; part: { id: string; part: Part[] }[]; cursor?: string; complete: boolean },
|
||||||
|
items: OptimisticItem[],
|
||||||
|
) {
|
||||||
|
if (items.length === 0) return { ...page, confirmed: [] as string[] }
|
||||||
|
const session = [...page.session]
|
||||||
|
const part = new Map(page.part.map((item) => [item.id, item.part]))
|
||||||
|
const confirmed: string[] = []
|
||||||
|
for (const item of items) {
|
||||||
|
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||||
|
if (!result.found) session.splice(result.index, 0, item.message)
|
||||||
|
const current = part.get(item.message.id)
|
||||||
|
if (result.found && hasParts(current, item.parts)) {
|
||||||
|
confirmed.push(item.message.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
part.set(item.message.id, merge(current ?? [], item.parts))
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...page,
|
||||||
|
session,
|
||||||
|
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, parts]) => ({ id, part: parts })),
|
||||||
|
confirmed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runInflight(map: Map<string, Promise<void>>, key: string, task: () => Promise<void>) {
|
||||||
|
const pending = map.get(key)
|
||||||
|
if (pending) return pending
|
||||||
|
const promise = task().finally(() => {
|
||||||
|
if (map.get(key) === promise) map.delete(key)
|
||||||
|
})
|
||||||
|
map.set(key, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
||||||
|
const items = new Map(a.map((item) => [item.id, item] as const))
|
||||||
|
for (const item of b) items.set(item.id, item)
|
||||||
|
return [...items.values()].sort((x, y) => cmp(x.id, y.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createServerSession(client: OpencodeClient) {
|
||||||
|
const [data, setData] = createStore({
|
||||||
|
info: {} as Record<string, Session | undefined>,
|
||||||
|
session_status: {} as Record<string, SessionStatus>,
|
||||||
|
session_diff: {} as Record<string, SnapshotFileDiff[]>,
|
||||||
|
todo: {} as Record<string, Todo[]>,
|
||||||
|
permission: {} as Record<string, PermissionRequest[]>,
|
||||||
|
question: {} as Record<string, QuestionRequest[]>,
|
||||||
|
message: {} as Record<string, Message[]>,
|
||||||
|
part: {} as Record<string, Part[]>,
|
||||||
|
part_text_accum_delta: {} as Record<string, string>,
|
||||||
|
session_working(id: string) {
|
||||||
|
return (this.session_status[id]?.type ?? "idle") !== "idle"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const requests = new Map<string, Promise<Session>>()
|
||||||
|
const inflight = new Map<string, Promise<void>>()
|
||||||
|
const inflightDiff = new Map<string, Promise<void>>()
|
||||||
|
const inflightTodo = new Map<string, Promise<void>>()
|
||||||
|
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const infoSeen = new Set<string>()
|
||||||
|
const pinned = new Map<string, number>()
|
||||||
|
const generations = new Map<string, number>()
|
||||||
|
const [meta, setMeta] = createStore({
|
||||||
|
limit: {} as Record<string, number | undefined>,
|
||||||
|
cursor: {} as Record<string, string | undefined>,
|
||||||
|
complete: {} as Record<string, boolean | undefined>,
|
||||||
|
loading: {} as Record<string, boolean | undefined>,
|
||||||
|
at: {} as Record<string, number | undefined>,
|
||||||
|
})
|
||||||
|
|
||||||
|
const remember = (session: Session) => {
|
||||||
|
setData("info", session.id, reconcile(session))
|
||||||
|
infoSeen.delete(session.id)
|
||||||
|
infoSeen.add(session.id)
|
||||||
|
if (infoSeen.size > sessionInfoLimit) {
|
||||||
|
const preserve = new Set([
|
||||||
|
...pinned.keys(),
|
||||||
|
...requests.keys(),
|
||||||
|
...Object.entries(data.permission)
|
||||||
|
.filter(([, items]) => items.length > 0)
|
||||||
|
.map(([sessionID]) => sessionID),
|
||||||
|
...Object.entries(data.question)
|
||||||
|
.filter(([, items]) => items.length > 0)
|
||||||
|
.map(([sessionID]) => sessionID),
|
||||||
|
...Object.entries(data.session_status)
|
||||||
|
.filter(([, status]) => status.type !== "idle")
|
||||||
|
.map(([sessionID]) => sessionID),
|
||||||
|
])
|
||||||
|
for (const sessionID of preserve) {
|
||||||
|
let current = data.info[sessionID]
|
||||||
|
while (current) {
|
||||||
|
preserve.add(current.id)
|
||||||
|
current = current.parentID ? data.info[current.parentID] : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stale: string[] = []
|
||||||
|
for (const sessionID of infoSeen) {
|
||||||
|
if (infoSeen.size - stale.length <= sessionInfoLimit) break
|
||||||
|
if (!preserve.has(sessionID)) stale.push(sessionID)
|
||||||
|
}
|
||||||
|
stale.forEach((sessionID) => infoSeen.delete(sessionID))
|
||||||
|
setData(
|
||||||
|
"info",
|
||||||
|
produce((draft) => stale.forEach((sessionID) => delete draft[sessionID])),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolve = (sessionID: string, options?: { force?: boolean }) => {
|
||||||
|
const cached = data.info[sessionID]
|
||||||
|
if (cached && !options?.force) return Promise.resolve(cached)
|
||||||
|
const pending = requests.get(sessionID)
|
||||||
|
if (pending) return pending
|
||||||
|
const generation = generations.get(sessionID) ?? 0
|
||||||
|
const request = client.session.get({ sessionID }).then((result) => {
|
||||||
|
if (!result.data) throw new Error(`Session not found: ${sessionID}`)
|
||||||
|
if ((generations.get(sessionID) ?? 0) !== generation) return result.data
|
||||||
|
return remember(result.data)
|
||||||
|
})
|
||||||
|
requests.set(sessionID, request)
|
||||||
|
void request.then(
|
||||||
|
() => {
|
||||||
|
if (requests.get(sessionID) === request) requests.delete(sessionID)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (requests.get(sessionID) === request) requests.delete(sessionID)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
const peekLineage = (sessionID: string) => {
|
||||||
|
const session = data.info[sessionID]
|
||||||
|
if (!session) return
|
||||||
|
const seen = new Set([session.id])
|
||||||
|
let root = session
|
||||||
|
while (root.parentID) {
|
||||||
|
if (seen.has(root.parentID)) throw new Error(`Session parent cycle: ${root.parentID}`)
|
||||||
|
seen.add(root.parentID)
|
||||||
|
const parent = data.info[root.parentID]
|
||||||
|
if (!parent) return
|
||||||
|
root = parent
|
||||||
|
}
|
||||||
|
return { session, root }
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOptimistic = (sessionID: string, messageID?: string) => {
|
||||||
|
if (!messageID) {
|
||||||
|
optimistic.delete(sessionID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const items = optimistic.get(sessionID)
|
||||||
|
if (!items) return
|
||||||
|
items.delete(messageID)
|
||||||
|
if (items.size === 0) optimistic.delete(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
const evict = (sessionIDs: string[]) => {
|
||||||
|
if (sessionIDs.length === 0) return
|
||||||
|
sessionIDs.forEach((sessionID) => {
|
||||||
|
generations.set(sessionID, (generations.get(sessionID) ?? 0) + 1)
|
||||||
|
clearOptimistic(sessionID)
|
||||||
|
requests.delete(sessionID)
|
||||||
|
inflight.delete(sessionID)
|
||||||
|
inflightDiff.delete(sessionID)
|
||||||
|
inflightTodo.delete(sessionID)
|
||||||
|
})
|
||||||
|
setData(
|
||||||
|
produce((draft) => {
|
||||||
|
dropSessionCaches(draft, sessionIDs)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
setMeta(
|
||||||
|
produce((draft) => {
|
||||||
|
for (const sessionID of sessionIDs) {
|
||||||
|
delete draft.limit[sessionID]
|
||||||
|
delete draft.cursor[sessionID]
|
||||||
|
delete draft.complete[sessionID]
|
||||||
|
delete draft.loading[sessionID]
|
||||||
|
delete draft.at[sessionID]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const protectedSessions = () =>
|
||||||
|
new Set([
|
||||||
|
...pinned.keys(),
|
||||||
|
...requests.keys(),
|
||||||
|
...inflight.keys(),
|
||||||
|
...inflightDiff.keys(),
|
||||||
|
...inflightTodo.keys(),
|
||||||
|
...optimistic.keys(),
|
||||||
|
...Object.entries(data.permission)
|
||||||
|
.filter(([, items]) => items.length > 0)
|
||||||
|
.map(([sessionID]) => sessionID),
|
||||||
|
...Object.entries(data.question)
|
||||||
|
.filter(([, items]) => items.length > 0)
|
||||||
|
.map(([sessionID]) => sessionID),
|
||||||
|
...Object.entries(data.session_status)
|
||||||
|
.filter(([, status]) => status.type !== "idle")
|
||||||
|
.map(([sessionID]) => sessionID),
|
||||||
|
])
|
||||||
|
|
||||||
|
const touch = (sessionID: string) =>
|
||||||
|
evict(
|
||||||
|
pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }),
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchMessages = async (sessionID: string, limit: number, before?: string) => {
|
||||||
|
const response = await retry(() => client.session.messages({ sessionID, limit, before }))
|
||||||
|
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
|
||||||
|
return {
|
||||||
|
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
part: items.map((item) => ({
|
||||||
|
id: item.info.id,
|
||||||
|
part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
})),
|
||||||
|
cursor: response.response.headers.get("x-next-cursor") ?? undefined,
|
||||||
|
complete: !response.response.headers.get("x-next-cursor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMessages = async (sessionID: string, limit: number, before?: string, mode?: "replace" | "prepend") => {
|
||||||
|
if (meta.loading[sessionID]) return
|
||||||
|
const generation = generations.get(sessionID) ?? 0
|
||||||
|
setMeta("loading", sessionID, true)
|
||||||
|
await fetchMessages(sessionID, limit, before)
|
||||||
|
.then((page) => {
|
||||||
|
if ((generations.get(sessionID) ?? 0) !== generation) return
|
||||||
|
const next = mergeOptimisticPage(page, [...(optimistic.get(sessionID)?.values() ?? [])])
|
||||||
|
next.confirmed.forEach((messageID) => clearOptimistic(sessionID, messageID))
|
||||||
|
const messages = mode === "prepend" ? merge(data.message[sessionID] ?? [], next.session) : next.session
|
||||||
|
batch(() => {
|
||||||
|
setData("message", sessionID, reconcile(messages, { key: "id" }))
|
||||||
|
for (const item of next.part) {
|
||||||
|
const parts = item.part.filter((part) => !SKIP_PARTS.has(part.type))
|
||||||
|
if (parts.length) setData("part", item.id, reconcile(parts, { key: "id" }))
|
||||||
|
}
|
||||||
|
setMeta("limit", sessionID, messages.length)
|
||||||
|
setMeta("cursor", sessionID, next.cursor)
|
||||||
|
setMeta("complete", sessionID, next.complete)
|
||||||
|
setMeta("at", sessionID, Date.now())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if ((generations.get(sessionID) ?? 0) === generation) setMeta("loading", sessionID, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sync = (sessionID: string, options?: { force?: boolean; messageLimit?: number }) => {
|
||||||
|
touch(sessionID)
|
||||||
|
return runInflight(inflight, sessionID, async () => {
|
||||||
|
const cached = data.message[sessionID] !== undefined && meta.limit[sessionID] !== undefined
|
||||||
|
if (cached && data.info[sessionID] && !options?.force) return
|
||||||
|
await Promise.all([
|
||||||
|
resolve(sessionID, options),
|
||||||
|
cached && !options?.force
|
||||||
|
? Promise.resolve()
|
||||||
|
: loadMessages(sessionID, options?.messageLimit ?? meta.limit[sessionID] ?? initialMessagePageSize),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefetch = async (sessionID: string, limit: number) => {
|
||||||
|
touch(sessionID)
|
||||||
|
await inflight.get(sessionID)
|
||||||
|
if (
|
||||||
|
Date.now() - (meta.at[sessionID] ?? 0) <= 15_000 &&
|
||||||
|
(meta.complete[sessionID] || (data.message[sessionID]?.length ?? 0) >= limit)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await runInflight(inflight, sessionID, () => loadMessages(sessionID, limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventSessionID = (event: { type: string; properties?: unknown }) => {
|
||||||
|
const properties = event.properties
|
||||||
|
if (!properties || typeof properties !== "object") return
|
||||||
|
if ("sessionID" in properties && typeof properties.sessionID === "string") return properties.sessionID
|
||||||
|
if (
|
||||||
|
"info" in properties &&
|
||||||
|
properties.info &&
|
||||||
|
typeof properties.info === "object" &&
|
||||||
|
"sessionID" in properties.info &&
|
||||||
|
typeof properties.info.sessionID === "string"
|
||||||
|
)
|
||||||
|
return properties.info.sessionID
|
||||||
|
if (
|
||||||
|
"part" in properties &&
|
||||||
|
properties.part &&
|
||||||
|
typeof properties.part === "object" &&
|
||||||
|
"sessionID" in properties.part &&
|
||||||
|
typeof properties.part.sessionID === "string"
|
||||||
|
)
|
||||||
|
return properties.part.sessionID
|
||||||
|
}
|
||||||
|
|
||||||
|
const apply = (event: { type: string; properties?: unknown }) => {
|
||||||
|
const eventID = eventSessionID(event)
|
||||||
|
if (eventID) {
|
||||||
|
touch(eventID)
|
||||||
|
if (!data.info[eventID]) void resolve(eventID).catch(() => {})
|
||||||
|
}
|
||||||
|
switch (event.type) {
|
||||||
|
case "session.created":
|
||||||
|
remember((event.properties as { info: Session }).info)
|
||||||
|
return
|
||||||
|
case "session.updated": {
|
||||||
|
const info = (event.properties as { info: Session }).info
|
||||||
|
remember(info)
|
||||||
|
if (info.time.archived) evict([info.id])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "session.deleted": {
|
||||||
|
const sessionID = (event.properties as { info: Session }).info.id
|
||||||
|
infoSeen.delete(sessionID)
|
||||||
|
setData(
|
||||||
|
"info",
|
||||||
|
produce((draft) => void delete draft[sessionID]),
|
||||||
|
)
|
||||||
|
evict([sessionID])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "session.diff": {
|
||||||
|
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
|
||||||
|
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "todo.updated": {
|
||||||
|
const props = event.properties as { sessionID: string; todos: Todo[] }
|
||||||
|
setData("todo", props.sessionID, reconcile(props.todos, { key: "id" }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "session.status": {
|
||||||
|
const props = event.properties as { sessionID: string; status: SessionStatus }
|
||||||
|
setData("session_status", props.sessionID, reconcile(props.status))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "message.updated": {
|
||||||
|
const info = cleanMessage((event.properties as { info: Message }).info)
|
||||||
|
const messages = data.message[info.sessionID]
|
||||||
|
if (!messages) {
|
||||||
|
setData("message", info.sessionID, [info])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = Binary.search(messages, info.id, (message) => message.id)
|
||||||
|
if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
|
||||||
|
if (!result.found)
|
||||||
|
setData("message", info.sessionID, (value = []) => {
|
||||||
|
const next = value.slice()
|
||||||
|
next.splice(result.index, 0, info)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "message.removed": {
|
||||||
|
const props = event.properties as { sessionID: string; messageID: string }
|
||||||
|
setData(
|
||||||
|
produce((draft) => {
|
||||||
|
const messages = draft.message[props.sessionID]
|
||||||
|
if (messages) {
|
||||||
|
const result = Binary.search(messages, props.messageID, (message) => message.id)
|
||||||
|
if (result.found) messages.splice(result.index, 1)
|
||||||
|
}
|
||||||
|
for (const part of draft.part[props.messageID] ?? []) delete draft.part_text_accum_delta[part.id]
|
||||||
|
delete draft.part[props.messageID]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "message.part.updated": {
|
||||||
|
const part = (event.properties as { part: Part }).part
|
||||||
|
if (SKIP_PARTS.has(part.type)) return
|
||||||
|
setData(
|
||||||
|
"part_text_accum_delta",
|
||||||
|
produce((draft) => void delete draft[part.id]),
|
||||||
|
)
|
||||||
|
const parts = data.part[part.messageID]
|
||||||
|
if (!parts) {
|
||||||
|
setData("part", part.messageID, [part])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = Binary.search(parts, part.id, (item) => item.id)
|
||||||
|
if (result.found) setData("part", part.messageID, result.index, reconcile(part))
|
||||||
|
if (!result.found)
|
||||||
|
setData("part", part.messageID, (value = []) => {
|
||||||
|
const next = value.slice()
|
||||||
|
next.splice(result.index, 0, part)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "message.part.removed": {
|
||||||
|
const props = event.properties as { messageID: string; partID: string }
|
||||||
|
setData(
|
||||||
|
produce((draft) => {
|
||||||
|
delete draft.part_text_accum_delta[props.partID]
|
||||||
|
const parts = draft.part[props.messageID]
|
||||||
|
if (!parts) return
|
||||||
|
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||||
|
if (result.found) parts.splice(result.index, 1)
|
||||||
|
if (parts.length === 0) delete draft.part[props.messageID]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "message.part.delta": {
|
||||||
|
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
||||||
|
const parts = data.part[props.messageID]
|
||||||
|
if (!parts) return
|
||||||
|
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||||
|
if (!result.found) return
|
||||||
|
const field = props.field as keyof (typeof parts)[number]
|
||||||
|
const current = parts[result.index]?.[field]
|
||||||
|
setData(
|
||||||
|
"part_text_accum_delta",
|
||||||
|
props.partID,
|
||||||
|
(value) => (value ?? (typeof current === "string" ? current : "")) + props.delta,
|
||||||
|
)
|
||||||
|
setData(
|
||||||
|
"part",
|
||||||
|
props.messageID,
|
||||||
|
produce((draft) => {
|
||||||
|
if (!draft) return
|
||||||
|
const part = draft[result.index]
|
||||||
|
const field = props.field as keyof typeof part
|
||||||
|
;(part[field] as string) = ((part[field] as string | undefined) ?? "") + props.delta
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "permission.asked": {
|
||||||
|
const permission = event.properties as PermissionRequest
|
||||||
|
const permissions = data.permission[permission.sessionID] ?? []
|
||||||
|
const result = Binary.search(permissions, permission.id, (item) => item.id)
|
||||||
|
if (result.found) setData("permission", permission.sessionID, result.index, reconcile(permission))
|
||||||
|
if (!result.found)
|
||||||
|
setData(
|
||||||
|
"permission",
|
||||||
|
permission.sessionID,
|
||||||
|
produce((draft = []) => void draft.splice(result.index, 0, permission)),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "permission.replied": {
|
||||||
|
const props = event.properties as { sessionID: string; requestID: string }
|
||||||
|
setData(
|
||||||
|
"permission",
|
||||||
|
props.sessionID,
|
||||||
|
produce((draft) => {
|
||||||
|
if (!draft) return
|
||||||
|
const result = Binary.search(draft, props.requestID, (item) => item.id)
|
||||||
|
if (result.found) draft.splice(result.index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "question.asked": {
|
||||||
|
const question = event.properties as QuestionRequest
|
||||||
|
const questions = data.question[question.sessionID] ?? []
|
||||||
|
const result = Binary.search(questions, question.id, (item) => item.id)
|
||||||
|
if (result.found) setData("question", question.sessionID, result.index, reconcile(question))
|
||||||
|
if (!result.found)
|
||||||
|
setData(
|
||||||
|
"question",
|
||||||
|
question.sessionID,
|
||||||
|
produce((draft = []) => void draft.splice(result.index, 0, question)),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "question.replied":
|
||||||
|
case "question.rejected": {
|
||||||
|
const props = event.properties as { sessionID: string; requestID: string }
|
||||||
|
setData(
|
||||||
|
"question",
|
||||||
|
props.sessionID,
|
||||||
|
produce((draft) => {
|
||||||
|
if (!draft) return
|
||||||
|
const result = Binary.search(draft, props.requestID, (item) => item.id)
|
||||||
|
if (result.found) draft.splice(result.index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
set: setData,
|
||||||
|
get: (sessionID: string) => data.info[sessionID],
|
||||||
|
peek: (sessionID: string) => data.info[sessionID],
|
||||||
|
remember,
|
||||||
|
resolve,
|
||||||
|
lineage: {
|
||||||
|
peek: peekLineage,
|
||||||
|
async resolve(sessionID: string) {
|
||||||
|
const session = await resolve(sessionID)
|
||||||
|
return { session, root: await rootSession(session, resolve) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sync,
|
||||||
|
prefetch,
|
||||||
|
shouldPrefetch(sessionID: string, limit: number) {
|
||||||
|
if (data.message[sessionID] === undefined) return true
|
||||||
|
if (Date.now() - (meta.at[sessionID] ?? 0) > 15_000) return true
|
||||||
|
if (meta.complete[sessionID]) return false
|
||||||
|
return (meta.limit[sessionID] ?? 0) <= limit
|
||||||
|
},
|
||||||
|
fresh(sessionID: string, ttl: number) {
|
||||||
|
return Date.now() - (meta.at[sessionID] ?? 0) <= ttl
|
||||||
|
},
|
||||||
|
optimistic: {
|
||||||
|
add(input: { sessionID: string; message: Message; parts: Part[] }) {
|
||||||
|
const items = optimistic.get(input.sessionID)
|
||||||
|
if (items) items.set(input.message.id, input)
|
||||||
|
if (!items) optimistic.set(input.sessionID, new Map([[input.message.id, input]]))
|
||||||
|
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
|
||||||
|
setData(
|
||||||
|
"part",
|
||||||
|
input.message.id,
|
||||||
|
input.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
remove(input: { sessionID: string; messageID: string }) {
|
||||||
|
clearOptimistic(input.sessionID, input.messageID)
|
||||||
|
setData("message", input.sessionID, (messages) => messages?.filter((message) => message.id !== input.messageID))
|
||||||
|
setData(
|
||||||
|
"part",
|
||||||
|
produce((draft) => void delete draft[input.messageID]),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
diff(sessionID: string, options?: { force?: boolean }) {
|
||||||
|
touch(sessionID)
|
||||||
|
if (data.session_diff[sessionID] !== undefined && !options?.force) return Promise.resolve()
|
||||||
|
return runInflight(inflightDiff, sessionID, () => {
|
||||||
|
const generation = generations.get(sessionID) ?? 0
|
||||||
|
return retry(() => client.session.diff({ sessionID })).then((result) => {
|
||||||
|
if ((generations.get(sessionID) ?? 0) !== generation) return
|
||||||
|
setData("session_diff", sessionID, reconcile(cleanDiffs(result.data), { key: "file" }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
todo(sessionID: string, options?: { force?: boolean }) {
|
||||||
|
touch(sessionID)
|
||||||
|
if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve()
|
||||||
|
return runInflight(inflightTodo, sessionID, () => {
|
||||||
|
const generation = generations.get(sessionID) ?? 0
|
||||||
|
return retry(() => client.session.todo({ sessionID })).then((result) => {
|
||||||
|
if ((generations.get(sessionID) ?? 0) !== generation) return
|
||||||
|
setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
more: (sessionID: string) =>
|
||||||
|
data.message[sessionID] !== undefined &&
|
||||||
|
meta.limit[sessionID] !== undefined &&
|
||||||
|
!meta.complete[sessionID] &&
|
||||||
|
!!meta.cursor[sessionID],
|
||||||
|
loading: (sessionID: string) => meta.loading[sessionID] ?? false,
|
||||||
|
async loadMore(sessionID: string, count = historyMessagePageSize) {
|
||||||
|
touch(sessionID)
|
||||||
|
if (meta.loading[sessionID] || meta.complete[sessionID] || !meta.cursor[sessionID]) return
|
||||||
|
await loadMessages(sessionID, count, meta.cursor[sessionID], "prepend")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
evict(sessionID: string) {
|
||||||
|
if (protectedSessions().has(sessionID)) return
|
||||||
|
seen.delete(sessionID)
|
||||||
|
evict([sessionID])
|
||||||
|
},
|
||||||
|
pin(sessionID: string) {
|
||||||
|
pinned.set(sessionID, (pinned.get(sessionID) ?? 0) + 1)
|
||||||
|
touch(sessionID)
|
||||||
|
},
|
||||||
|
unpin(sessionID: string) {
|
||||||
|
const count = pinned.get(sessionID)
|
||||||
|
if (!count || count === 1) pinned.delete(sessionID)
|
||||||
|
if (count && count > 1) pinned.set(sessionID, count - 1)
|
||||||
|
},
|
||||||
|
apply,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerSession = ReturnType<typeof createServerSession>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
|
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse } from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
||||||
@@ -17,8 +17,7 @@ import {
|
|||||||
loadProvidersQuery,
|
loadProvidersQuery,
|
||||||
} from "./global-sync/bootstrap"
|
} from "./global-sync/bootstrap"
|
||||||
import { createChildStoreManager } from "./global-sync/child-store"
|
import { createChildStoreManager } from "./global-sync/child-store"
|
||||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
||||||
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
|
|
||||||
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|
||||||
import { trimSessions } from "./global-sync/session-trim"
|
import { trimSessions } from "./global-sync/session-trim"
|
||||||
import type { ProjectMeta } from "./global-sync/types"
|
import type { ProjectMeta } from "./global-sync/types"
|
||||||
@@ -29,7 +28,8 @@ import { createRefreshQueue } from "./global-sync/queue"
|
|||||||
import { directoryKey } from "./global-sync/utils"
|
import { directoryKey } from "./global-sync/utils"
|
||||||
import { PathKey } from "@/utils/path-key"
|
import { PathKey } from "@/utils/path-key"
|
||||||
import { createDirSyncContext } from "./directory-sync"
|
import { createDirSyncContext } from "./directory-sync"
|
||||||
import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { createRefCountMap } from "@/utils/refcount"
|
import { createRefCountMap } from "@/utils/refcount"
|
||||||
import { useGlobal } from "./global"
|
import { useGlobal } from "./global"
|
||||||
import { ServerConnection, useServer } from "./server"
|
import { ServerConnection, useServer } from "./server"
|
||||||
@@ -37,15 +37,13 @@ import { retry } from "@opencode-ai/core/util/retry"
|
|||||||
import type { ServerScope } from "@/utils/server-scope"
|
import type { ServerScope } from "@/utils/server-scope"
|
||||||
import { persisted } from "@/utils/persist"
|
import { persisted } from "@/utils/persist"
|
||||||
import { toggleMcp } from "./global-sync/mcp"
|
import { toggleMcp } from "./global-sync/mcp"
|
||||||
|
import { createServerSession } from "./server-session"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
error?: InitError
|
error?: InitError
|
||||||
path: Path
|
path: Path
|
||||||
project: Project[]
|
project: Project[]
|
||||||
session_todo: {
|
|
||||||
[sessionID: string]: Todo[]
|
|
||||||
}
|
|
||||||
provider: NormalizedProviderListResponse
|
provider: NormalizedProviderListResponse
|
||||||
provider_auth: ProviderAuthResponse
|
provider_auth: ProviderAuthResponse
|
||||||
config: Config
|
config: Config
|
||||||
@@ -117,7 +115,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
return !bootstrap.isPending
|
return !bootstrap.isPending
|
||||||
},
|
},
|
||||||
project: [],
|
project: [],
|
||||||
session_todo: {},
|
|
||||||
provider_auth: {},
|
provider_auth: {},
|
||||||
get path() {
|
get path() {
|
||||||
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
|
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||||
@@ -187,20 +184,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||||
}) as typeof setGlobalStore
|
}) as typeof setGlobalStore
|
||||||
|
|
||||||
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
|
||||||
if (!sessionID) return
|
|
||||||
if (!todos) {
|
|
||||||
setGlobalStore(
|
|
||||||
"session_todo",
|
|
||||||
produce((draft) => {
|
|
||||||
delete draft[sessionID]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const paused = () => untrack(() => globalStore.reload) !== undefined
|
const paused = () => untrack(() => globalStore.reload) !== undefined
|
||||||
|
|
||||||
const queue = createRefreshQueue({
|
const queue = createRefreshQueue({
|
||||||
@@ -210,6 +193,8 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
bootstrapInstance,
|
bootstrapInstance,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const session = createServerSession(serverSDK.client)
|
||||||
|
|
||||||
const children = createChildStoreManager({
|
const children = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
scope: serverSDK.scope,
|
scope: serverSDK.scope,
|
||||||
@@ -238,7 +223,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
sessionMeta.delete(key)
|
sessionMeta.delete(key)
|
||||||
sdkCache.delete(key)
|
sdkCache.delete(key)
|
||||||
clearProviderRev(serverSDK.scope, key)
|
clearProviderRev(serverSDK.scope, key)
|
||||||
clearSessionPrefetchDirectory(serverSDK.scope, key)
|
|
||||||
},
|
},
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
queryOptions: queryOptionsApi,
|
queryOptions: queryOptionsApi,
|
||||||
@@ -262,11 +246,10 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
if (meta && meta.limit >= retainedLimit) {
|
if (meta && meta.limit >= retainedLimit) {
|
||||||
const next = trimSessions(store.session, {
|
const next = trimSessions(store.session, {
|
||||||
limit: retainedLimit,
|
limit: retainedLimit,
|
||||||
permission: store.permission,
|
permission: session.data.permission,
|
||||||
})
|
})
|
||||||
if (next.length !== store.session.length) {
|
if (next.length !== store.session.length) {
|
||||||
setStore("session", reconcile(next, { key: "id" }))
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
|
|
||||||
}
|
}
|
||||||
children.unpin(key)
|
children.unpin(key)
|
||||||
return
|
return
|
||||||
@@ -289,11 +272,12 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||||
const limit = Math.max(store.limit, options?.limit ?? 0, sessionMeta.get(key)?.limit ?? 0)
|
const limit = Math.max(store.limit, options?.limit ?? 0, sessionMeta.get(key)?.limit ?? 0)
|
||||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||||
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
const next = trimSessions([...nonArchived, ...childSessions], {
|
||||||
limit,
|
limit,
|
||||||
permission: store.permission,
|
permission: session.data.permission,
|
||||||
})
|
})
|
||||||
batch(() => {
|
batch(() => {
|
||||||
|
next.forEach(session.remember)
|
||||||
setStore(
|
setStore(
|
||||||
"sessionTotal",
|
"sessionTotal",
|
||||||
estimateRootSessionTotal({
|
estimateRootSessionTotal({
|
||||||
@@ -302,8 +286,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
limited: x.limited,
|
limited: x.limited,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
setStore("session", reconcile(next, { key: "id" }))
|
||||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
|
||||||
})
|
})
|
||||||
sessionMeta.set(key, { limit })
|
sessionMeta.set(key, { limit })
|
||||||
})
|
})
|
||||||
@@ -357,6 +340,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
loadSessions,
|
loadSessions,
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
queryClient,
|
queryClient,
|
||||||
|
session,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -374,6 +358,8 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
const event = e.details
|
const event = e.details
|
||||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||||
|
|
||||||
|
session.apply(event)
|
||||||
|
|
||||||
if (directory === "global") {
|
if (directory === "global") {
|
||||||
applyGlobalEvent({
|
applyGlobalEvent({
|
||||||
event,
|
event,
|
||||||
@@ -403,8 +389,9 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
store,
|
store,
|
||||||
setStore,
|
setStore,
|
||||||
push: queue.push,
|
push: queue.push,
|
||||||
setSessionTodo,
|
|
||||||
retainedLimit: sessionMeta.get(key)?.limit,
|
retainedLimit: sessionMeta.get(key)?.limit,
|
||||||
|
sessionContent: false,
|
||||||
|
permission: session.data.permission,
|
||||||
vcsCache: children.vcsCache.get(key),
|
vcsCache: children.vcsCache.get(key),
|
||||||
loadLsp: () => {
|
loadLsp: () => {
|
||||||
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
|
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
|
||||||
@@ -478,9 +465,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
|||||||
// bootstrap,
|
// bootstrap,
|
||||||
updateConfig: updateConfigMutation.mutateAsync,
|
updateConfig: updateConfigMutation.mutateAsync,
|
||||||
project: projectApi,
|
project: projectApi,
|
||||||
todo: {
|
session,
|
||||||
set: setSessionTodo,
|
|
||||||
},
|
|
||||||
mcp: {
|
mcp: {
|
||||||
toggle: async (directory: string, name: string) => {
|
toggle: async (directory: string, name: string) => {
|
||||||
const key = directoryKey(directory)
|
const key = directoryKey(directory)
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export interface Settings {
|
|||||||
showReasoningSummaries: boolean
|
showReasoningSummaries: boolean
|
||||||
shellToolPartsExpanded: boolean
|
shellToolPartsExpanded: boolean
|
||||||
editToolPartsExpanded: boolean
|
editToolPartsExpanded: boolean
|
||||||
showSessionProgressBar: boolean
|
|
||||||
showCustomAgents: boolean
|
showCustomAgents: boolean
|
||||||
mobileTitlebarPosition: "top" | "bottom"
|
mobileTitlebarPosition: "top" | "bottom"
|
||||||
newLayoutDesigns?: boolean
|
newLayoutDesigns?: boolean
|
||||||
@@ -117,7 +116,6 @@ const defaultSettings: Settings = {
|
|||||||
showReasoningSummaries: false,
|
showReasoningSummaries: false,
|
||||||
shellToolPartsExpanded: false,
|
shellToolPartsExpanded: false,
|
||||||
editToolPartsExpanded: false,
|
editToolPartsExpanded: false,
|
||||||
showSessionProgressBar: true,
|
|
||||||
showCustomAgents: false,
|
showCustomAgents: false,
|
||||||
mobileTitlebarPosition: "top",
|
mobileTitlebarPosition: "top",
|
||||||
},
|
},
|
||||||
@@ -239,13 +237,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setEditToolPartsExpanded(value: boolean) {
|
setEditToolPartsExpanded(value: boolean) {
|
||||||
setStore("general", "editToolPartsExpanded", value)
|
setStore("general", "editToolPartsExpanded", value)
|
||||||
},
|
},
|
||||||
showSessionProgressBar: withFallback(
|
|
||||||
() => store.general?.showSessionProgressBar,
|
|
||||||
defaultSettings.general.showSessionProgressBar,
|
|
||||||
),
|
|
||||||
setShowSessionProgressBar(value: boolean) {
|
|
||||||
setStore("general", "showSessionProgressBar", value)
|
|
||||||
},
|
|
||||||
showCustomAgents,
|
showCustomAgents,
|
||||||
setShowCustomAgents(value: boolean) {
|
setShowCustomAgents(value: boolean) {
|
||||||
setStore("general", "showCustomAgents", value)
|
setStore("general", "showCustomAgents", value)
|
||||||
|
|||||||
@@ -590,8 +590,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
||||||
"settings.general.row.showSessionProgressBar.title": "إظهار شريط تقدم الجلسة",
|
|
||||||
"settings.general.row.showSessionProgressBar.description": "عرض شريط التقدم المتحرك أعلى الجلسة أثناء عمل الوكيل",
|
|
||||||
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
||||||
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -598,9 +598,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progresso da sessão",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Exibir a barra de progresso animada no topo da sessão quando o agente estiver trabalhando",
|
|
||||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||||
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -663,9 +663,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Prikaži traku napretka sesije",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Prikaži animiranu traku napretka na vrhu sesije kada agent radi",
|
|
||||||
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
||||||
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -657,9 +657,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Vis sessionens fremdriftslinje",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Vis den animerede fremdriftslinje øverst i sessionen, når agenten arbejder",
|
|
||||||
"settings.general.row.wayland.title": "Brug native Wayland",
|
"settings.general.row.wayland.title": "Brug native Wayland",
|
||||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -609,9 +609,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Sitzungsfortschrittsleiste anzeigen",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Die animierte Fortschrittsleiste oben in der Sitzung anzeigen, wenn der Agent arbeitet",
|
|
||||||
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
||||||
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -854,9 +854,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
|
"settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Show edit, write, and patch tool parts expanded by default in the timeline",
|
"Show edit, write, and patch tool parts expanded by default in the timeline",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Display the animated progress bar at the top of the session when the agent is working",
|
|
||||||
"settings.general.row.newLayoutDesigns.title": "New layout and designs",
|
"settings.general.row.newLayoutDesigns.title": "New layout and designs",
|
||||||
"settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI",
|
"settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI",
|
||||||
"settings.general.row.pinchZoom.title": "Pinch to zoom",
|
"settings.general.row.pinchZoom.title": "Pinch to zoom",
|
||||||
|
|||||||
@@ -667,9 +667,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
|
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
|
"Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progreso de la sesión",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Mostrar la barra de progreso animada en la parte superior de la sesión cuando el agente esté trabajando",
|
|
||||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||||
"settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.",
|
"settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -606,9 +606,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
|
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
|
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Afficher la barre de progression de la session",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Afficher la barre de progression animée en haut de la session lorsque l'agent travaille",
|
|
||||||
"settings.general.row.wayland.title": "Utiliser Wayland natif",
|
"settings.general.row.wayland.title": "Utiliser Wayland natif",
|
||||||
"settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.",
|
"settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -595,9 +595,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
|
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
|
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
|
||||||
"settings.general.row.showSessionProgressBar.title": "セッション進行状況バーを表示",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"エージェントの作業中に、セッション上部にアニメーション付きの進行状況バーを表示します",
|
|
||||||
"settings.general.row.wayland.title": "ネイティブWaylandを使用",
|
"settings.general.row.wayland.title": "ネイティブWaylandを使用",
|
||||||
"settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。",
|
"settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -591,9 +591,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기",
|
"settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다",
|
"타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다",
|
||||||
"settings.general.row.showSessionProgressBar.title": "세션 진행 표시줄 표시",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"에이전트가 작업 중일 때 세션 상단에 애니메이션 진행 표시줄을 표시합니다",
|
|
||||||
"settings.general.row.wayland.title": "네이티브 Wayland 사용",
|
"settings.general.row.wayland.title": "네이티브 Wayland 사용",
|
||||||
"settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.",
|
"settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -664,9 +664,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen",
|
"Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Vis fremdriftslinje for sesjonen",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Vis den animerte fremdriftslinjen øverst i sesjonen når agenten jobber",
|
|
||||||
"settings.general.row.wayland.title": "Bruk innebygd Wayland",
|
"settings.general.row.wayland.title": "Bruk innebygd Wayland",
|
||||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.",
|
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -596,9 +596,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
|
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
|
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Pokazuj pasek postępu sesji",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Wyświetlaj animowany pasek postępu u góry sesji, gdy agent pracuje",
|
|
||||||
"settings.general.row.wayland.title": "Użyj natywnego Wayland",
|
"settings.general.row.wayland.title": "Użyj natywnego Wayland",
|
||||||
"settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.",
|
"settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -664,9 +664,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
|
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
|
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Показывать индикатор прогресса сессии",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Показывать анимированный индикатор прогресса вверху сессии, когда агент работает",
|
|
||||||
"settings.general.row.wayland.title": "Использовать нативный Wayland",
|
"settings.general.row.wayland.title": "Использовать нативный Wayland",
|
||||||
"settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.",
|
"settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.",
|
||||||
"settings.general.row.wayland.tooltip":
|
"settings.general.row.wayland.tooltip":
|
||||||
|
|||||||
@@ -655,9 +655,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
||||||
"settings.general.row.showSessionProgressBar.title": "แสดงแถบความคืบหน้าของเซสชัน",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"แสดงแถบความคืบหน้าแบบเคลื่อนไหวที่ด้านบนของเซสชันเมื่อเอเจนต์กำลังทำงาน",
|
|
||||||
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
|
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
|
||||||
"settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท",
|
"settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท",
|
||||||
"settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า",
|
"settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า",
|
||||||
|
|||||||
@@ -671,10 +671,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
|
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
|
||||||
|
|
||||||
"settings.general.row.showSessionProgressBar.title": "Oturum ilerleme çubuğunu göster",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Ajan çalışırken oturumun üst kısmında animasyonlu ilerleme çubuğunu göster",
|
|
||||||
|
|
||||||
"settings.general.row.wayland.title": "Yerel Wayland kullan",
|
"settings.general.row.wayland.title": "Yerel Wayland kullan",
|
||||||
"settings.general.row.wayland.description":
|
"settings.general.row.wayland.description":
|
||||||
"Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.",
|
"Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.",
|
||||||
|
|||||||
@@ -781,9 +781,6 @@ export const dict = {
|
|||||||
"settings.general.row.editToolPartsExpanded.title": "Розгортати частини інструменту редагування",
|
"settings.general.row.editToolPartsExpanded.title": "Розгортати частини інструменту редагування",
|
||||||
"settings.general.row.editToolPartsExpanded.description":
|
"settings.general.row.editToolPartsExpanded.description":
|
||||||
"Показувати частини інструментів редагування, запису та патчів розгорнутими за замовчуванням на часовій шкалі",
|
"Показувати частини інструментів редагування, запису та патчів розгорнутими за замовчуванням на часовій шкалі",
|
||||||
"settings.general.row.showSessionProgressBar.title": "Показувати індикатор прогресу сесії",
|
|
||||||
"settings.general.row.showSessionProgressBar.description":
|
|
||||||
"Відображати анімований індикатор прогресу вгорі сесії, коли агент працює",
|
|
||||||
|
|
||||||
"settings.general.row.wayland.title": "Використовувати нативний Wayland",
|
"settings.general.row.wayland.title": "Використовувати нативний Wayland",
|
||||||
"settings.general.row.wayland.description": "Вимкнути резервний X11 на Wayland. Потребує перезапуску.",
|
"settings.general.row.wayland.description": "Вимкнути резервний X11 на Wayland. Потребує перезапуску.",
|
||||||
|
|||||||
@@ -664,8 +664,6 @@ export const dict = {
|
|||||||
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
||||||
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
||||||
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
|
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
|
||||||
"settings.general.row.showSessionProgressBar.title": "显示会话进度条",
|
|
||||||
"settings.general.row.showSessionProgressBar.description": "当智能体正在工作时,在会话顶部显示动画进度条",
|
|
||||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||||
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
|
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
|
||||||
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
|
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
|
||||||
|
|||||||
@@ -650,8 +650,6 @@ export const dict = {
|
|||||||
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
||||||
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
|
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
|
||||||
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
|
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
|
||||||
"settings.general.row.showSessionProgressBar.title": "顯示工作階段進度列",
|
|
||||||
"settings.general.row.showSessionProgressBar.description": "當代理程式正在運作時,在工作階段頂部顯示動畫進度列",
|
|
||||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||||
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
|
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
|
||||||
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
|
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "@opencode-ai/ui/styles/tailwind";
|
@import "@opencode-ai/ui/styles/tailwind";
|
||||||
|
@import "@opencode-ai/session-ui/styles";
|
||||||
@import "@opencode-ai/ui/v2/styles/tailwind.css";
|
@import "@opencode-ai/ui/v2/styles/tailwind.css";
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -23,44 +24,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@keyframes session-progress-whip {
|
|
||||||
0% {
|
|
||||||
clip-path: inset(0 100% 0 0 round 999px);
|
|
||||||
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
48% {
|
|
||||||
clip-path: inset(0 0 0 0 round 999px);
|
|
||||||
animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
clip-path: inset(0 0 0 100% round 999px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="session-progress"] {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0 0 auto;
|
|
||||||
height: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 220ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="session-progress"][data-state="hiding"] {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="session-progress-bar"] {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 999px;
|
|
||||||
clip-path: inset(0 100% 0 0 round 999px);
|
|
||||||
will-change: clip-path;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="getting-started"] {
|
[data-component="getting-started"] {
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
container-name: getting-started;
|
container-name: getting-started;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/session-ui/context"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, createResource, onCleanup, type ParentProps, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { LocalProvider } from "@/context/local"
|
import { LocalProvider } from "@/context/local"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
@@ -11,7 +11,7 @@ import { decode64 } from "@/utils/base64"
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import type { ServerConnection } from "@/context/server"
|
import type { ServerConnection } from "@/context/server"
|
||||||
import { sessionHref } from "@/utils/session-route"
|
import { sessionHref } from "@/utils/session-route"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
|
|
||||||
export function DirectoryDataProvider(
|
export function DirectoryDataProvider(
|
||||||
props: ParentProps<{
|
props: ParentProps<{
|
||||||
@@ -24,7 +24,7 @@ export function DirectoryDataProvider(
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const global = useGlobal()
|
const serverSync = useServerSync()
|
||||||
const directory = () => (typeof props.directory === "function" ? props.directory() : props.directory)
|
const directory = () => (typeof props.directory === "function" ? props.directory() : props.directory)
|
||||||
const slug = createMemo(() => base64Encode(directory()))
|
const slug = createMemo(() => base64Encode(directory()))
|
||||||
const href = (sessionID: string) => {
|
const href = (sessionID: string) => {
|
||||||
@@ -50,15 +50,18 @@ export function DirectoryDataProvider(
|
|||||||
.catch(() => {}),
|
.catch(() => {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
serverSync().session.pin(sessionID)
|
||||||
|
onCleanup(() => serverSync().session.unpin(sessionID))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataProvider
|
<DataProvider
|
||||||
data={sync().data}
|
data={sync().data}
|
||||||
directory={directory()}
|
directory={directory()}
|
||||||
onNavigateToSession={(sessionID: string) => {
|
onNavigateToSession={(sessionID: string) => navigate(href(sessionID))}
|
||||||
const server = props.server?.()
|
|
||||||
if (server && params.id) global.sessionPlacement.inherit(server, params.id, sessionID)
|
|
||||||
navigate(href(sessionID))
|
|
||||||
}}
|
|
||||||
onSessionHref={href}
|
onSessionHref={href}
|
||||||
>
|
>
|
||||||
<LocalProvider>{props.children}</LocalProvider>
|
<LocalProvider>{props.children}</LocalProvider>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ import { ServerHealthIndicator } from "@/components/server/server-row"
|
|||||||
import { type ServerHealth } from "@/utils/server-health"
|
import { type ServerHealth } from "@/utils/server-health"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { useMarked } from "@opencode-ai/ui/context/marked"
|
import { useMarked } from "@opencode-ai/ui/context/marked"
|
||||||
import { preloadMarkdown } from "@opencode-ai/ui/markdown-cache"
|
import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
|
||||||
|
|
||||||
const HOME_SESSION_LIMIT = 64
|
const HOME_SESSION_LIMIT = 64
|
||||||
const HOME_ROW_LAYOUT =
|
const HOME_ROW_LAYOUT =
|
||||||
@@ -220,10 +220,9 @@ export function NewHome() {
|
|||||||
void directory.session
|
void directory.session
|
||||||
.sync(record.session.id)
|
.sync(record.session.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const store = ctx.sync.child(record.session.directory)[0]
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
(store.message[record.session.id] ?? []).flatMap((message) =>
|
(ctx.sync.session.data.message[record.session.id] ?? []).flatMap((message) =>
|
||||||
(store.part[message.id] ?? []).flatMap((part) => {
|
(ctx.sync.session.data.part[message.id] ?? []).flatMap((part) => {
|
||||||
if (part.type !== "text" || !part.text) return []
|
if (part.type !== "text" || !part.text) return []
|
||||||
return preloadMarkdown(part.text, part.id, marked)
|
return preloadMarkdown(part.text, part.id, marked)
|
||||||
}),
|
}),
|
||||||
@@ -343,12 +342,6 @@ export function NewHome() {
|
|||||||
if (!conn) return
|
if (!conn) return
|
||||||
const directory = project?.worktree ?? session.directory
|
const directory = project?.worktree ?? session.directory
|
||||||
const ctx = global.ensureServerCtx(conn)
|
const ctx = global.ensureServerCtx(conn)
|
||||||
global.sessionPlacement.set({
|
|
||||||
server: ServerConnection.key(conn),
|
|
||||||
leafID: session.id,
|
|
||||||
rootID: session.id,
|
|
||||||
directory: session.directory,
|
|
||||||
})
|
|
||||||
ctx.projects.open(directory)
|
ctx.projects.open(directory)
|
||||||
ctx.projects.touch(directory)
|
ctx.projects.touch(directory)
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default function NewLayout(props: ParentProps) {
|
|||||||
createEffect(() => setV2Toast(true))
|
createEffect(() => setV2Toast(true))
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!notification.ready() || !params.id) return
|
if (!notification.ready() || !params.id) return
|
||||||
|
if (notification.session.unseenCount(params.id) === 0) return
|
||||||
notification.session.markViewed(params.id)
|
notification.session.markViewed(params.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
batch,
|
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
@@ -26,7 +25,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { Session, type Message } from "@opencode-ai/sdk/v2/client"
|
import { Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
@@ -37,16 +36,7 @@ import { toaster } from "@opencode-ai/ui/toast"
|
|||||||
import { setV2Toast, showToast, ToastRegion } from "@/utils/toast"
|
import { setV2Toast, showToast, ToastRegion } from "@/utils/toast"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { clearWorkspaceTerminals } from "@/context/terminal"
|
import { clearWorkspaceTerminals } from "@/context/terminal"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
import { pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
||||||
import {
|
|
||||||
clearSessionPrefetchInflight,
|
|
||||||
clearSessionPrefetch,
|
|
||||||
getSessionPrefetch,
|
|
||||||
isSessionPrefetchCurrent,
|
|
||||||
runSessionPrefetch,
|
|
||||||
setSessionPrefetch,
|
|
||||||
shouldSkipSessionPrefetch,
|
|
||||||
} from "@/context/global-sync/session-prefetch"
|
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
@@ -684,7 +674,6 @@ export default function LegacyLayout(props: ParentProps) {
|
|||||||
serverSDK().url
|
serverSDK().url
|
||||||
|
|
||||||
prefetchToken.value += 1
|
prefetchToken.value += 1
|
||||||
clearSessionPrefetchInflight(serverSDK().scope)
|
|
||||||
prefetchQueues.clear()
|
prefetchQueues.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -712,88 +701,12 @@ export default function LegacyLayout(props: ParentProps) {
|
|||||||
return created
|
return created
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeByID = <T extends { id: string }>(current: T[], incoming: T[]) => {
|
|
||||||
if (current.length === 0) {
|
|
||||||
return incoming.slice().sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = new Map<string, T>()
|
|
||||||
for (const item of current) {
|
|
||||||
map.set(item.id, item)
|
|
||||||
}
|
|
||||||
for (const item of incoming) {
|
|
||||||
map.set(item.id, item)
|
|
||||||
}
|
|
||||||
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
||||||
const [store, setStore] = serverSync().child(directory, { bootstrap: false })
|
await serverSync()
|
||||||
|
.session.prefetch(sessionID, prefetchChunk)
|
||||||
return runSessionPrefetch({
|
.catch(() => {})
|
||||||
scope: serverSDK().scope,
|
if (prefetchToken.value !== token) return
|
||||||
directory,
|
for (const stale of markPrefetched(directory, sessionID)) serverSync().session.evict(stale)
|
||||||
sessionID,
|
|
||||||
task: (rev) =>
|
|
||||||
retry(() => serverSDK().client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
|
||||||
.then((messages) => {
|
|
||||||
if (prefetchToken.value !== token) return
|
|
||||||
if (!isSessionPrefetchCurrent(serverSDK().scope, directory, sessionID, rev)) return
|
|
||||||
|
|
||||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
|
||||||
const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id)
|
|
||||||
const sorted = mergeByID([], next)
|
|
||||||
const stale = markPrefetched(directory, sessionID)
|
|
||||||
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
|
||||||
const meta = {
|
|
||||||
limit: sorted.length,
|
|
||||||
cursor,
|
|
||||||
complete: !cursor,
|
|
||||||
at: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stale.length > 0) {
|
|
||||||
clearSessionPrefetch(serverSDK().scope, directory, stale)
|
|
||||||
for (const id of stale) {
|
|
||||||
serverSync().todo.set(id, undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = store.message[sessionID] ?? []
|
|
||||||
const merged = mergeByID(
|
|
||||||
current.filter((item): item is Message => !!item?.id),
|
|
||||||
sorted,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isSessionPrefetchCurrent(serverSDK().scope, directory, sessionID, rev)) return
|
|
||||||
|
|
||||||
batch(() => {
|
|
||||||
if (stale.length > 0) {
|
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
|
||||||
dropSessionCaches(draft, stale)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setStore("message", sessionID, reconcile(merged, { key: "id" }))
|
|
||||||
setSessionPrefetch({ scope: serverSDK().scope, directory, sessionID, ...meta })
|
|
||||||
|
|
||||||
for (const message of items) {
|
|
||||||
const currentParts = store.part[message.info.id] ?? []
|
|
||||||
const mergedParts = mergeByID(
|
|
||||||
currentParts.filter((item): item is (typeof currentParts)[number] & { id: string } => !!item?.id),
|
|
||||||
message.parts.filter((item): item is (typeof message.parts)[number] & { id: string } => !!item?.id),
|
|
||||||
)
|
|
||||||
|
|
||||||
setStore("part", message.info.id, reconcile(mergedParts, { key: "id" }))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return meta
|
|
||||||
})
|
|
||||||
.catch(() => undefined),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pumpPrefetch = (directory: string) => {
|
const pumpPrefetch = (directory: string) => {
|
||||||
@@ -820,15 +733,7 @@ export default function LegacyLayout(props: ParentProps) {
|
|||||||
const directory = session.directory
|
const directory = session.directory
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
|
|
||||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
const cached = untrack(() => !serverSync().session.shouldPrefetch(session.id, prefetchChunk))
|
||||||
const cached = untrack(() => {
|
|
||||||
const info = getSessionPrefetch(serverSDK().scope, directory, session.id)
|
|
||||||
return shouldSkipSessionPrefetch({
|
|
||||||
message: store.message[session.id] !== undefined,
|
|
||||||
info,
|
|
||||||
chunk: prefetchChunk,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if (cached) return
|
if (cached) return
|
||||||
|
|
||||||
const q = queueFor(directory)
|
const q = queueFor(directory)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function useSessionTabAvatarState(
|
|||||||
const hasPermissions = createMemo(() => {
|
const hasPermissions = createMemo(() => {
|
||||||
if (!active()) return false
|
if (!active()) return false
|
||||||
const [store] = globalSync().child(directory(), { bootstrap: false })
|
const [store] = globalSync().child(directory(), { bootstrap: false })
|
||||||
return !!sessionPermissionRequest(store.session, store.permission, sessionId(), (item) => {
|
return !!sessionPermissionRequest(store.session, globalSync().session.data.permission, sessionId(), (item) => {
|
||||||
return !permission.autoResponds(item, directory())
|
return !permission.autoResponds(item, directory())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -23,8 +23,7 @@ export function useSessionTabAvatarState(
|
|||||||
const loading = createMemo(() => {
|
const loading = createMemo(() => {
|
||||||
if (!active()) return false
|
if (!active()) return false
|
||||||
if (hasPermissions()) return false
|
if (hasPermissions()) return false
|
||||||
const [store] = globalSync().child(directory(), { bootstrap: false })
|
return globalSync().session.data.session_working(sessionId())
|
||||||
return store.session_working(sessionId())
|
|
||||||
})
|
})
|
||||||
return { unread, loading }
|
return { unread, loading }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getProjectAvatarVariant } from "@/context/layout"
|
|||||||
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { SessionProgressIndicatorV2 } from "@opencode-ai/ui/v2/session-progress-indicator-v2"
|
import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2"
|
||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
|
|
||||||
export function SessionTabAvatar(props: {
|
export function SessionTabAvatar(props: {
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ export const ProjectIcon = (props: {
|
|||||||
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
||||||
const hasPermissions = createMemo(() =>
|
const hasPermissions = createMemo(() =>
|
||||||
dirs().some((directory) => {
|
dirs().some((directory) => {
|
||||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
return hasProjectPermissions(serverSync().session.data.permission, (item) => {
|
||||||
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
|
if (serverSync().session.get(item.sessionID)?.directory !== directory) return false
|
||||||
|
return !permission.autoResponds(item, directory)
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
|
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
|
||||||
@@ -151,16 +153,23 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
||||||
const [sessionStore] = serverSync().child(props.session.directory)
|
const [sessionStore] = serverSync().child(props.session.directory)
|
||||||
const hasPermissions = createMemo(() => {
|
const hasPermissions = createMemo(() => {
|
||||||
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
|
return !!sessionPermissionRequest(
|
||||||
return !permission.autoResponds(item, props.session.directory)
|
sessionStore.session,
|
||||||
})
|
serverSync().session.data.permission,
|
||||||
|
props.session.id,
|
||||||
|
(item) => {
|
||||||
|
return !permission.autoResponds(item, props.session.directory)
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
const isWorking = createMemo(() => {
|
const isWorking = createMemo(() => {
|
||||||
if (hasPermissions()) return false
|
if (hasPermissions()) return false
|
||||||
return sessionStore.session_working(props.session.id)
|
return serverSync().session.data.session_working(props.session.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent))
|
const tint = createMemo(() =>
|
||||||
|
messageAgentColor(serverSync().session.data.message[props.session.id], sessionStore.agent),
|
||||||
|
)
|
||||||
const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded()))
|
const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded()))
|
||||||
const currentChild = createMemo(() => {
|
const currentChild = createMemo(() => {
|
||||||
if (!props.showChild) return
|
if (!props.showChild) return
|
||||||
|
|||||||
@@ -304,8 +304,10 @@ export const SortableProject = (props: {
|
|||||||
const projectStore = createMemo(() => serverSync().child(props.project.worktree, { bootstrap: false })[0])
|
const projectStore = createMemo(() => serverSync().child(props.project.worktree, { bootstrap: false })[0])
|
||||||
const isWorking = createMemo(() =>
|
const isWorking = createMemo(() =>
|
||||||
dirs().some((directory) => {
|
dirs().some((directory) => {
|
||||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
return Object.keys(serverSync().session.data.session_status).some((id) => {
|
||||||
return Object.keys(store.session_status).some((id) => store.session_working(id))
|
if (serverSync().session.get(id)?.directory !== directory) return false
|
||||||
|
return serverSync().session.data.session_working(id)
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
|
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
|||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
|
import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
|
||||||
@@ -516,9 +516,7 @@ export default function Page() {
|
|||||||
todoTimer = undefined
|
todoTimer = undefined
|
||||||
if (!id) return
|
if (!id) return
|
||||||
if (status === "idle" && !blocked) return
|
if (status === "idle" && !blocked) return
|
||||||
const cached = untrack(
|
const cached = untrack(() => sync().data.todo[id] !== undefined)
|
||||||
() => sync().data.todo[id] !== undefined || serverSync().data.session_todo[id] !== undefined,
|
|
||||||
)
|
|
||||||
|
|
||||||
todoFrame = requestAnimationFrame(() => {
|
todoFrame = requestAnimationFrame(() => {
|
||||||
todoFrame = undefined
|
todoFrame = undefined
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
const todos = createMemo((): Todo[] => {
|
const todos = createMemo((): Todo[] => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
return serverSync().data.session_todo[id] ?? []
|
return serverSync().session.data.todo[id] ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
const done = createMemo(
|
const done = createMemo(
|
||||||
@@ -111,7 +111,6 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
const clear = () => {
|
const clear = () => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
serverSync().todo.set(id, [])
|
|
||||||
sync().set("todo", id, [])
|
sync().set("todo", id, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { For, Show } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch }
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import type { FileSearchHandle } from "@opencode-ai/ui/file"
|
import type { FileSearchHandle } from "@opencode-ai/session-ui/file"
|
||||||
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
||||||
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
||||||
import { createLineCommentController } from "@opencode-ai/ui/line-comment-annotations"
|
import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations"
|
||||||
import { sampledChecksum } from "@opencode-ai/core/util/encode"
|
import { sampledChecksum } from "@opencode-ai/core/util/encode"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { createEffect, onCleanup, type JSX } from "solid-js"
|
import { createEffect, onCleanup, type JSX } from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/session-ui/session-review"
|
||||||
import type {
|
import type {
|
||||||
SessionReviewCommentActions,
|
SessionReviewCommentActions,
|
||||||
SessionReviewCommentDelete,
|
SessionReviewCommentDelete,
|
||||||
SessionReviewCommentUpdate,
|
SessionReviewCommentUpdate,
|
||||||
} from "@opencode-ai/ui/session-review"
|
} from "@opencode-ai/session-ui/session-review"
|
||||||
import type { SelectedLineRange } from "@/context/file"
|
import type { SelectedLineRange } from "@/context/file"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
Part as MessagePart,
|
Part as MessagePart,
|
||||||
partDefaultOpen,
|
partDefaultOpen,
|
||||||
type UserActions,
|
type UserActions,
|
||||||
} from "@opencode-ai/ui/message-part"
|
} from "@opencode-ai/session-ui/message-part"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
@@ -35,7 +35,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { SessionRetry } from "@opencode-ai/ui/session-retry"
|
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
@@ -51,12 +51,11 @@ import type {
|
|||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||||
import { normalize } from "@opencode-ai/ui/session-diff"
|
import { normalize } from "@opencode-ai/session-ui/session-diff"
|
||||||
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
||||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSessionKey } from "@/pages/session/session-layout"
|
import { useSessionKey } from "@/pages/session/session-layout"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
@@ -95,8 +94,6 @@ const taskDescription = (part: PartType, sessionID: string) => {
|
|||||||
if (typeof value === "string" && value) return value
|
if (typeof value === "string" && value) return value
|
||||||
}
|
}
|
||||||
|
|
||||||
const pace = (width: number) => Math.round(Math.max(1200, Math.min(3200, (Math.max(width, 360) * 2000) / 900)))
|
|
||||||
|
|
||||||
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
|
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
|
||||||
const current = target instanceof Element ? target : undefined
|
const current = target instanceof Element ? target : undefined
|
||||||
const nested = current?.closest("[data-scrollable]")
|
const nested = current?.closest("[data-scrollable]")
|
||||||
@@ -567,18 +564,7 @@ export function MessageTimeline(props: {
|
|||||||
open: false,
|
open: false,
|
||||||
dismiss: null as "escape" | "outside" | null,
|
dismiss: null as "escape" | "outside" | null,
|
||||||
})
|
})
|
||||||
const [bar, setBar] = createStore({
|
|
||||||
ms: pace(640),
|
|
||||||
})
|
|
||||||
let more: HTMLButtonElement | undefined
|
let more: HTMLButtonElement | undefined
|
||||||
let head: HTMLDivElement | undefined
|
|
||||||
|
|
||||||
const updateTitleMetrics = () => {
|
|
||||||
if (!head || head.clientWidth <= 0) return
|
|
||||||
setBar("ms", pace(head.clientWidth))
|
|
||||||
}
|
|
||||||
|
|
||||||
createResizeObserver(() => head, updateTitleMetrics)
|
|
||||||
|
|
||||||
const bindListRoot = (root: HTMLDivElement) => {
|
const bindListRoot = (root: HTMLDivElement) => {
|
||||||
if (root === listRoot()) return
|
if (root === listRoot()) return
|
||||||
@@ -660,14 +646,14 @@ export function MessageTimeline(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shareMutation = useMutation(() => ({
|
const shareMutation = useMutation(() => ({
|
||||||
mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id, directory: sdk().directory }),
|
mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id }),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error("Failed to share session", err)
|
console.error("Failed to share session", err)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const unshareMutation = useMutation(() => ({
|
const unshareMutation = useMutation(() => ({
|
||||||
mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id, directory: sdk().directory }),
|
mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id }),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error("Failed to unshare session", err)
|
console.error("Failed to unshare session", err)
|
||||||
},
|
},
|
||||||
@@ -1291,10 +1277,6 @@ export function MessageTimeline(props: {
|
|||||||
>
|
>
|
||||||
<Show when={showHeader()}>
|
<Show when={showHeader()}>
|
||||||
<div
|
<div
|
||||||
ref={(el) => {
|
|
||||||
head = el
|
|
||||||
updateTitleMetrics()
|
|
||||||
}}
|
|
||||||
data-session-title
|
data-session-title
|
||||||
classList={{
|
classList={{
|
||||||
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
||||||
@@ -1306,17 +1288,6 @@ export function MessageTimeline(props: {
|
|||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={workingStatus() !== "hidden" && settings.general.showSessionProgressBar()}>
|
|
||||||
<div data-component="session-progress" data-state={workingStatus()} aria-hidden="true">
|
|
||||||
<div
|
|
||||||
data-component="session-progress-bar"
|
|
||||||
style={{
|
|
||||||
background: tint() ?? "var(--icon-interactive-base)",
|
|
||||||
animation: `session-progress-whip ${bar.ms}ms infinite`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="h-12 w-full flex items-center justify-between gap-2">
|
<div class="h-12 w-full flex items-center justify-between gap-2">
|
||||||
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||||
<div class="flex items-center min-w-0 grow-1">
|
<div class="flex items-center min-w-0 grow-1">
|
||||||
|
|||||||
@@ -1,37 +1,29 @@
|
|||||||
import type { Message, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { Message, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { createMemo, createResource, onCleanup, untrack, type Accessor } from "solid-js"
|
import { createMemo, createResource, onCleanup, untrack, type Accessor } from "solid-js"
|
||||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
|
|
||||||
const emptyUserMessages: UserMessage[] = []
|
const emptyUserMessages: UserMessage[] = []
|
||||||
|
const sessionFreshness = 15_000
|
||||||
|
|
||||||
export function createTimelineModel(input: {
|
export function createTimelineModel(input: {
|
||||||
sessionID: Accessor<string | undefined>
|
sessionID: Accessor<string | undefined>
|
||||||
revertMessageID: Accessor<string | undefined>
|
revertMessageID: Accessor<string | undefined>
|
||||||
}) {
|
}) {
|
||||||
const sdk = useSDK()
|
const serverSync = useServerSync()
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
let refreshFrame: number | undefined
|
let refreshFrame: number | undefined
|
||||||
let refreshTimer: number | undefined
|
let refreshTimer: number | undefined
|
||||||
|
|
||||||
const [resource] = createResource(
|
const [resource] = createResource(
|
||||||
() => [sdk().directory, input.sessionID()] as const,
|
() => input.sessionID(),
|
||||||
([directory, id]) => {
|
(id) => {
|
||||||
clearRefresh()
|
clearRefresh()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
const cached = untrack(() => sync().data.message[id] !== undefined)
|
const cached = untrack(() => sync().data.message[id] !== undefined)
|
||||||
const stale = cached
|
const stale = cached && !serverSync().session.fresh(id, sessionFreshness)
|
||||||
? (() => {
|
|
||||||
const info = getSessionPrefetch(serverSDK().scope, directory, id)
|
|
||||||
if (!info) return true
|
|
||||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
|
||||||
})()
|
|
||||||
: false
|
|
||||||
|
|
||||||
refreshFrame = requestAnimationFrame(() => {
|
refreshFrame = requestAnimationFrame(() => {
|
||||||
refreshFrame = undefined
|
refreshFrame = undefined
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||||
import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2"
|
import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { groupParts, PartGroup, renderable } from "@opencode-ai/ui/message-part"
|
import { groupParts, PartGroup, renderable } from "@opencode-ai/session-ui/message-part"
|
||||||
import { Data, Equal } from "effect"
|
import { Data, Equal } from "effect"
|
||||||
|
|
||||||
export type SummaryDiff = SnapshotFileDiff & { file: string }
|
export type SummaryDiff = SnapshotFileDiff & { file: string }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
||||||
import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file"
|
import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { ServerConnection } from "@/context/server"
|
|
||||||
import { createSessionPlacementStore } from "./session-placement"
|
|
||||||
|
|
||||||
describe("session placement", () => {
|
|
||||||
const local = ServerConnection.Key.make("http://localhost:4096")
|
|
||||||
const remote = ServerConnection.Key.make("https://example.com")
|
|
||||||
|
|
||||||
test("aliases a leaf and root without crossing servers", () => {
|
|
||||||
const store = createSessionPlacementStore()
|
|
||||||
store.set({ server: local, leafID: "child", rootID: "root", directory: "/repo" })
|
|
||||||
store.set({ server: remote, leafID: "child", rootID: "other", directory: "/remote" })
|
|
||||||
|
|
||||||
expect(store.get(local, "child")).toEqual({ rootID: "root", directory: "/repo" })
|
|
||||||
expect(store.get(local, "root")).toEqual({ rootID: "root", directory: "/repo" })
|
|
||||||
expect(store.get(remote, "child")).toEqual({ rootID: "other", directory: "/remote" })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("inherits known placement for in-app child navigation", () => {
|
|
||||||
const store = createSessionPlacementStore()
|
|
||||||
store.set({ server: local, leafID: "parent", rootID: "root", directory: "/repo" })
|
|
||||||
|
|
||||||
expect(store.inherit(local, "parent", "child")).toEqual({ rootID: "root", directory: "/repo" })
|
|
||||||
expect(store.get(local, "child")).toEqual({ rootID: "root", directory: "/repo" })
|
|
||||||
expect(store.inherit(local, "missing", "unknown")).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("bounds retained placement aliases", () => {
|
|
||||||
const store = createSessionPlacementStore()
|
|
||||||
for (let index = 0; index < 300; index++) {
|
|
||||||
store.set({ server: local, leafID: `leaf-${index}`, rootID: `root-${index}`, directory: `/repo/${index}` })
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(store.size()).toBeLessThanOrEqual(256)
|
|
||||||
expect(store.get(local, "leaf-0")).toBeUndefined()
|
|
||||||
expect(store.get(local, "leaf-299")).toEqual({ rootID: "root-299", directory: "/repo/299" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { ServerConnection } from "@/context/server"
|
|
||||||
|
|
||||||
export type SessionPlacement = {
|
|
||||||
rootID: string
|
|
||||||
directory: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSessionPlacementStore() {
|
|
||||||
const placements = new Map<string, SessionPlacement>()
|
|
||||||
const limit = 256
|
|
||||||
const key = (server: ServerConnection.Key, sessionID: string) => `${server}\0${sessionID}`
|
|
||||||
const write = (id: string, placement: SessionPlacement) => {
|
|
||||||
placements.delete(id)
|
|
||||||
placements.set(id, placement)
|
|
||||||
while (placements.size > limit) placements.delete(placements.keys().next().value!)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
get(server: ServerConnection.Key, sessionID: string) {
|
|
||||||
const id = key(server, sessionID)
|
|
||||||
const placement = placements.get(id)
|
|
||||||
if (placement) write(id, placement)
|
|
||||||
return placement
|
|
||||||
},
|
|
||||||
set(input: SessionPlacement & { server: ServerConnection.Key; leafID: string }) {
|
|
||||||
const placement = { rootID: input.rootID, directory: input.directory }
|
|
||||||
write(key(input.server, input.leafID), placement)
|
|
||||||
write(key(input.server, input.rootID), placement)
|
|
||||||
return placement
|
|
||||||
},
|
|
||||||
inherit(server: ServerConnection.Key, sourceID: string, leafID: string) {
|
|
||||||
const placement = placements.get(key(server, sourceID))
|
|
||||||
if (!placement) return
|
|
||||||
write(key(server, leafID), placement)
|
|
||||||
return placement
|
|
||||||
},
|
|
||||||
size() {
|
|
||||||
return placements.size
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,4 +36,13 @@ describe("session routes", () => {
|
|||||||
}),
|
}),
|
||||||
).toBe(sessions.root)
|
).toBe(sessions.root)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("rejects a parent cycle", async () => {
|
||||||
|
const sessions: Record<string, { id: string; parentID?: string }> = {
|
||||||
|
child: { id: "child", parentID: "parent" },
|
||||||
|
parent: { id: "parent", parentID: "child" },
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(rootSession(sessions.child, async (id) => sessions[id]!)).rejects.toThrow("Session parent cycle: child")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,8 +18,13 @@ export function requireServerKey(segment: string | undefined) {
|
|||||||
|
|
||||||
type SessionParent = { id: string; parentID?: string }
|
type SessionParent = { id: string; parentID?: string }
|
||||||
|
|
||||||
export async function rootSession(session: SessionParent, get: (sessionID: string) => Promise<SessionParent>) {
|
export async function rootSession<T extends SessionParent>(session: T, get: (sessionID: string) => Promise<T>) {
|
||||||
|
const seen = new Set([session.id])
|
||||||
let current = session
|
let current = session
|
||||||
while (current.parentID) current = await get(current.parentID)
|
while (current.parentID) {
|
||||||
|
if (seen.has(current.parentID)) throw new Error(`Session parent cycle: ${current.parentID}`)
|
||||||
|
seen.add(current.parentID)
|
||||||
|
current = await get(current.parentID)
|
||||||
|
}
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export async function handler(
|
|||||||
providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId)
|
providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId)
|
||||||
Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => {
|
Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => {
|
||||||
if (v === "$ip") return headers.set(k, ip)
|
if (v === "$ip") return headers.set(k, ip)
|
||||||
|
if (v === "$caller") return headers.set(k, `caller:${ip}`)
|
||||||
if (v === "$session") return headers.set(k, sessionId)
|
if (v === "$session") return headers.set(k, sessionId)
|
||||||
if (v === "$model") return headers.set(k, model)
|
if (v === "$model") return headers.set(k, model)
|
||||||
if (v === "$request") return headers.set(k, requestId)
|
if (v === "$request") return headers.set(k, requestId)
|
||||||
@@ -354,7 +355,7 @@ export async function handler(
|
|||||||
responseLength += value.length
|
responseLength += value.length
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
const parts = buffer.split(providerInfo.streamSeparator)
|
const parts = buffer.split(/\r\n\r\n|\n\n|\r\r/)
|
||||||
buffer = parts.pop() ?? ""
|
buffer = parts.pop() ?? ""
|
||||||
|
|
||||||
for (let part of parts) {
|
for (let part of parts) {
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
|||||||
return encoder.encode(messages.join(""))
|
return encoder.encode(messages.join(""))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamSeparator: "\n\n",
|
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({
|
|||||||
return body
|
return body
|
||||||
},
|
},
|
||||||
createBinaryStreamDecoder: () => undefined,
|
createBinaryStreamDecoder: () => undefined,
|
||||||
streamSeparator: "\r\n\r\n",
|
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
createBinaryStreamDecoder: () => undefined,
|
createBinaryStreamDecoder: () => undefined,
|
||||||
streamSeparator: "\n\n",
|
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({
|
|||||||
},
|
},
|
||||||
modifyBody: (body: Record<string, any>) => body,
|
modifyBody: (body: Record<string, any>) => body,
|
||||||
createBinaryStreamDecoder: () => undefined,
|
createBinaryStreamDecoder: () => undefined,
|
||||||
streamSeparator: "\n\n",
|
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export type ProviderHelper = (input: {
|
|||||||
modifyHeaders: (headers: Headers, apiKey: string, stickyId: string) => void
|
modifyHeaders: (headers: Headers, apiKey: string, stickyId: string) => void
|
||||||
modifyBody: (body: Record<string, any>) => Record<string, any>
|
modifyBody: (body: Record<string, any>) => Record<string, any>
|
||||||
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
|
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
|
||||||
streamSeparator: string
|
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
parse: (chunk: string) => void
|
parse: (chunk: string) => void
|
||||||
retrieve: () => any
|
retrieve: () => any
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -118,11 +118,12 @@ function toLakeEvent(time: string, data: Record<string, unknown>) {
|
|||||||
request: string(data, "request"),
|
request: string(data, "request"),
|
||||||
client: string(data, "client"),
|
client: string(data, "client"),
|
||||||
user_agent: string(data, "user_agent"),
|
user_agent: string(data, "user_agent"),
|
||||||
|
model: string(data, "model"),
|
||||||
|
model_tier: string(data, "model.tier"),
|
||||||
model_variant: string(data, "model.variant"),
|
model_variant: string(data, "model.variant"),
|
||||||
source: string(data, "source"),
|
source: string(data, "source"),
|
||||||
provider: string(data, "provider"),
|
provider: string(data, "provider"),
|
||||||
provider_model: string(data, "provider.model"),
|
provider_model: string(data, "provider.model"),
|
||||||
model: string(data, "model"),
|
|
||||||
llm_error_code: integer(data, "llm.error.code"),
|
llm_error_code: integer(data, "llm.error.code"),
|
||||||
llm_error_message: string(data, "llm.error.message"),
|
llm_error_message: string(data, "llm.error.message"),
|
||||||
error_response: string(data, "error.response"),
|
error_response: string(data, "error.response"),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.17.9",
|
"version": "1.17.10",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export * as Catalog from "./catalog"
|
export * as Catalog from "./catalog"
|
||||||
|
|
||||||
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
|
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
|
||||||
|
import { Catalog } from "@opencode-ai/schema/catalog"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { ModelRequest } from "./model-request"
|
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Policy } from "./policy"
|
import { Policy } from "./policy"
|
||||||
@@ -18,9 +18,7 @@ export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
|||||||
|
|
||||||
export const PolicyActions = Schema.Literals(["provider.use"])
|
export const PolicyActions = Schema.Literals(["provider.use"])
|
||||||
|
|
||||||
export const Event = {
|
export const Event = Catalog.Event
|
||||||
Updated: EventV2.define({ type: "catalog.updated", schema: {} }),
|
|
||||||
}
|
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
providers: Map<ProviderV2.ID, ProviderRecord>
|
providers: Map<ProviderV2.ID, ProviderRecord>
|
||||||
@@ -86,7 +84,8 @@ export const layer = Layer.effect(
|
|||||||
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
|
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
|
||||||
: model.api
|
: model.api
|
||||||
const request = {
|
const request = {
|
||||||
...ModelRequest.merge({ ...provider.request, generation: {}, options: {} }, model.request),
|
headers: { ...provider.request.headers, ...model.request.headers },
|
||||||
|
body: { ...provider.request.body, ...model.request.body },
|
||||||
variant: model.request.variant,
|
variant: model.request.variant,
|
||||||
}
|
}
|
||||||
return ModelV2.Info.make({
|
return ModelV2.Info.make({
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { define } from "../../plugin/internal"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { ModelRequest } from "../../model-request"
|
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
@@ -59,15 +58,11 @@ export const Plugin = define({
|
|||||||
Object.assign(provider.request.body, item.request.body)
|
Object.assign(provider.request.body, item.request.body)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const providerApi = catalog.provider.get(providerID)?.provider.api
|
|
||||||
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
|
|
||||||
|
|
||||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||||
catalog.model.update(providerID, id, (model) => {
|
catalog.model.update(providerID, id, (model) => {
|
||||||
if (config.family !== undefined) model.family = config.family
|
if (config.family !== undefined) model.family = config.family
|
||||||
if (config.name !== undefined) model.name = config.name
|
if (config.name !== undefined) model.name = config.name
|
||||||
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
|
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
|
||||||
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
|
|
||||||
if (config.capabilities !== undefined) {
|
if (config.capabilities !== undefined) {
|
||||||
model.capabilities = {
|
model.capabilities = {
|
||||||
tools: config.capabilities.tools,
|
tools: config.capabilities.tools,
|
||||||
@@ -76,10 +71,8 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config.request !== undefined) {
|
if (config.request !== undefined) {
|
||||||
ModelRequest.assign(model.request, {
|
Object.assign(model.request.headers, config.request.headers)
|
||||||
headers: config.request.headers,
|
Object.assign(model.request.body, config.request.body)
|
||||||
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
|
|
||||||
})
|
|
||||||
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
||||||
}
|
}
|
||||||
if (config.variants !== undefined) {
|
if (config.variants !== undefined) {
|
||||||
@@ -90,15 +83,11 @@ export const Plugin = define({
|
|||||||
id: variant.id,
|
id: variant.id,
|
||||||
headers: {},
|
headers: {},
|
||||||
body: {},
|
body: {},
|
||||||
generation: {},
|
|
||||||
options: {},
|
|
||||||
}
|
}
|
||||||
model.variants.push(existing)
|
model.variants.push(existing)
|
||||||
}
|
}
|
||||||
ModelRequest.assign(existing, {
|
Object.assign(existing.headers, variant.headers)
|
||||||
headers: variant.headers,
|
Object.assign(existing.body, variant.body)
|
||||||
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config.cost !== undefined) {
|
if (config.cost !== undefined) {
|
||||||
|
|||||||
@@ -84,15 +84,20 @@ export const layer = Layer.effect(
|
|||||||
return yield* new DestinationProjectMismatchError({ expected: current.projectID, actual: destination.id })
|
return yield* new DestinationProjectMismatchError({ expected: current.projectID, actual: destination.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
const patch =
|
const moveChanges = input.moveChanges && source.directory !== destination.directory
|
||||||
input.moveChanges && source.directory !== destination.directory
|
const sourceRepository = moveChanges ? yield* git.repo.discover(current.location.directory) : undefined
|
||||||
? yield* git
|
if (moveChanges && !sourceRepository)
|
||||||
.patch(current.location.directory)
|
return yield* new CaptureChangesError({ message: "Source is not a Git repository" })
|
||||||
.pipe(Effect.mapError((error) => new CaptureChangesError({ message: error.message })))
|
const patch = sourceRepository
|
||||||
: ""
|
? yield* git.change
|
||||||
|
.capture({ repository: sourceRepository, path: current.location.directory })
|
||||||
|
.pipe(Effect.mapError((error) => new CaptureChangesError({ message: error.message })))
|
||||||
|
: Git.ChangeSet.make("")
|
||||||
if (patch) {
|
if (patch) {
|
||||||
yield* git
|
const repository = yield* git.repo.discover(directory)
|
||||||
.applyPatch({ directory, patch })
|
if (!repository) return yield* new ApplyChangesError({ message: "Destination is not a Git repository" })
|
||||||
|
yield* git.change
|
||||||
|
.apply({ repository, path: directory, changes: patch })
|
||||||
.pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message })))
|
.pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message })))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,16 +109,29 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (patch) {
|
if (patch) {
|
||||||
yield* git.softResetChanges(current.location.directory).pipe(
|
const repository = yield* git.repo.discover(current.location.directory)
|
||||||
Effect.mapError(
|
if (!repository)
|
||||||
(error) =>
|
return yield* new ResetSourceChangesError({
|
||||||
new ResetSourceChangesError({
|
directory: current.location.directory,
|
||||||
directory: current.location.directory,
|
message: "Source is not a Git repository",
|
||||||
message: error.message,
|
})
|
||||||
cause: error.cause,
|
yield* git.change
|
||||||
}),
|
.discard({
|
||||||
),
|
repository,
|
||||||
)
|
path: current.location.directory,
|
||||||
|
index: "preserve",
|
||||||
|
untracked: "remove",
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
Effect.mapError(
|
||||||
|
(error) =>
|
||||||
|
new ResetSourceChangesError({
|
||||||
|
directory: current.location.directory,
|
||||||
|
message: error.message,
|
||||||
|
cause: error.cause,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+23
-94
@@ -1,46 +1,19 @@
|
|||||||
export * as EventV2 from "./event"
|
export * as EventV2 from "./event"
|
||||||
|
|
||||||
import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
|
import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
|
||||||
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
|
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
||||||
import { and, asc, eq, gt } from "drizzle-orm"
|
import { and, asc, eq, gt } from "drizzle-orm"
|
||||||
import { Database } from "./database/database"
|
import { Database } from "./database/database"
|
||||||
import { EventSequenceTable, EventTable } from "./event/sql"
|
import { EventSequenceTable, EventTable } from "./event/sql"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { withStatics } from "./schema"
|
|
||||||
import { Identifier } from "./util/identifier"
|
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { LayerNode } from "./effect/layer-node"
|
||||||
import { isDeepStrictEqual } from "node:util"
|
import { isDeepStrictEqual } from "node:util"
|
||||||
|
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
|
||||||
|
|
||||||
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
|
export const ID = Event.ID
|
||||||
Schema.brand("Event.ID"),
|
export type ID = import("@opencode-ai/schema/event").ID
|
||||||
withStatics((schema) => ({
|
export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
||||||
create: () => schema.make("evt_" + Identifier.ascending()),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
export type ID = typeof ID.Type
|
|
||||||
|
|
||||||
export type Definition<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
|
|
||||||
readonly type: Type
|
|
||||||
readonly durable?: {
|
|
||||||
readonly version: number
|
|
||||||
readonly aggregate: string
|
|
||||||
}
|
|
||||||
readonly data: DataSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
|
|
||||||
|
|
||||||
export type Payload<D extends Definition = Definition> = {
|
|
||||||
readonly id: ID
|
|
||||||
readonly type: D["type"]
|
|
||||||
readonly data: Data<D>
|
|
||||||
readonly durable?: {
|
|
||||||
readonly aggregateID: string
|
|
||||||
readonly seq: number
|
|
||||||
readonly version: number
|
|
||||||
}
|
|
||||||
readonly location?: Location.Ref
|
|
||||||
readonly metadata?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
||||||
export type Unsubscribe = Effect.Effect<void>
|
export type Unsubscribe = Effect.Effect<void>
|
||||||
@@ -74,52 +47,8 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
|
|||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
export function versionedType(type: string, version: number) {
|
export const define = Event.define
|
||||||
return `${type}.${version}`
|
export const versionedType = Event.versionedType
|
||||||
}
|
|
||||||
|
|
||||||
export const registry = new Map<string, Definition>()
|
|
||||||
const durableRegistry = new Map<string, Definition>()
|
|
||||||
|
|
||||||
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
|
|
||||||
readonly type: Type
|
|
||||||
readonly durable?: {
|
|
||||||
readonly version: number
|
|
||||||
readonly aggregate: string
|
|
||||||
}
|
|
||||||
readonly schema: Fields
|
|
||||||
}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
|
|
||||||
const Data = Schema.Struct(input.schema)
|
|
||||||
const Payload = Schema.Struct({
|
|
||||||
id: ID,
|
|
||||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
||||||
type: Schema.Literal(input.type),
|
|
||||||
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number })),
|
|
||||||
location: Schema.optional(Location.Ref),
|
|
||||||
data: Data,
|
|
||||||
}).annotate({ identifier: input.type })
|
|
||||||
|
|
||||||
const definition = Object.assign(Payload, {
|
|
||||||
type: input.type,
|
|
||||||
...(input.durable === undefined ? {} : { durable: input.durable }),
|
|
||||||
data: Data,
|
|
||||||
})
|
|
||||||
const existing = registry.get(input.type)
|
|
||||||
if (
|
|
||||||
input.durable === undefined ||
|
|
||||||
existing?.durable === undefined ||
|
|
||||||
input.durable.version >= existing.durable.version
|
|
||||||
) {
|
|
||||||
registry.set(input.type, definition)
|
|
||||||
}
|
|
||||||
if (input.durable) durableRegistry.set(versionedType(input.type, input.durable.version), definition)
|
|
||||||
return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
|
|
||||||
Definition<Type, Schema.Struct<Fields>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function definitions() {
|
|
||||||
return registry.values().toArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublishOptions {
|
export interface PublishOptions {
|
||||||
readonly id?: ID
|
readonly id?: ID
|
||||||
@@ -169,6 +98,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
typed: new Map<string, PubSub.PubSub<Payload>>(),
|
typed: new Map<string, PubSub.PubSub<Payload>>(),
|
||||||
}
|
}
|
||||||
const projectors = new Map<string, Subscriber[]>()
|
const projectors = new Map<string, Subscriber[]>()
|
||||||
|
// TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads.
|
||||||
const listeners = new Array<Subscriber>()
|
const listeners = new Array<Subscriber>()
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
|
|
||||||
@@ -194,6 +124,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
function commitDurableEvent(
|
function commitDurableEvent(
|
||||||
|
definition: Definition,
|
||||||
event: Payload,
|
event: Payload,
|
||||||
input?: {
|
input?: {
|
||||||
readonly seq: number
|
readonly seq: number
|
||||||
@@ -204,7 +135,6 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
commit?: (seq: number) => Effect.Effect<void>,
|
commit?: (seq: number) => Effect.Effect<void>,
|
||||||
) {
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = registry.get(event.type)
|
|
||||||
const durable = definition?.durable
|
const durable = definition?.durable
|
||||||
if (durable) {
|
if (durable) {
|
||||||
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
|
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
|
||||||
@@ -238,9 +168,10 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const latest = row?.seq ?? -1
|
const latest = row?.seq ?? -1
|
||||||
const encoded = Schema.encodeUnknownSync(
|
const encoded = Schema.encodeUnknownSync(definition.data)(event.data) as Record<
|
||||||
definition.data as Schema.Codec<unknown, unknown, never, never>,
|
string,
|
||||||
)(event.data) as Record<string, unknown>
|
unknown
|
||||||
|
>
|
||||||
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
|
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidDurableEventError({
|
||||||
@@ -356,9 +287,8 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) {
|
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = registry.get(event.type)
|
|
||||||
if (!definition?.durable && commit)
|
if (!definition?.durable && commit)
|
||||||
return yield* Effect.die(
|
return yield* Effect.die(
|
||||||
new InvalidDurableEventError({
|
new InvalidDurableEventError({
|
||||||
@@ -367,7 +297,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (definition?.durable) {
|
if (definition?.durable) {
|
||||||
const committed = yield* commitDurableEvent(event as Payload, undefined, commit)
|
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
|
||||||
if (committed) {
|
if (committed) {
|
||||||
event = {
|
event = {
|
||||||
...event,
|
...event,
|
||||||
@@ -416,6 +346,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||||
: undefined)
|
: undefined)
|
||||||
return yield* publishEvent(
|
return yield* publishEvent(
|
||||||
|
definition,
|
||||||
{
|
{
|
||||||
id: options?.id ?? ID.create(),
|
id: options?.id ?? ID.create(),
|
||||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||||
@@ -433,7 +364,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
||||||
) {
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = durableRegistry.get(event.type)
|
const definition = Durable.get(event.type)
|
||||||
if (!definition?.durable) {
|
if (!definition?.durable) {
|
||||||
yield* Effect.die(
|
yield* Effect.die(
|
||||||
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
|
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
|
||||||
@@ -442,11 +373,9 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
const payload = {
|
const payload = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(
|
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||||
event.data,
|
|
||||||
),
|
|
||||||
} as Payload
|
} as Payload
|
||||||
const committed = yield* commitDurableEvent(payload, {
|
const committed = yield* commitDurableEvent(definition, payload, {
|
||||||
seq: event.seq,
|
seq: event.seq,
|
||||||
aggregateID: event.aggregateID,
|
aggregateID: event.aggregateID,
|
||||||
ownerID: options?.ownerID,
|
ownerID: options?.ownerID,
|
||||||
@@ -530,8 +459,8 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
|
|
||||||
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
|
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
|
||||||
|
|
||||||
const decodeSerializedEvent = (event: SerializedEvent): Payload => {
|
const decodeSerializedEvent = (event: SerializedEvent) => {
|
||||||
const definition = durableRegistry.get(event.type)
|
const definition = Durable.get(event.type)
|
||||||
if (!definition?.durable) {
|
if (!definition?.durable) {
|
||||||
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
|
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
|
||||||
}
|
}
|
||||||
@@ -539,7 +468,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
id: event.id,
|
id: event.id,
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
|
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
|
||||||
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(event.data),
|
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * as File from "./file"
|
||||||
|
|
||||||
|
import { Revert } from "@opencode-ai/schema/revert"
|
||||||
|
|
||||||
|
export const Diff = Revert.FileDiff
|
||||||
|
export type Diff = typeof Diff.Type
|
||||||
@@ -2,12 +2,11 @@ export * as FileSystem from "./filesystem"
|
|||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { EventV2 } from "./event"
|
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { PositiveInt, RelativePath } from "./schema"
|
import { PositiveInt, RelativePath } from "./schema"
|
||||||
import { FileSystemSearch } from "./filesystem/search"
|
import { FileSystemSearch } from "./filesystem/search"
|
||||||
import { Entry, Match } from "@opencode-ai/schema/filesystem"
|
import { Entry, FileSystem, FindInput, Match } from "@opencode-ai/schema/filesystem"
|
||||||
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
|
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
|
||||||
|
|
||||||
export const ReadInput = Schema.Struct({
|
export const ReadInput = Schema.Struct({
|
||||||
@@ -29,11 +28,7 @@ export const ListInput = Schema.Struct({
|
|||||||
})
|
})
|
||||||
export type ListInput = typeof ListInput.Type
|
export type ListInput = typeof ListInput.Type
|
||||||
|
|
||||||
export class FindInput extends Schema.Class<FindInput>("FileSystem.FindInput")({
|
export { FindInput }
|
||||||
query: Schema.String,
|
|
||||||
type: Schema.Literals(["file", "directory"]).pipe(Schema.optional),
|
|
||||||
limit: PositiveInt.pipe(Schema.optional),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class GlobInput extends Schema.Class<GlobInput>("FileSystem.GlobInput")({
|
export class GlobInput extends Schema.Class<GlobInput>("FileSystem.GlobInput")({
|
||||||
pattern: Schema.String,
|
pattern: Schema.String,
|
||||||
@@ -48,14 +43,7 @@ export class GrepInput extends Schema.Class<GrepInput>("FileSystem.GrepInput")({
|
|||||||
limit: PositiveInt.pipe(Schema.optional),
|
limit: PositiveInt.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export const Event = {
|
export const Event = FileSystem.Event
|
||||||
Edited: EventV2.define({
|
|
||||||
type: "file.edited",
|
|
||||||
schema: {
|
|
||||||
file: Schema.String,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
|
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
|
||||||
|
|||||||
@@ -127,12 +127,19 @@ export const fffLayer = Layer.effect(
|
|||||||
Fff.create({
|
Fff.create({
|
||||||
basePath: location.directory,
|
basePath: location.directory,
|
||||||
aiMode: true,
|
aiMode: true,
|
||||||
enableFsRootScanning: true,
|
|
||||||
enableHomeDirScanning: true,
|
|
||||||
}),
|
}),
|
||||||
catch: (cause) => cause,
|
catch: (cause) => cause,
|
||||||
}).pipe(Effect.orDie)
|
}).pipe(
|
||||||
if (!result.ok) return yield* Effect.die(result.error)
|
Effect.catch((error) => Effect.logWarning("failed to initialize fff", { error }).pipe(Effect.as(undefined))),
|
||||||
|
)
|
||||||
|
if (!result?.ok) {
|
||||||
|
if (result) yield* Effect.logWarning("failed to initialize fff", { error: result.error })
|
||||||
|
return Service.of({
|
||||||
|
find: () => Effect.succeed([]),
|
||||||
|
glob: () => Effect.succeed([]),
|
||||||
|
grep: () => Effect.succeed([]),
|
||||||
|
})
|
||||||
|
}
|
||||||
yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore))
|
yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore))
|
||||||
return Service.of({
|
return Service.of({
|
||||||
glob: (input) =>
|
glob: (input) =>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ export * as Watcher from "./watcher"
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||||
import type ParcelWatcher from "@parcel/watcher"
|
import type ParcelWatcher from "@parcel/watcher"
|
||||||
import { Cause, Context, Effect, Layer, Schema } from "effect"
|
import { Cause, Context, Effect, Layer } from "effect"
|
||||||
|
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Config } from "../config"
|
import { Config } from "../config"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
@@ -19,15 +20,7 @@ declare const OPENCODE_LIBC: string | undefined
|
|||||||
|
|
||||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||||
|
|
||||||
export const Event = {
|
export const Event = FileSystemWatcher.Event
|
||||||
Updated: EventV2.define({
|
|
||||||
type: "file.watcher.updated",
|
|
||||||
schema: {
|
|
||||||
file: Schema.String,
|
|
||||||
event: Schema.Literals(["add", "change", "unlink"]),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
||||||
try {
|
try {
|
||||||
@@ -119,7 +112,7 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (location.vcs?.type === "git") {
|
if (location.vcs?.type === "git") {
|
||||||
const resolved = yield* git.dir(location.directory)
|
const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory
|
||||||
const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
|
const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
|
||||||
if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
|
if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
|
||||||
const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
|
const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
|
||||||
|
|||||||
+741
-198
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user