mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-02 08:25:00 -04:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8aa4bfc04 | |||
| 0ee7cfa1fe | |||
| a261b55e43 | |||
| a136caa1b3 | |||
| 7a4d18390a | |||
| edbe2280a5 | |||
| d721fc0a49 | |||
| dc985ff881 | |||
| a468680965 | |||
| 0c0d193474 | |||
| b278e49e96 | |||
| c613c33787 | |||
| 9431356715 | |||
| f6197cefe1 | |||
| 7f33576f46 | |||
| 7ae856a9e9 | |||
| b01eb22b2c | |||
| 3cf1cef7fe | |||
| b375890745 | |||
| 0a36433062 | |||
| 5426478e46 | |||
| 3003867c25 | |||
| 8c0edca175 | |||
| 1af8dafd3e | |||
| c47cb28781 | |||
| 2859ce6e73 | |||
| 39babcbb53 | |||
| 64dc6d39ab | |||
| 17ba5539e7 | |||
| 9211ef7e95 | |||
| 605ae48c6f | |||
| 75557000de | |||
| 773d33e282 | |||
| 46e9863589 | |||
| 47f3d8b709 | |||
| 134f4136da | |||
| f011d77128 | |||
| ab5a12d916 | |||
| 76ecf2e58c | |||
| 057958c933 | |||
| b1a7ee5695 | |||
| 107180701f | |||
| e464999eb9 | |||
| cc9b73b0bd | |||
| fe5ac304b5 | |||
| ba1b660d57 | |||
| e45e0e1125 | |||
| 5b14fb4a1b | |||
| 730ea6d2e3 | |||
| 21a0fdd4c0 | |||
| cb65926c82 |
@@ -112,7 +112,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist/lildax-*
|
||||
path: packages/cli/dist/cli-*
|
||||
|
||||
outputs:
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
|
||||
+38
-27
@@ -8,12 +8,15 @@ OpenCode sessions preserve durable conversational history while assembling the r
|
||||
The structured collection of contextual facts presented to the model as initial instructions and chronological updates.
|
||||
_Avoid_: System prompt
|
||||
|
||||
**Context Component**:
|
||||
One independently loaded fact within the **System Context**, represented by a stable key and one effectfully loaded baseline/update rendering.
|
||||
**Context Source**:
|
||||
One independently observed typed value within the **System Context**, represented by a stable key, JSON codec, infallible loader, pure baseline/update renderers, and an optional removal renderer for dynamic sources.
|
||||
_Avoid_: Prompt fragment
|
||||
|
||||
**System Context Registry**:
|
||||
The Location-scoped registry of ordered, scoped producers that contribute to the current **System Context**.
|
||||
|
||||
**Mid-Conversation System Message**:
|
||||
A durable chronological instruction that tells the model the newly effective state of a changed **Context Component**.
|
||||
A durable chronological instruction that tells the model the newly effective state of a changed **Context Source**.
|
||||
_Avoid_: System update, system notification, raw text diff
|
||||
|
||||
**Context Epoch**:
|
||||
@@ -23,48 +26,57 @@ The span during which one initially rendered **System Context** remains immutabl
|
||||
The full **System Context** rendered at the start of a **Context Epoch**.
|
||||
_Avoid_: Live system prompt
|
||||
|
||||
**Context Checkpoint**:
|
||||
The durable model-hidden comparison state used to detect which **Context Components** changed since context was last admitted to a provider turn.
|
||||
**Context Snapshot**:
|
||||
The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a provider turn.
|
||||
|
||||
**Unavailable Context**:
|
||||
An expected temporary inability to load a **Context Component** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded.
|
||||
An expected temporary inability to observe a **Context Source** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded.
|
||||
|
||||
**Safe Provider-Turn Boundary**:
|
||||
The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
|
||||
|
||||
## Relationships
|
||||
|
||||
- A **System Context** contains one or more **Context Components**.
|
||||
- A changed **Context Component** may produce one **Mid-Conversation System Message** containing its newly effective state.
|
||||
- A **Mid-Conversation System Message** persists its originating **Context Component** key and the exact rendered text sent to the model.
|
||||
- A **Context Checkpoint** advances atomically with the corresponding durable **Mid-Conversation System Message**.
|
||||
- A **Context Checkpoint** stores one rendered-content hash per stable **Context Component** key so core and plugin-defined components can evolve independently.
|
||||
- Changes from multiple **Context Components** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
|
||||
- A **System Context** is an opaque carrier composed from zero or more **Context Sources**.
|
||||
- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Provider-Turn Boundary**.
|
||||
- A changed **Context Source** may produce one **Mid-Conversation System Message** containing its newly effective state.
|
||||
- A **Mid-Conversation System Message** persists the exact combined rendered text sent to the model.
|
||||
- The current **Context Snapshot** advances atomically with the corresponding durable **Mid-Conversation System Message**.
|
||||
- A **Context Snapshot** stores one codec-encoded JSON value and, for removable dynamic sources, a pre-rendered removal message per stable **Context Source** key.
|
||||
- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
|
||||
- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes.
|
||||
- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
|
||||
- The first provider turn renders the latest **Baseline System Context** and initializes its **Context Checkpoint** without emitting a redundant **Mid-Conversation System Message**.
|
||||
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Checkpoint**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
|
||||
- A **Context Checkpoint** is an evolvable component map; a newly registered core or plugin-defined **Context Component** absent from an existing checkpoint emits its current state once at the next **Safe Provider-Turn Boundary**.
|
||||
- **Context Component** keys are stable and namespaced; duplicate keys fail assembly. Built-in components preserve declaration order and plugin-defined components append in lexicographic key order so rendered context is deterministic.
|
||||
- Each **Context Component** loader returns its model-visible baseline string and absolute current-state update string from one coherent sample; the update string is hashed for change detection.
|
||||
- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline.
|
||||
- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion.
|
||||
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
|
||||
- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Provider-Turn Boundary**.
|
||||
- **Context Source** keys are stable and namespaced; duplicate keys fail composition. `SystemContext.combine(...)` preserves caller order; the **System Context Registry** evaluates producers concurrently and combines them in stable contribution-key order so rendered context remains deterministic.
|
||||
- Each **Context Source** loader returns one coherent typed value. `SystemContext.make(...)` hides that value type so differently typed sources compose uniformly. Its codec compares and stores that value; its pure renderers produce model-visible baseline, update, and removal text only when needed.
|
||||
- `SystemContext.initialize(...)` observes a composed **System Context** once and produces a fresh **Baseline System Context** with its **Context Snapshot**.
|
||||
- `SystemContext.reconcile(...)` observes a composed **System Context** once and returns exactly one next action: unchanged, updated, replacement ready, or replacement blocked.
|
||||
- `SystemContext.replace(...)` represents an explicit baseline-replacing transition such as compaction or model/provider switch; it either produces a fresh generation or reports that replacement is blocked by unavailable admitted context.
|
||||
- Context Epoch preparation retries until stable after optimistic revision mismatches so concurrent replacement requests cannot terminate an otherwise valid safe-boundary run.
|
||||
- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text.
|
||||
- Ordinary **Context Component** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**.
|
||||
- Nested project instruction files discovered while reading join the effective instructions returned by the instruction service and are admitted durably at the next **Safe Provider-Turn Boundary**.
|
||||
- A discovered nested project instruction remains active for the session while it stays in the same location and is folded into later **Baseline System Contexts** after compaction.
|
||||
- Ordinary **Context Source** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**.
|
||||
- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Provider-Turn Boundary**.
|
||||
- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location.
|
||||
- Moving a Session clears its active **Context Epoch**, so the destination must initialize a complete baseline before another prompt can promote.
|
||||
- Context Epoch initialization is fenced against the authoritative Session Location, so an old-Location runner cannot recreate source context after a concurrent move.
|
||||
- Instruction discovery, source identity, persistence, and file loading belong to the instruction service; the **System Context** abstraction only composes effectful producers and renders loaded values.
|
||||
- Plugin-defined **Context Components** register through a scoped replayable registry so plugin hot reload adds and removes components predictably.
|
||||
- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Provider-Turn Boundary**.
|
||||
- Built-in and instruction context producers register through the **System Context Registry** with stable contribution keys. Plugin-defined context registration and hot-reload lifecycle remain a follow-up built on the same scoped registry seam.
|
||||
- Context source changes never wake idle sessions; the next naturally scheduled **Safe Provider-Turn Boundary** loads and compares current values lazily.
|
||||
- Once admitted, a **Mid-Conversation System Message** remains durable even if the following provider attempt fails and is replayed unchanged on retry.
|
||||
- **Mid-Conversation System Messages** remain durable model-projection history but are hidden from normal user-facing transcript surfaces.
|
||||
- The date **Context Component** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later.
|
||||
- **Mid-Conversation System Messages** remain durable Session-message history; normal user-facing transcript surfaces may hide them.
|
||||
- The date **Context Source** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later.
|
||||
- A **Context Epoch** begins with one immutable **Baseline System Context**.
|
||||
- A **Baseline System Context** is stored durably and reused verbatim across process restarts within its **Context Epoch**.
|
||||
- A **Baseline System Context** durably preserves deterministic keyed top-level component strings rather than eagerly joining all text; request assembly lowers them into canonical LLM system parts.
|
||||
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
|
||||
- Compaction or a model/provider switch starts a new **Context Epoch** because the baseline can be replaced without preserving the prior provider cache.
|
||||
- A model/provider switch always starts a new **Context Epoch** while preserving chronological conversation history.
|
||||
- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
|
||||
- When an effective instruction file changes, its **Mid-Conversation System Message** includes the complete current contents and supersedes the prior version from that source; when it is removed, the message states that it no longer applies.
|
||||
- When the effective aggregate instruction set changes, its **Mid-Conversation System Message** includes the complete current ordered set and supersedes the prior aggregate value; when no ambient instructions remain, the message states that previously loaded instructions no longer apply.
|
||||
- Ambient project instruction discovery honors `OPENCODE_DISABLE_PROJECT_CONFIG`; global instructions remain eligible.
|
||||
|
||||
## Example dialogue
|
||||
|
||||
@@ -73,5 +85,4 @@ The point immediately before a provider call, after durable input promotion and
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- Legacy `experimental.chat.system.transform` can mutate the assembled baseline system prompt arbitrarily, but V2 plugins do not yet expose an equivalent hook. Decide separately whether to port it, replace dynamic uses with plugin-defined **Context Components**, or narrow its semantics.
|
||||
- A location change likely starts a new **Context Epoch** so location-dependent instructions and discovery can be rebuilt cleanly, but implementation should verify whether an append-only update is sufficient and meaningfully preserves cache.
|
||||
- Legacy `experimental.chat.system.transform` can mutate the assembled baseline system prompt arbitrarily, but V2 plugins do not yet expose an equivalent hook. Decide separately whether to port it, replace dynamic uses with plugin-defined **Context Sources**, or narrow its semantics.
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -142,7 +142,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -169,7 +169,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -191,7 +191,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -215,7 +215,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -235,7 +235,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -260,7 +260,7 @@
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@ai-sdk/xai": "3.0.82",
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
@@ -270,7 +270,7 @@
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@opencode-ai/llm": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
@@ -324,7 +324,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"effect": "catalog:",
|
||||
@@ -378,7 +378,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -392,7 +392,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.15.10",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -404,7 +404,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -435,7 +435,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -451,7 +451,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -464,7 +464,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -482,7 +482,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -509,7 +509,7 @@
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@ai-sdk/xai": "3.0.82",
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -524,7 +524,7 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "2.6.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
@@ -609,7 +609,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -625,9 +625,9 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.3.1",
|
||||
"@opentui/keymap": ">=0.3.1",
|
||||
"@opentui/solid": ">=0.3.1",
|
||||
"@opentui/core": ">=0.3.2",
|
||||
"@opentui/keymap": ">=0.3.2",
|
||||
"@opentui/solid": ">=0.3.2",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -647,7 +647,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -662,7 +662,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -676,7 +676,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -689,7 +689,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
@@ -722,7 +722,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -741,7 +741,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -830,7 +830,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -882,9 +882,19 @@
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@aws-sdk/credential-provider-cognito-identity": "3.972.22",
|
||||
"@aws-sdk/credential-provider-env": "3.972.25",
|
||||
"@aws-sdk/credential-provider-http": "3.972.27",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.29",
|
||||
"@aws-sdk/credential-provider-login": "3.972.29",
|
||||
"@aws-sdk/credential-provider-node": "3.972.30",
|
||||
"@aws-sdk/credential-provider-process": "3.972.25",
|
||||
"@aws-sdk/credential-provider-sso": "3.972.29",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.972.29",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@smithy/shared-ini-file-loader": "4.4.8",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
},
|
||||
@@ -900,9 +910,9 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.3.1",
|
||||
"@opentui/keymap": "0.3.1",
|
||||
"@opentui/solid": "0.3.1",
|
||||
"@opentui/core": "0.3.2",
|
||||
"@opentui/keymap": "0.3.2",
|
||||
"@opentui/solid": "0.3.2",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -1068,7 +1078,7 @@
|
||||
|
||||
"@aws-sdk/client-athena": ["@aws-sdk/client-athena@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9eMUCu1Ay3C9ojo+dJcynSdpbxuwDVtZUt/Xhce+c2+mgDsmvRzjww+wfLpZwRNWxBWmeauQQAZk52tCwQgXsQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="],
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="],
|
||||
|
||||
"@aws-sdk/client-firehose": ["@aws-sdk/client-firehose@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-tDrtgczN2lQsflLDPYu/wdOoyCZLVYtgzmWnYzSEOBWd/cp2AbuQ7D+FemSwUTzyoMTuhhIevyEJKzqsF+QYxA=="],
|
||||
|
||||
@@ -1076,31 +1086,29 @@
|
||||
|
||||
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="],
|
||||
|
||||
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zwGLSiK48z3PzKpQiDMKP85+fpIrPMF1qQOQW9OW7BGj5AuBZIisT2O4VzIgYJeh+t47MLU7VgBQL7muc+MJDg=="],
|
||||
|
||||
"@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/credential-provider-node": "3.782.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-Q1QLY3xE2z1trgriusP/6w40mI/yJjM524bN4gs+g6YX4sZGufpa7+Dj+JjL4fz8f9BCJ3ZlI+p4WxFxH7qvdQ=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.932.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-AS8gypYQCbNojwgjvZGkJocC2CoEICDx9ZJ15ILsv+MlcCVLtUJSRSx3VzJOUY2EEIaGLRrPNlIqyn/9/fySvA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.38", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-OHkK6xOx/IHkSbQdDWxnVCLU+j28EFl8wyWgBILQDFAPY8n240C/O4gjmFx+zFU12lL8njgJQ5GWAIWq88CnSQ=="],
|
||||
"@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.22", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-ih6ORpme4i2qJqGckOQ9Lt2iiZ+5tm3bnfsT5TwoPyFnuDURXv3OdhYa3Nr/m0iJr38biqKYKdGKb5GR1KB2hw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg=="],
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.43", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA=="],
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" } }, "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA=="],
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-login": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA=="],
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.933.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-ini": "3.933.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-L2dE0Y7iMLammQewPKNeEh1z/fdJyYEU+/QsLBD9VEh+SXcN/FIyTi21Isw8wPZN6lMB9PDVtISzBnF8HuSFrw=="],
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ=="],
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/token-providers": "3.1056.0", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA=="],
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/token-providers": "3.1026.0", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ=="],
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew=="],
|
||||
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1057.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1057.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw=="],
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="],
|
||||
|
||||
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="],
|
||||
|
||||
@@ -1122,13 +1130,13 @@
|
||||
|
||||
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-9BGTbJyA/4PTdwQWE9hAFIJGpsYkyEW20WON3i15aDqo5oRZwZmqaVageOD57YYqG8JDJjvcwKyDdR4cc38dvg=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iOq86f2H67924kQUIPOAvlmMaOAvOLoDOIb66I2YqSUpMYB6ufiuJW3RlREgskxv86S5qKzMnfy/X6CqMjK6XQ=="],
|
||||
|
||||
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.932.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-NCIRJvoRc9246RZHIusY1+n/neeG2yGhBGdKhghmrNdM+mLLN6Ii7CKFZjx3DhxtpHMpl1HWLTMhdVrGwP2upw=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA=="],
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1026.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A=="],
|
||||
|
||||
@@ -1754,7 +1762,7 @@
|
||||
|
||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="],
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.9.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -1782,23 +1790,27 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.3.1", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.3.1", "@opentui/core-darwin-x64": "0.3.1", "@opentui/core-linux-arm64": "0.3.1", "@opentui/core-linux-x64": "0.3.1", "@opentui/core-win32-arm64": "0.3.1", "@opentui/core-win32-x64": "0.3.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-kQFSsSCgtlasSqTigCgKmM67xaquGvTg+vwimDnFSZtcBEt4E3dz7qLrbeh5FVvTA+RMbwe+Bozq03PW+SgjXw=="],
|
||||
"@opentui/core": ["@opentui/core@0.3.2", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.3.2", "@opentui/core-darwin-x64": "0.3.2", "@opentui/core-linux-arm64": "0.3.2", "@opentui/core-linux-arm64-musl": "0.3.2", "@opentui/core-linux-x64": "0.3.2", "@opentui/core-linux-x64-musl": "0.3.2", "@opentui/core-win32-arm64": "0.3.2", "@opentui/core-win32-x64": "0.3.2" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-5rCVS/3Obb3iLqg/egLCRArt7hAu3lX/9PWVHqUlnJylCT6b5NYDFljt0r3x8v3VG98LB71UzpnDv7DgmGKATw=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krvVfiBpeBY+727R8yogdqIcxkK3RUVcI97bqjl8jTeDMcWOkFFfHezssRMPmbR5x++1tX669Fz3fuxoe7XUIg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rFnGfqqEOGiUTbxglpiDA500KeRqcI1ukemhNfDrEzx3imAArS8mFZSuUG7ib31P5EpX+PXuvg0G9/3YXfgWeQ=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-D/6ec5H8SPpSBMr01/sqgSddIl1Qc1QMKsDl/wV5MpbxYc7Qvie9qlNvvoSsWNfAXAbafLRb1jQBzouk41cp1w=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z+/GxKvB3NzMDSwuyWR7HDStbaNRf5a09lt5W9b4BGmCAFW/mbX0Tuh3kloubcMgiq5vLnVaNzY19hrIgJrjGQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-E/FFBoAsWJyS/EO/cF7h7DuEENYa9nAdSv1W/TIyKXpBisN6K3U1Xgbk528TkfWjrwJjhGs+9OMYdXuAHd5LTw=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-0TRuGNR+2GGk0rQuDaIxkIa/3Ty/XySfeOQLAUX4Jqaifky04As69fYT17yOhHqg5viCJUAGG/SdW8IH6C2osg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Btb7Q4BOC55Aj2qCs0VoxGuj87DNfUEaSx0z89oeU4npTN+6SpJApyGZTCNNeSe2sdmOGeh/8eAR4X96ORjcKg=="],
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-noDKfwYUjutQUx5rtoyKrBIYaeSCmAmtxOSJdnKecyWEhMygtdHp4ssPtxzsZMuQAliHogCmD7vUG/pGMgcXMQ=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-+lt24u3KwEPG69oXDOLz9N484wPcAHvrPbDNU77OT6DvWew+StAjh40eY+Zeu0TkTNDWfj7qnQKV0GKWtFA3cw=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EZp+Lg9eZwzwNny4l2ACHdC95JbEKYbor1WImnm6IEo1e2Fgl9mltYv2J+i+Ea+dXQbrkK/MRIw7CRusxfqFFA=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-eVkKMYirYgpn92lI0YT/GKru4J+UiXjzwyzNRFX+P59OHXvL3GFdqJMcJmX4/zvyjg4c8HDnU79YLnyG+TlXLw=="],
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/FNGeJYhCHcIE3qBIo+nl8014NZ8u/XXUwQY2RjuWhMnzK9kQUfZ3cW4FP6FkOA/k4jGvByya47193II6djFtw=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.3.1", "", { "dependencies": { "@opentui/core": "0.3.1" }, "peerDependencies": { "@opentui/react": "0.3.1", "@opentui/solid": "0.3.1", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-BTj+ggsarO2uyvd6CWzvgfsekA8c4aEclbAPKPZGVjBI3Fo5+KAHUrXvteFO5qpGMANfEJTtVHoRu5cic1Nlaw=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-/zb5nCZKDgBS1UEQXrzTYUbbTPFMygbiZ/BfNWjEDIbm63gVzQ7pVouYbGP+88CRXIJtwT6LeoVPgp9nmOrDWQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.3.1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.3.1", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-2R6wEijfMub9COTBCm8IKVj2y7+Sc4fZZjJawxk8sE6+++mzeUaokKNJTlYhZXpMju4LKMv6j9CjWkG8JYfbcg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-q8xqMhW1jlJVzos+A5+FXRquH01j1ZHmrNi/9++W1Ebz3LQYn+8Z8j7rcV/meIiDuo9nyRHigQQT+cy9xV4N2g=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.3.2", "", { "dependencies": { "@opentui/core": "0.3.2" }, "peerDependencies": { "@opentui/react": "0.3.2", "@opentui/solid": "0.3.2", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-y+IPBagxPvVrKIISmlfvO7ScvVIXGMmV92x0O+WzQ2vPf6fi5xgJc4YNmgEACfGdnM3ub14MkgEXqMOeUN7ZQA=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.3.2", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.3.2", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-Yff0gSwIY/o0XeMciYeAUkQtea8bWzR0UjjVglmcBe13hWuJZt/GfjbDMdNNQ8zCrLubLEh04an5fYXCd7NMYQ=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -2338,7 +2350,7 @@
|
||||
|
||||
"@smithy/protocol-http": ["@smithy/protocol-http@5.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-jOD+4WNWQLntiLJn3r82C7BLheEbRCKTbU5U5bskZmT7nwRiGkh0IghuHwHRZ1ZEFXpHltQxxp9/koOPsdluJg=="],
|
||||
|
||||
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-W7IPDXj8AZdyH5EWEXmOvN7ao8iN0JKJ0FNLpGcqj08HZc0MmqGcJnGgh3DfUdGYtzrPIEudxs+ovq/EWZgLjg=="],
|
||||
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.8", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw=="],
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA=="],
|
||||
|
||||
@@ -5418,26 +5430,36 @@
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-DcDXAl32I/YZnJ9LyX/XpRXOtoTYIwgmYxoNMGkyvtomdjPpkXPGhz93VJyzKFFNffz/SZwEkoAuWOkeOzo90Q=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-4Org8yUew+Y1+buTcH5A39unAdkVRnQxcOp3XexvFAVctbtznDytk3UKbkXq8FYWEVdz1ycxnAqHaKHePyGQEg=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-T/FpIr0OcR1kad/u5ZTDE2ziFG7QM6pq1MN8atHBmyyOJgqWjGez9TQ0W2WCixS7EE9fsUQKWn70l5M+e+O/qg=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-sWd7bGH+thMsNGYdqPdLuH7SDEkpplWCDKSCWhjkMPXwz3o/9BK3ZNrd5JGUB8y+cPbs3rXIe3Ah7iSlKXj5FQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-ol9yhY2nzPTrQoKX9WZ8cps2JABcDt0Dlhr59FRYYW/2S5h07PFrhfZZzD8sXZ8XpYfObTIJ+evmbcz41m1SJQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-h4VTGl6lxJkM/odeRPzXB5YZbkxVr5FnJ2Bwv78+IY9Ah7QsXSBhefvvz1CQDoQNzOLYsNMQ3PhMQM7i6tpoPQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-hYTWRlhOnhs2tYCYgNK30jaQKyl4FvXQl0AK9tKRZq8sunS9ygi+USYiG6LCb7DuqT0Gl3jONP9jtb4D4NYhHw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@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=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@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=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/client-s3/@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=="],
|
||||
|
||||
"@aws-sdk/client-sso/@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=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/core": "^3.2.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.0.2", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.782.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-ini": "3.782.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-tkSegM0Z6WMXpLB8oPys/d+umYIocvO298mGvcMCncpRl77L9XkvSLJIFzaHes+o7djAgIduYw8wKIMStFss2w=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-FaxO1xom4MAoUJsldmR92nT1G6uZxTdNYOFYtdHfd6N2wcNaTuxgjIvqzg5y7QIH9kn58XX/dzf1iTjgqUStZw=="],
|
||||
@@ -5460,6 +5482,8 @@
|
||||
|
||||
"@aws-sdk/core/@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=="],
|
||||
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
@@ -5472,23 +5496,17 @@
|
||||
|
||||
"@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HygGyKuMG5AaGXsmM0d81miWDon55xwalRHB3UmDg3QBhtunbNIoIaWUbNTKuBZXcIN6emeeEZw/YgSMqLc0YA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.933.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.933.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/token-providers": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/R1DBR7xNcuZIhS2RirU+P2o8E8/fOk+iLAhbqeSTq+g09fP/F6W7ouFpS5eVE2NIfWG7YBFoVddOhvuqpn51g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-c7Eccw2lhFx2/+qJn3g+uIDWRuWi2A6Sz3PVvckFUEzPsP0dPUo19hlvtarwP5GzrsXn0yEPRVhpewsIaSCGaQ=="],
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
@@ -5496,16 +5514,18 @@
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
|
||||
|
||||
"@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/middleware-flexible-checksums/@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=="],
|
||||
@@ -5514,12 +5534,30 @@
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
"@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-DcDXAl32I/YZnJ9LyX/XpRXOtoTYIwgmYxoNMGkyvtomdjPpkXPGhz93VJyzKFFNffz/SZwEkoAuWOkeOzo90Q=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.15", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-4Org8yUew+Y1+buTcH5A39unAdkVRnQxcOp3XexvFAVctbtznDytk3UKbkXq8FYWEVdz1ycxnAqHaKHePyGQEg=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-T/FpIr0OcR1kad/u5ZTDE2ziFG7QM6pq1MN8atHBmyyOJgqWjGez9TQ0W2WCixS7EE9fsUQKWn70l5M+e+O/qg=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-sWd7bGH+thMsNGYdqPdLuH7SDEkpplWCDKSCWhjkMPXwz3o/9BK3ZNrd5JGUB8y+cPbs3rXIe3Ah7iSlKXj5FQ=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-ol9yhY2nzPTrQoKX9WZ8cps2JABcDt0Dlhr59FRYYW/2S5h07PFrhfZZzD8sXZ8XpYfObTIJ+evmbcz41m1SJQ=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-h4VTGl6lxJkM/odeRPzXB5YZbkxVr5FnJ2Bwv78+IY9Ah7QsXSBhefvvz1CQDoQNzOLYsNMQ3PhMQM7i6tpoPQ=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "tslib": "^2.6.2" } }, "sha512-hYTWRlhOnhs2tYCYgNK30jaQKyl4FvXQl0AK9tKRZq8sunS9ygi+USYiG6LCb7DuqT0Gl3jONP9jtb4D4NYhHw=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@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=="],
|
||||
|
||||
"@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
|
||||
"@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||
@@ -5808,6 +5846,8 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||
|
||||
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="],
|
||||
|
||||
"ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -6224,17 +6264,9 @@
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="],
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wd4KdRy2YjLsE4Y7pz00470Iip06GlRHkG4dyLW7/hFMzEO2o7ixswCWp6J2VGZVAX64acknlv2Q0z02ebjmhw=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.782.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.782.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/token-providers": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-1y1ucxTtTIGDSNSNxriQY8msinilhe9gGvQpUDYW9gboyC7WQJPDw66imy258V6osdtdi+xoHzVCbCz3WhosMQ=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xCna0opVPaueEbJoclj5C6OpDNi0Gynj+4d7tnuXGgQhTHPyAz8ZyClkVqpi5qvHTgxROdUEDxWqEO5jqRHZHQ=="],
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
@@ -6242,26 +6274,30 @@
|
||||
|
||||
"@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
"@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
@@ -6764,13 +6800,7 @@
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.782.0", "", { "dependencies": { "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-4tPuk/3+THPrzKaXW4jE2R67UyGwHLFizZ47pcjJWbhb78IIJAy94vbeqEQ+veS84KF5TXcU7g5jGTXC0D70Wg=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
|
||||
@@ -6780,12 +6810,6 @@
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@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=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@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=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
@@ -6966,7 +6990,7 @@
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
@@ -6976,8 +7000,6 @@
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@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=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
@@ -7026,6 +7048,8 @@
|
||||
|
||||
"tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
exact = true
|
||||
# Only install newly resolved package versions published at least 3 days ago.
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider"]
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider"]
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-dvFu5Cbs8MFoSBQXwv4HN2vyh5p20dh6QC5zZiFr0qs=",
|
||||
"aarch64-linux": "sha256-l0xO7Nocl6enQxLQlLB71mG+NuT6I1eQQ1FgLtYGQOg=",
|
||||
"aarch64-darwin": "sha256-WY6Lstxt4n4n63kYZUX09birHx7sNvl0Pegc6L13mGE=",
|
||||
"x86_64-darwin": "sha256-sZdG40TSE9KhrmLQyQMPRugGo6R7AS3wgHiEGYtcXtc="
|
||||
"x86_64-linux": "sha256-mXTzANDuuy+BY4vzhuuL5Q6JVVTJCKdHuD/Fo8pSfgI=",
|
||||
"aarch64-linux": "sha256-t1Uf+PIDvj9bogsSo2Dg1e+zJM2CHQ8lpA/I3vFQA1Q=",
|
||||
"aarch64-darwin": "sha256-HKpMwzpYhCQOu0xHugi4ZIC/Va2BSiQpM2TbA6BEZDU=",
|
||||
"x86_64-darwin": "sha256-m5h7h9KxkcIrdTO2QzQftq68d0Ru0IsCfu3WzMp4P68="
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -39,9 +39,9 @@
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.3.1",
|
||||
"@opentui/keymap": "0.3.1",
|
||||
"@opentui/solid": "0.3.1",
|
||||
"@opentui/core": "0.3.2",
|
||||
"@opentui/keymap": "0.3.2",
|
||||
"@opentui/solid": "0.3.2",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
@@ -133,9 +133,19 @@
|
||||
"electron"
|
||||
],
|
||||
"overrides": {
|
||||
"@aws-sdk/credential-provider-cognito-identity": "3.972.22",
|
||||
"@aws-sdk/credential-provider-env": "3.972.25",
|
||||
"@aws-sdk/credential-provider-http": "3.972.27",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.29",
|
||||
"@aws-sdk/credential-provider-login": "3.972.29",
|
||||
"@aws-sdk/credential-provider-node": "3.972.30",
|
||||
"@aws-sdk/credential-provider-process": "3.972.25",
|
||||
"@aws-sdk/credential-provider-sso": "3.972.29",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.972.29",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@smithy/shared-ini-file-loader": "4.4.8",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
||||
const directory = "C:/OpenCode/PromptThinkingLevelRegression"
|
||||
const projectID = "proj_prompt_thinking_level_regression"
|
||||
@@ -56,7 +57,7 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => {
|
||||
const composer = page.locator('[data-component="session-composer"]')
|
||||
const input = composer.locator('[data-component="prompt-input"]')
|
||||
const control = composer.locator('[data-component="prompt-variant-control"]')
|
||||
await expect(composer).toBeVisible()
|
||||
await expectAppVisible(composer)
|
||||
|
||||
await idleComposer(page)
|
||||
await expect(control).toBeHidden()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { test } from "@playwright/test"
|
||||
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
||||
test("shows loaded sessions before the directory path request resolves", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
@@ -33,7 +34,7 @@ test("shows loaded sessions before the directory path request resolves", async (
|
||||
|
||||
await page.goto("/")
|
||||
try {
|
||||
await expect(page.getByText(fixture.expected.sourceTitle).first()).toBeVisible({ timeout: 5_000 })
|
||||
await expectAppVisible(page.getByText(fixture.expected.sourceTitle).first())
|
||||
} finally {
|
||||
releasePath()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const directory = "C:/OpenCode/TimelineStateRegression"
|
||||
const projectID = "proj_timeline_state_regression"
|
||||
@@ -106,10 +107,10 @@ test.describe("regression: session timeline local row state", () => {
|
||||
await configurePage(page)
|
||||
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
||||
await expectSessionTitle(page, title)
|
||||
|
||||
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
|
||||
await expect(wrapper).toBeVisible()
|
||||
await expectAppVisible(wrapper)
|
||||
await expectExpanded(wrapper, true)
|
||||
|
||||
await wrapper.evaluate((element) => {
|
||||
@@ -142,11 +143,11 @@ test.describe("regression: session timeline local row state", () => {
|
||||
await configurePage(page)
|
||||
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
||||
await expectSessionTitle(page, title)
|
||||
|
||||
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
|
||||
await expect(wrapper).toBeVisible()
|
||||
await expect(wrapper.locator('[data-component="file"][data-mode="diff"]').first()).toBeVisible()
|
||||
await expectAppVisible(wrapper)
|
||||
await expectAppVisible(wrapper.locator('[data-component="file"][data-mode="diff"]').first())
|
||||
await markDiffProbe(page)
|
||||
|
||||
events.push({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const directory = "C:/OpenCode/ContextResizeRegression"
|
||||
const projectID = "proj_context_resize_regression"
|
||||
@@ -23,9 +24,9 @@ test.describe("regression: session timeline context group resize", () => {
|
||||
await configurePage(page)
|
||||
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
||||
await expect(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()).toBeVisible()
|
||||
await expect(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first()).toBeVisible()
|
||||
await expectSessionTitle(page, title)
|
||||
await expectAppVisible(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first())
|
||||
await expectAppVisible(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first())
|
||||
await settle(page)
|
||||
|
||||
const samples = await sampleExpansion(page)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { fixture, pageMessages } from "./session-timeline.fixture"
|
||||
import { trackPageErrors, expectNoSmokeErrors } from "../utils/errors"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { APP_READY_TIMEOUT, expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const forbiddenText = ["Load details", "Show earlier steps"]
|
||||
|
||||
@@ -411,18 +412,21 @@ function expectCompleteScroll(
|
||||
|
||||
async function selectHomeProject(page: Page, projectName: string) {
|
||||
await page.goto("/")
|
||||
await page
|
||||
const row = page
|
||||
.locator('[data-component="home-project-row"]')
|
||||
.filter({ hasText: new RegExp(projectName, "i") })
|
||||
.click()
|
||||
.first()
|
||||
await expectAppVisible(row)
|
||||
await row.click()
|
||||
await expect(row).toHaveAttribute("data-selected", "", { timeout: APP_READY_TIMEOUT })
|
||||
await expect(page).toHaveURL(/\/$/)
|
||||
}
|
||||
|
||||
async function navigateToSession(page: Page, directory: string, sessionId: string, expectedTitle: string) {
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionId}`)
|
||||
await expect(page.getByRole("heading", { name: expectedTitle })).toBeVisible()
|
||||
await expectSessionTitle(page, expectedTitle)
|
||||
}
|
||||
|
||||
async function expectSessionReady(page: Page) {
|
||||
await expect(page.getByRole("textbox", { name: /Ask anything/i })).toBeVisible()
|
||||
await expectAppVisible(page.getByRole("textbox", { name: /Ask anything/i }))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, type Locator, type Page } from "@playwright/test"
|
||||
|
||||
export const APP_READY_TIMEOUT = 30_000
|
||||
|
||||
export async function expectAppVisible(locator: Locator) {
|
||||
await expect(locator).toBeVisible({ timeout: APP_READY_TIMEOUT })
|
||||
}
|
||||
|
||||
export async function expectSessionTitle(page: Page, title: string) {
|
||||
await expectAppVisible(page.getByRole("heading", { name: title }))
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
+37
-34
@@ -25,7 +25,6 @@ import {
|
||||
onCleanup,
|
||||
type ParentProps,
|
||||
Show,
|
||||
Suspense,
|
||||
} from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
@@ -44,6 +43,7 @@ import { PromptProvider } from "@/context/prompt"
|
||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { TabsProvider } from "@/context/tabs"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import Layout from "@/pages/layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
@@ -211,26 +211,21 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
Effect.runPromise,
|
||||
),
|
||||
)
|
||||
const checking = createMemo(
|
||||
() => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state),
|
||||
)
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
<Show
|
||||
when={!checking()}
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/*<Show
|
||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||
fallback={
|
||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>*/}
|
||||
{checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
|
||||
<Show
|
||||
when={startupHealthCheck()}
|
||||
when={startupHealthCheck.latest}
|
||||
fallback={
|
||||
<ConnectionError
|
||||
onRetry={() => {
|
||||
@@ -246,8 +241,7 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||
>
|
||||
{props.children}
|
||||
</Show>
|
||||
{/*</Show>*/}
|
||||
</Suspense>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -310,32 +304,41 @@ function ServerKey(props: ParentProps) {
|
||||
export function AppInterface(props: {
|
||||
children?: JSX.Element
|
||||
defaultServer: ServerConnection.Key
|
||||
canonicalLocalServer?: ServerConnection.Key
|
||||
servers?: Array<ServerConnection.Any>
|
||||
router?: Component<BaseRouterProps>
|
||||
disableHealthCheck?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||
<GlobalProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||
<ServerProvider
|
||||
defaultServer={props.defaultServer}
|
||||
canonicalLocalServer={props.canonicalLocalServer}
|
||||
servers={props.servers}
|
||||
>
|
||||
<GlobalProvider>
|
||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||
<ServerKey>
|
||||
<QueryProvider>
|
||||
<ServerSDKProvider>
|
||||
<ServerSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</QueryProvider>
|
||||
</ServerKey>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => (
|
||||
<TabsProvider>
|
||||
<ServerKey>
|
||||
<QueryProvider>
|
||||
<ServerSDKProvider>
|
||||
<ServerSyncProvider>
|
||||
<RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>
|
||||
</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</QueryProvider>
|
||||
</ServerKey>
|
||||
</TabsProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</ConnectionGate>
|
||||
</GlobalProvider>
|
||||
</ServerProvider>
|
||||
|
||||
@@ -6,21 +6,23 @@ import { useMutation } from "@tanstack/solid-query"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { useGlobal } from "@/context/global"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
export function DialogEditProject(props: { project: LocalProject; server: ServerConnection.Any }) {
|
||||
const dialog = useDialog()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
const serverCtx = createMemo(() => global.createServerCtx(props.server))
|
||||
const serverSDK = () => serverCtx().sdk
|
||||
const serverSync = () => serverCtx().sync
|
||||
|
||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||
const defaultName = createMemo(() => props.project.name || folderName())
|
||||
@@ -78,19 +80,19 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const start = store.startup.trim()
|
||||
|
||||
if (props.project.id && props.project.id !== "global") {
|
||||
await serverSDK.client.project.update({
|
||||
await serverSDK().client.project.update({
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
||||
commands: { start },
|
||||
})
|
||||
serverSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||
serverSync().project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
serverSync.project.meta(props.project.worktree, {
|
||||
serverSync().project.meta(props.project.worktree, {
|
||||
name,
|
||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
||||
commands: { start: start || undefined },
|
||||
|
||||
@@ -18,6 +18,7 @@ import { usePlatform } from "@/context/platform"
|
||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||
import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
|
||||
const DEFAULT_USERNAME = "opencode"
|
||||
|
||||
@@ -191,6 +192,7 @@ export function DialogSelectServer() {
|
||||
export function useServerManagementController(options: { onSelect?: () => void } = {}) {
|
||||
const navigate = useNavigate()
|
||||
const server = useServer()
|
||||
const tabs = useTabs()
|
||||
const global = useGlobal()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
@@ -311,12 +313,14 @@ export function useServerManagementController(options: { onSelect?: () => void }
|
||||
}))
|
||||
|
||||
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
||||
const originalKey = ServerConnection.key(original)
|
||||
const active = server.key
|
||||
tabs.removeServer(originalKey)
|
||||
const newConn = server.add(next)
|
||||
if (!newConn) return
|
||||
const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active
|
||||
const nextActive = active === originalKey ? ServerConnection.key(newConn) : active
|
||||
if (nextActive) server.setActive(nextActive)
|
||||
server.remove(ServerConnection.key(original))
|
||||
server.remove(originalKey)
|
||||
}
|
||||
|
||||
const items = createMemo(() => {
|
||||
@@ -501,6 +505,7 @@ export function useServerManagementController(options: { onSelect?: () => void }
|
||||
})
|
||||
|
||||
async function handleRemove(url: ServerConnection.Key) {
|
||||
tabs.removeServer(url)
|
||||
server.remove(url)
|
||||
if ((await platform.getDefaultServer?.()) === url) {
|
||||
void platform.setDefaultServer?.(null)
|
||||
|
||||
@@ -127,6 +127,7 @@ beforeAll(async () => {
|
||||
mock.module("@/context/sdk", () => ({
|
||||
useSDK: () => {
|
||||
const sdk = {
|
||||
scope: "local",
|
||||
directory: "/repo/main",
|
||||
client: rootClient,
|
||||
url: "http://localhost:4096",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { buildRequestParts } from "./build-request-parts"
|
||||
import { setCursorPosition } from "./editor-dom"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { ScopedKey } from "@/utils/server-scope"
|
||||
|
||||
type PendingPrompt = {
|
||||
abort: AbortController
|
||||
@@ -212,6 +213,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const layout = useLayout()
|
||||
const language = useLanguage()
|
||||
const params = useParams()
|
||||
const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID)
|
||||
|
||||
const errorMessage = (err: unknown) => {
|
||||
if (err && typeof err === "object" && "data" in err) {
|
||||
@@ -232,11 +234,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
|
||||
input.onAbort?.()
|
||||
|
||||
const queued = pending.get(sessionID)
|
||||
const key = pendingKey(sessionID)
|
||||
const queued = pending.get(key)
|
||||
if (queued) {
|
||||
queued.abort.abort()
|
||||
queued.cleanup()
|
||||
pending.delete(sessionID)
|
||||
pending.delete(key)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return sdk.client.session
|
||||
@@ -341,7 +344,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
})
|
||||
return
|
||||
}
|
||||
WorktreeState.pending(createdWorktree.directory)
|
||||
WorktreeState.pending(sdk.scope, createdWorktree.directory)
|
||||
sessionDirectory = createdWorktree.directory
|
||||
}
|
||||
|
||||
@@ -500,7 +503,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
clearInput()
|
||||
|
||||
const waitForWorktree = async () => {
|
||||
const worktree = WorktreeState.get(sessionDirectory)
|
||||
const worktree = WorktreeState.get(sdk.scope, sessionDirectory)
|
||||
if (!worktree || worktree.status !== "pending") return true
|
||||
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
@@ -517,7 +520,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
restoreInput()
|
||||
}
|
||||
|
||||
pending.set(session.id, { abort: controller, cleanup })
|
||||
pending.set(pendingKey(session.id), { abort: controller, cleanup })
|
||||
|
||||
const abortWait = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||
if (controller.signal.aborted) {
|
||||
@@ -544,11 +547,13 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
}, timeoutMs)
|
||||
})
|
||||
|
||||
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abortWait, timeout]).finally(() => {
|
||||
if (timer.id === undefined) return
|
||||
clearTimeout(timer.id)
|
||||
})
|
||||
pending.delete(session.id)
|
||||
const result = await Promise.race([WorktreeState.wait(sdk.scope, sessionDirectory), abortWait, timeout]).finally(
|
||||
() => {
|
||||
if (timer.id === undefined) return
|
||||
clearTimeout(timer.id)
|
||||
},
|
||||
)
|
||||
pending.delete(pendingKey(session.id))
|
||||
if (controller.signal.aborted) return false
|
||||
if (result.status === "failed") throw new Error(result.message)
|
||||
return true
|
||||
@@ -563,7 +568,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
optimisticBusy: sessionDirectory === projectDirectory,
|
||||
before: waitForWorktree,
|
||||
}).catch((err) => {
|
||||
pending.delete(session.id)
|
||||
pending.delete(pendingKey(session.id))
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "idle" })
|
||||
}
|
||||
|
||||
@@ -178,11 +178,11 @@ export const SettingsGeneral: Component = () => {
|
||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||
|
||||
const serverSync = useServerSync()
|
||||
const globalSdk = useServerSDK()
|
||||
const serverSdk = useServerSDK()
|
||||
|
||||
const [shells] = createResource(
|
||||
() =>
|
||||
globalSdk.client.pty
|
||||
serverSdk.client.pty
|
||||
.shells()
|
||||
.then((res) => res.data ?? [])
|
||||
.catch(() => [] as ShellOption[]),
|
||||
|
||||
@@ -16,14 +16,8 @@ export const DialogSettings: Component = () => {
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<Dialog size="x-large" class="settings-v2-dialog" data-component="settings-v2-dialog">
|
||||
<TabsV2
|
||||
orientation="vertical"
|
||||
variant="settings"
|
||||
defaultValue="general"
|
||||
class="settings-v2"
|
||||
data-component="settings-v2"
|
||||
>
|
||||
<Dialog size="x-large" variant="settings" class="settings-v2-dialog">
|
||||
<TabsV2 orientation="vertical" variant="settings" defaultValue="general" class="settings-v2">
|
||||
<TabsV2.List>
|
||||
<div class="flex flex-col justify-between h-full w-full">
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
|
||||
@@ -179,12 +179,12 @@ export const SettingsGeneralV2: Component = () => {
|
||||
|
||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||
|
||||
const globalSync = useServerSync()
|
||||
const globalSdk = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const serverSdk = useServerSDK()
|
||||
|
||||
const [shells] = createResource(
|
||||
() =>
|
||||
globalSdk.client.pty
|
||||
serverSdk.client.pty
|
||||
.shells()
|
||||
.then((res) => res.data ?? [])
|
||||
.catch(() => [] as ShellOption[]),
|
||||
@@ -208,11 +208,11 @@ export const SettingsGeneralV2: Component = () => {
|
||||
})
|
||||
|
||||
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
||||
const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
|
||||
const currentShell = createMemo(() => serverSync.data.config.shell ?? "")
|
||||
|
||||
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
||||
const list = shells.latest
|
||||
const current = globalSync.data.config.shell
|
||||
const current = serverSync.data.config.shell
|
||||
|
||||
const nameCounts = new Map<string, number>()
|
||||
for (const s of list) {
|
||||
@@ -347,7 +347,7 @@ export const SettingsGeneralV2: Component = () => {
|
||||
onSelect={(option) => {
|
||||
if (!option) return
|
||||
if (option.value === currentShell()) return
|
||||
globalSync.updateConfig({ shell: option.value })
|
||||
serverSync.updateConfig({ shell: option.value })
|
||||
}}
|
||||
/>
|
||||
</SettingsRowV2>
|
||||
|
||||
@@ -33,8 +33,8 @@ const PROVIDER_ICON_SIZE = 16
|
||||
export const SettingsProvidersV2: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const globalSDK = useServerSDK()
|
||||
const globalSync = useServerSync()
|
||||
const serverSdk = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const providers = useProviders()
|
||||
|
||||
const connected = createMemo(() => {
|
||||
@@ -77,7 +77,7 @@ export const SettingsProvidersV2: Component = () => {
|
||||
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
|
||||
|
||||
const isConfigCustom = (providerID: string) => {
|
||||
const provider = globalSync.data.config.provider?.[providerID]
|
||||
const provider = serverSync.data.config.provider?.[providerID]
|
||||
if (!provider) return false
|
||||
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
||||
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
||||
@@ -85,11 +85,11 @@ export const SettingsProvidersV2: Component = () => {
|
||||
}
|
||||
|
||||
const disableProvider = async (providerID: string, name: string) => {
|
||||
const before = globalSync.data.config.disabled_providers ?? []
|
||||
const before = serverSync.data.config.disabled_providers ?? []
|
||||
const next = before.includes(providerID) ? before : [...before, providerID]
|
||||
globalSync.set("config", "disabled_providers", next)
|
||||
serverSync.set("config", "disabled_providers", next)
|
||||
|
||||
await globalSync
|
||||
await serverSync
|
||||
.updateConfig({ disabled_providers: next })
|
||||
.then(() => {
|
||||
showToast({
|
||||
@@ -100,7 +100,7 @@ export const SettingsProvidersV2: Component = () => {
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
globalSync.set("config", "disabled_providers", before)
|
||||
serverSync.set("config", "disabled_providers", before)
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
@@ -108,14 +108,14 @@ export const SettingsProvidersV2: Component = () => {
|
||||
|
||||
const disconnect = async (providerID: string, name: string) => {
|
||||
if (isConfigCustom(providerID)) {
|
||||
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
||||
await serverSdk.client.auth.remove({ providerID }).catch(() => undefined)
|
||||
await disableProvider(providerID, name)
|
||||
return
|
||||
}
|
||||
await globalSDK.client.auth
|
||||
await serverSdk.client.auth
|
||||
.remove({ providerID })
|
||||
.then(async () => {
|
||||
await globalSDK.client.global.dispose()
|
||||
await serverSdk.client.global.dispose()
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
@import "@opencode-ai/ui/v2/text-input-v2.css";
|
||||
@import "@opencode-ai/ui/v2/button-v2.css";
|
||||
|
||||
[data-component="settings-v2"] {
|
||||
[data-component="tabs-v2"][data-variant="settings"] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[data-component="settings-v2-dialog"] [data-slot="dialog-body"] {
|
||||
[data-component="dialog-v2"][data-variant="settings"] [data-slot="dialog-container"] {
|
||||
background: var(--v2-background-bg-base);
|
||||
}
|
||||
|
||||
[data-component="dialog-v2"][data-variant="settings"] [data-slot="dialog-body"] {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -81,10 +85,6 @@
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted);
|
||||
}
|
||||
|
||||
[data-slot="dialog-container"]:has(.settings-v2-dialog) {
|
||||
box-shadow: var(--v2-elevation-overlay);
|
||||
}
|
||||
|
||||
[data-component="settings-v2-row"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -153,12 +153,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-component="settings-v2-dialog"] [data-component="select-v2-root"] {
|
||||
[data-component="dialog-v2"][data-variant="settings"] [data-component="select-v2-root"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-component="settings-v2-dialog"] [data-component="button-v2"] {
|
||||
[data-component="dialog-v2"][data-variant="settings"] [data-component="button-v2"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { createEffect, createMemo, For, mapArray, Match, Show, startTransition, Switch, untrack } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onMount,
|
||||
Show,
|
||||
startTransition,
|
||||
Switch,
|
||||
untrack,
|
||||
} from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -9,7 +21,7 @@ import { useTheme } from "@opencode-ai/ui/theme/context"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
|
||||
import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/context/layout"
|
||||
import { getProjectAvatarVariant, LayoutRoute, useLayout, type LocalProject } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -17,18 +29,16 @@ import { useSettings } from "@/context/settings"
|
||||
import { WindowsAppMenu } from "./windows-app-menu"
|
||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { decodeDirectory } from "@/pages/directory-layout"
|
||||
import { iife } from "@opencode-ai/core/util/iife"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import {
|
||||
readSessionTabsRemovedDetail,
|
||||
SESSION_TABS_REMOVED_EVENT,
|
||||
type SessionTabsRemovedDetail,
|
||||
} from "@/components/titlebar-session-events"
|
||||
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { ServerConnection, useServer } from "@/context/server"
|
||||
import { tabHref, useTabs, type Tab } from "@/context/tabs"
|
||||
|
||||
type TauriDesktopWindow = {
|
||||
startDragging?: () => Promise<void>
|
||||
@@ -56,8 +66,6 @@ const v2TitlebarHeight = 36
|
||||
const minTitlebarZoom = 0.25
|
||||
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
||||
|
||||
const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
|
||||
|
||||
export type TitlebarUpdate = {
|
||||
version: () => string | undefined
|
||||
installing: () => boolean
|
||||
@@ -71,6 +79,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const theme = useTheme()
|
||||
const server = useServer()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const params = useParams()
|
||||
@@ -243,6 +252,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const serverSync = useServerSync()
|
||||
const navigate = useNavigate()
|
||||
const homeMatch = useMatch(() => "/")
|
||||
const layout = useLayout()
|
||||
|
||||
const newSessionHref = () => {
|
||||
if (params.dir) return `/${params.dir}/session`
|
||||
@@ -253,77 +263,63 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
return `/${base64Encode(project.worktree)}/session`
|
||||
}
|
||||
|
||||
type Tab = { dir: string; sessionId: string; href: string }
|
||||
|
||||
const [tabsStore, tabsStoreActions] = iife(() => {
|
||||
const [store, setStore] = createStore<Tab[]>(
|
||||
iife(() => {
|
||||
if (!params.dir || !params.id) return []
|
||||
return [
|
||||
{
|
||||
dir: decodeDirectory(params.dir) ?? "",
|
||||
sessionId: params.id,
|
||||
href: makeSessionHref(params.dir, params.id),
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
|
||||
const actions = {
|
||||
addTab: (tab: Tab) => {
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
if (tabs.some((t) => t.href === tab.href)) return
|
||||
|
||||
tabs.push(tab)
|
||||
}),
|
||||
)
|
||||
},
|
||||
removeTab: (href: string) => {
|
||||
void startTransition(() => {
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
const index = tabs.findIndex((t) => t.href === href)
|
||||
if (index === -1) return
|
||||
tabs.splice(index, 1)
|
||||
const nextTab = tabs[index] ?? tabs[tabs.length - 1]
|
||||
if (nextTab) navigate(nextTab.href)
|
||||
else navigate("/")
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
removeSessions: (input: SessionTabsRemovedDetail) => {
|
||||
void startTransition(() => {
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
const sessionIDs = new Set(input.sessionIDs)
|
||||
const currentHref = params.dir && params.id ? makeSessionHref(params.dir, params.id) : undefined
|
||||
const currentIndex = currentHref ? tabs.findIndex((tab) => tab.href === currentHref) : -1
|
||||
const removedCurrent =
|
||||
currentIndex !== -1 &&
|
||||
tabs[currentIndex]?.dir === input.directory &&
|
||||
sessionIDs.has(tabs[currentIndex]?.sessionId ?? "")
|
||||
|
||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
||||
const tab = tabs[i]
|
||||
if (!tab) continue
|
||||
if (tab.dir !== input.directory) continue
|
||||
if (!sessionIDs.has(tab.sessionId)) continue
|
||||
tabs.splice(i, 1)
|
||||
}
|
||||
|
||||
if (!removedCurrent) return
|
||||
const nextTab = tabs[currentIndex] ?? tabs[tabs.length - 1]
|
||||
if (nextTab) navigate(nextTab.href)
|
||||
else navigate("/")
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
const tabs = useTabs()
|
||||
const tabsStore = tabs.store
|
||||
const tabsStoreActions = tabs
|
||||
const navigateTab = (tab: Tab) => {
|
||||
const href = tabHref(tab)
|
||||
if (tab.server === server.key) {
|
||||
navigate(href)
|
||||
return
|
||||
}
|
||||
void startTransition(() => {
|
||||
server.setActive(tab.server)
|
||||
navigate(href)
|
||||
})
|
||||
}
|
||||
|
||||
return [store, actions]
|
||||
const matchRoute = (route: LayoutRoute) => {
|
||||
if (route.type === "home") return
|
||||
if (route.type === "dir-new-sesssion") {
|
||||
}
|
||||
if (route.type === "session") {
|
||||
const main = tabsStore.find(
|
||||
(item) =>
|
||||
item.type === "session" && item.server === route.server && item.sessionId === route.sessionId,
|
||||
)
|
||||
if (main) return main
|
||||
const sync = serverSync.createDirSyncContext(route.dir)
|
||||
const session = sync.session.get(route.sessionId)
|
||||
if (session?.parentID) {
|
||||
const parentID = session.parentID
|
||||
const parent = tabsStore.find(
|
||||
(item) => item.type === "session" && item.server === route.server && item.sessionId === parentID,
|
||||
)
|
||||
if (parent) return parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentTab = () => matchRoute(layout.route())
|
||||
|
||||
createEffect(() => {
|
||||
const route = layout.route()
|
||||
if (!tabs.ready()) return
|
||||
const tab = currentTab()
|
||||
if (tab) return
|
||||
|
||||
if (route.type === "session") {
|
||||
const sync = serverSync.createDirSyncContext(route.dir)
|
||||
const session = sync.session.get(route.sessionId)
|
||||
if (!session) return
|
||||
const sessionId = session.parentID ?? session.id
|
||||
const next = {
|
||||
server: route.server ?? server.key,
|
||||
dirBase64: route.dirBase64,
|
||||
sessionId,
|
||||
}
|
||||
tabsStoreActions.addSessionTab(next)
|
||||
}
|
||||
})
|
||||
|
||||
makeEventListener(window, SESSION_TABS_REMOVED_EVENT, (event) => {
|
||||
@@ -332,49 +328,12 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
tabsStoreActions.removeSessions(detail)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const params = useParams()
|
||||
if (!(params.dir && params.id)) return
|
||||
|
||||
tabsStoreActions.addTab({
|
||||
dir: decodeDirectory(params.dir) ?? "",
|
||||
sessionId: params.id,
|
||||
href: makeSessionHref(params.dir, params.id),
|
||||
})
|
||||
})
|
||||
|
||||
const projects = createMemo(() => layout.projects.list())
|
||||
const projectByID = createMemo(
|
||||
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
|
||||
)
|
||||
|
||||
const currentSessionTab = () => {
|
||||
if (!params.dir || !params.id) return
|
||||
const href = makeSessionHref(params.dir, params.id)
|
||||
return tabsStore.find((tab) => tab.href === href)
|
||||
}
|
||||
|
||||
const closeCurrentSessionTab = () => {
|
||||
const tab = currentSessionTab()
|
||||
if (!tab) return false
|
||||
tabsStoreActions.removeTab(tab.href)
|
||||
return true
|
||||
}
|
||||
|
||||
const closeNewSessionTab = () => {
|
||||
if (!(params.dir && !params.id)) return false
|
||||
const last = tabsStore[tabsStore.length - 1]
|
||||
if (last) navigate(last.href)
|
||||
else navigate("/")
|
||||
return true
|
||||
}
|
||||
|
||||
const openNewTab = () => navigate(newSessionHref())
|
||||
|
||||
const closeActiveTab = () => closeCurrentSessionTab() || closeNewSessionTab()
|
||||
command.register("tabs", () => {
|
||||
const current = currentTab()
|
||||
|
||||
command.register(() => {
|
||||
const commands = [
|
||||
return [
|
||||
{
|
||||
id: "tab.new",
|
||||
category: "tab",
|
||||
@@ -383,13 +342,15 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
hidden: true,
|
||||
onSelect: openNewTab,
|
||||
},
|
||||
{
|
||||
current && {
|
||||
id: "tab.close",
|
||||
category: "tab",
|
||||
title: language.t("command.tab.close"),
|
||||
keybind: "mod+w",
|
||||
hidden: true,
|
||||
onSelect: closeActiveTab,
|
||||
onSelect: () => {
|
||||
tabsStoreActions.removeTab(tabsStore.findIndex((tab) => current === tab))
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `tab.prev`,
|
||||
@@ -398,14 +359,14 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
keybind: `mod+option+ArrowLeft`,
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
|
||||
let index = tabsStore.findIndex((tab) => tab === currentTab())
|
||||
if (index === -1) return
|
||||
|
||||
index -= 1
|
||||
if (index === -1) index = tabsStore.length - 1
|
||||
|
||||
const next = tabsStore[index]
|
||||
if (next) navigate(next.href)
|
||||
if (next) navigateTab(next)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -415,14 +376,14 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
keybind: `mod+option+ArrowRight`,
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
|
||||
let index = tabsStore.findIndex((tab) => tab === currentTab())
|
||||
if (index === -1) return
|
||||
|
||||
index += 1
|
||||
if (index === tabsStore.length) index = 0
|
||||
|
||||
const next = tabsStore[index]
|
||||
if (next) navigate(next.href)
|
||||
if (next) navigateTab(next)
|
||||
},
|
||||
},
|
||||
...Array.from({ length: 9 }, (_, i) => {
|
||||
@@ -437,31 +398,23 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
const tab = tabsStore[index]
|
||||
if (tab) navigate(tab.href)
|
||||
if (tab) navigateTab(tab)
|
||||
},
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
return commands
|
||||
].filter((v) => v !== undefined)
|
||||
})
|
||||
|
||||
const tabsEnriched = iife(() => {
|
||||
const base = mapArray(
|
||||
() => tabsStore,
|
||||
(tab) => {
|
||||
const sync = serverSync.createDirSyncContext(tab.dir)
|
||||
const session = sync.session.get(tab.sessionId)
|
||||
return session ? { ...tab, info: session } : null
|
||||
},
|
||||
)
|
||||
const [tabsAreOverflowing, setTabsAreOverflowing] = createSignal(false)
|
||||
let tabScrollRef!: HTMLDivElement
|
||||
|
||||
return () => base().flatMap((s) => (s ? [s] : []))
|
||||
})
|
||||
function refreshTabsAreOverflowing() {
|
||||
setTabsAreOverflowing(tabScrollRef.scrollWidth > tabScrollRef.clientWidth)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="h-full flex-1 flex flex-row items-center gap-1.5 pr-3 pt-2"
|
||||
class="h-full flex-1 overflow-hidden flex flex-row items-center gap-1.5 pr-3 pt-2"
|
||||
classList={{
|
||||
"pl-2": mac(),
|
||||
"pl-4": !mac(),
|
||||
@@ -476,54 +429,89 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
size="large"
|
||||
as="a"
|
||||
href="/"
|
||||
class="!w-9"
|
||||
class="!w-9 shrink-0"
|
||||
icon={<IconV2 name="grid-plus" />}
|
||||
state={!!homeMatch() ? "pressed" : undefined}
|
||||
/>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden">
|
||||
<div class="flex min-w-0 flex-row items-center gap-1.5 overflow-hidden">
|
||||
<For each={tabsEnriched()}>
|
||||
{(tab, i) => (
|
||||
<>
|
||||
{i() !== 0 && (
|
||||
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
||||
)}
|
||||
<TabNavItem
|
||||
href={tab.href}
|
||||
title={tab.info.title}
|
||||
project={projectForSession(tab.info, projects(), projectByID())}
|
||||
directory={tab.dir}
|
||||
sessionId={tab.info.id}
|
||||
onClose={() => tabsStoreActions.removeTab(tab.href)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar [app-region:no-drag]"
|
||||
ref={tabScrollRef}
|
||||
>
|
||||
<div class="flex min-w-0 flex-row items-center gap-1.5">
|
||||
<For each={tabsStore}>
|
||||
{(tab, i) => {
|
||||
let ref!: HTMLDivElement
|
||||
|
||||
onMount(() => {
|
||||
refreshTabsAreOverflowing()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{i() !== 0 && (
|
||||
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
||||
)}
|
||||
<TabNavItem
|
||||
ref={ref}
|
||||
href={tabHref(tab)}
|
||||
server={tab.server}
|
||||
directory={decode64(tab.dirBase64)!}
|
||||
sessionId={tab.sessionId}
|
||||
onNavigate={() => {
|
||||
navigateTab(tab)
|
||||
|
||||
ref.scrollIntoView({ behavior: "instant" })
|
||||
}}
|
||||
onClose={() => tabsStoreActions.removeTab(i())}
|
||||
active={currentTab() === tab}
|
||||
activeServer={tab.server === server.key}
|
||||
forceTruncate={tabsAreOverflowing()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={creating() && params.dir}>
|
||||
{(_) => {
|
||||
let ref!: HTMLDivElement
|
||||
|
||||
onMount(() => {
|
||||
ref.scrollIntoView({ behavior: "instant" })
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
|
||||
<NewSessionTabItem
|
||||
ref={ref}
|
||||
href={`/${params.dir}/session`}
|
||||
title={language.t("command.session.new")}
|
||||
onClose={() => {
|
||||
const tab = tabsStore.at(-1)
|
||||
if (tab) navigateTab(tab)
|
||||
else navigate("/")
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
<Show
|
||||
when={creating() && params.dir}
|
||||
fallback={
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<IconV2 name="plus" />}
|
||||
as="a"
|
||||
href={newSessionHref()}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NewSessionTabItem
|
||||
href={`/${params.dir}/session`}
|
||||
title={language.t("command.session.new")}
|
||||
onClose={() => navigate(tabsEnriched().at(-1)?.href ?? "/")}
|
||||
/>
|
||||
</Show>
|
||||
<div class="min-w-0 flex-1" />
|
||||
</div>
|
||||
<Show when={!(creating() && params.dir)}>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<IconV2 name="plus" />}
|
||||
as="a"
|
||||
href={newSessionHref()}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</Show>
|
||||
<div class="flex-1" />
|
||||
<TitlebarV2Right state={v2RightState()} />
|
||||
<Show when={windows() && !electronWindows()}>
|
||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||
@@ -745,41 +733,85 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
|
||||
}
|
||||
|
||||
function TabNavItem(props: {
|
||||
ref?: HTMLDivElement
|
||||
href: string
|
||||
title: string
|
||||
project?: LocalProject
|
||||
server: ServerConnection.Key
|
||||
directory: string
|
||||
sessionId: string
|
||||
sessionId?: string
|
||||
hideClose?: boolean
|
||||
onClose: () => void
|
||||
onNavigate: () => void
|
||||
active?: boolean
|
||||
activeServer: boolean
|
||||
forceTruncate?: boolean
|
||||
}) {
|
||||
const match = useMatch(() => props.href)
|
||||
const isActive = () => !!match()
|
||||
const closeTab = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.onClose()
|
||||
}
|
||||
const global = useGlobal()
|
||||
const serverCtx = createMemo(() => {
|
||||
const conn = global.servers.list().find((item) => ServerConnection.key(item) === props.server)
|
||||
if (conn) return global.createServerCtx(conn)
|
||||
})
|
||||
const dirSyncCtx = createMemo(() => serverCtx()?.sync.createDirSyncContext(props.directory))
|
||||
|
||||
const [session] = createResource(
|
||||
() => {
|
||||
const ctx = dirSyncCtx()
|
||||
if (!ctx || !props.sessionId) return
|
||||
return [props.sessionId, ctx] as const
|
||||
},
|
||||
async ([sessionId, dirSyncCtx]) => {
|
||||
await dirSyncCtx.session.sync(sessionId).catch(() => {})
|
||||
return dirSyncCtx.session.get(sessionId)
|
||||
},
|
||||
{ initialValue: props.sessionId ? dirSyncCtx()?.session.get(props.sessionId) : undefined },
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={props.ref}
|
||||
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||
data-active={isActive()}
|
||||
data-active={props.active}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== 1) return
|
||||
closeTab(event)
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={props.href}
|
||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
|
||||
>
|
||||
<span data-slot="project-avatar-slot">
|
||||
<ProjectTabAvatar project={props.project} directory={props.directory} sessionId={props.sessionId} />
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">{props.title}</span>
|
||||
</a>
|
||||
<Show when={session.latest}>
|
||||
{(session) => {
|
||||
console.log({ session: session() })
|
||||
const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
|
||||
|
||||
<div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
||||
return (
|
||||
<a
|
||||
href={props.href}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
props.onNavigate()
|
||||
}}
|
||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
|
||||
>
|
||||
<span data-slot="project-avatar-slot">
|
||||
<ProjectTabAvatar
|
||||
project={project()}
|
||||
directory={props.directory}
|
||||
sessionId={session().id}
|
||||
activeServer={props.activeServer}
|
||||
/>
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">{session().title}</span>
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class="absolute not-group-hover:not-group-data-[active=true]:not-data-[truncate=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 data-[truncate=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2"
|
||||
data-truncate={props.forceTruncate}
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 rounded-r-[6px] bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
||||
style={{
|
||||
@@ -799,10 +831,15 @@ function TabNavItem(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectTabAvatar(props: { project?: LocalProject; directory: string; sessionId: string }) {
|
||||
function ProjectTabAvatar(props: {
|
||||
project?: LocalProject
|
||||
directory: string
|
||||
sessionId: string
|
||||
activeServer: boolean
|
||||
}) {
|
||||
const directory = () => props.directory
|
||||
const sessionId = () => props.sessionId
|
||||
const state = useSessionTabAvatarState(directory, sessionId)
|
||||
const state = useSessionTabAvatarState(directory, sessionId, () => props.activeServer)
|
||||
return (
|
||||
<ProjectAvatar
|
||||
fallback={displayName(props.project ?? { worktree: props.directory })}
|
||||
@@ -814,7 +851,7 @@ function ProjectTabAvatar(props: { project?: LocalProject; directory: string; se
|
||||
)
|
||||
}
|
||||
|
||||
function NewSessionTabItem(props: { href: string; title: string; onClose: () => void }) {
|
||||
function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) {
|
||||
const closeTab = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -822,7 +859,8 @@ function NewSessionTabItem(props: { href: string; title: string; onClose: () =>
|
||||
}
|
||||
return (
|
||||
<div
|
||||
class="group relative flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
|
||||
ref={props.ref}
|
||||
class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== 1) return
|
||||
closeTab(event)
|
||||
|
||||
@@ -3,6 +3,8 @@ import { createStore, reconcile, type SetStoreFunction, type Store } from "solid
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
import { createScopedCache } from "@/utils/scoped-cache"
|
||||
import { uuid } from "@/utils/uuid"
|
||||
import type { SelectedLineRange } from "@/context/file"
|
||||
@@ -166,11 +168,11 @@ export function createCommentSessionForTest(comments: Record<string, LineComment
|
||||
return createCommentSessionState(store, setStore)
|
||||
}
|
||||
|
||||
function createCommentSession(dir: string, id: string | undefined) {
|
||||
function createCommentSession(scope: ServerScope, dir: string, id: string | undefined) {
|
||||
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.scoped(dir, id, "comments", [legacy]),
|
||||
Persist.serverScoped(scope, dir, id, "comments", [legacy]),
|
||||
createStore<CommentStore>({
|
||||
comments: {},
|
||||
}),
|
||||
@@ -200,11 +202,16 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
|
||||
gate: false,
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const serverSDK = useServerSDK()
|
||||
const cache = createScopedCache(
|
||||
(key) => {
|
||||
const decoded = decodeSessionKey(key)
|
||||
return createRoot((dispose) => ({
|
||||
value: createCommentSession(decoded.dir, decoded.id === WORKSPACE_KEY ? undefined : decoded.id),
|
||||
value: createCommentSession(
|
||||
serverSDK.scope,
|
||||
decoded.dir,
|
||||
decoded.id === WORKSPACE_KEY ? undefined : decoded.id,
|
||||
),
|
||||
dispose,
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
getSessionPrefetchPromise,
|
||||
setSessionPrefetch,
|
||||
} from "./global-sync/session-prefetch"
|
||||
import { createServerSyncContext } from "./server-sync"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { createServerSdkContext, useServerSDK } from "./server-sdk"
|
||||
import { type createServerSyncContextInner } from "./server-sync"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
@@ -171,8 +171,11 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
|
||||
})
|
||||
}
|
||||
|
||||
export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
|
||||
const serverSDK = useServerSDK()
|
||||
export const createDirSyncContext = (
|
||||
directory: string,
|
||||
serverSync: ReturnType<typeof createServerSyncContextInner>,
|
||||
serverSDK: ReturnType<typeof createServerSdkContext> = useServerSDK(),
|
||||
) => {
|
||||
const client = serverSDK.createClient({ directory, throwOnError: true })
|
||||
|
||||
type Child = ReturnType<(typeof serverSync)["child"]>
|
||||
@@ -273,7 +276,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
||||
|
||||
const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
|
||||
if (sessionIDs.length === 0) return
|
||||
clearSessionPrefetch(directory, sessionIDs)
|
||||
clearSessionPrefetch(serverSDK.scope, directory, sessionIDs)
|
||||
for (const sessionID of sessionIDs) {
|
||||
serverSync.todo.set(sessionID, undefined)
|
||||
}
|
||||
@@ -345,6 +348,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
||||
setMeta("cursor", key, next.cursor)
|
||||
setMeta("complete", key, next.complete)
|
||||
setSessionPrefetch({
|
||||
scope: serverSDK.scope,
|
||||
directory: input.directory,
|
||||
sessionID: input.sessionID,
|
||||
limit: message.length,
|
||||
@@ -435,7 +439,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
||||
|
||||
touch(directory, setStore, sessionID)
|
||||
|
||||
const seeded = getSessionPrefetch(directory, sessionID)
|
||||
const seeded = getSessionPrefetch(serverSDK.scope, directory, sessionID)
|
||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||
batch(() => {
|
||||
setMeta("limit", key, seeded.limit)
|
||||
@@ -446,10 +450,10 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
|
||||
}
|
||||
|
||||
return runInflight(inflight, key, async () => {
|
||||
const pending = getSessionPrefetchPromise(directory, sessionID)
|
||||
const pending = getSessionPrefetchPromise(serverSDK.scope, directory, sessionID)
|
||||
if (pending) {
|
||||
await pending
|
||||
const seeded = getSessionPrefetch(directory, sessionID)
|
||||
const seeded = getSessionPrefetch(serverSDK.scope, directory, sessionID)
|
||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||
batch(() => {
|
||||
setMeta("limit", key, seeded.limit)
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
touchFileContent,
|
||||
} from "./file/content-cache"
|
||||
import { createFileViewCache } from "./file/view-cache"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope"
|
||||
import { createFileTreeStore } from "./file/tree-store"
|
||||
import { invalidateFromWatcher } from "./file/watcher"
|
||||
import {
|
||||
@@ -56,12 +58,15 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
const sdk = useSDK()
|
||||
useSync()
|
||||
const params = useParams()
|
||||
const serverSDK = useServerSDK()
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
|
||||
const scope = createMemo(() => sdk.directory)
|
||||
const path = createPathHelpers(scope)
|
||||
const tabs = layout.tabs(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = layout.tabs(() =>
|
||||
SessionStateKey.from(serverSDK.scope, SessionRouteKey.fromRoute(params.dir, params.id)),
|
||||
)
|
||||
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const [store, setStore] = createStore<{
|
||||
@@ -107,7 +112,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
})
|
||||
})
|
||||
|
||||
const viewCache = createFileViewCache()
|
||||
const viewCache = createFileViewCache(serverSDK.scope)
|
||||
const view = createMemo(() => viewCache.load(scope(), params.id))
|
||||
|
||||
const ensure = (file: string) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createStore, produce } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { createScopedCache } from "@/utils/scoped-cache"
|
||||
import type { FileViewState, SelectedLineRange } from "./types"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
const MAX_FILE_VIEW_SESSIONS = 20
|
||||
@@ -33,11 +34,11 @@ function equalSelectedLines(a: SelectedLineRange | null | undefined, b: Selected
|
||||
)
|
||||
}
|
||||
|
||||
function createViewSession(dir: string, id: string | undefined) {
|
||||
function createViewSession(scope: ServerScope, dir: string, id: string | undefined) {
|
||||
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
|
||||
|
||||
const [view, setView, _, ready] = persisted(
|
||||
Persist.scoped(dir, id, "file-view", [legacyViewKey]),
|
||||
Persist.serverScoped(scope, dir, id, "file-view", [legacyViewKey]),
|
||||
createStore<{
|
||||
file: Record<string, FileViewState>
|
||||
}>({
|
||||
@@ -119,14 +120,14 @@ function createViewSession(dir: string, id: string | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
export function createFileViewCache() {
|
||||
export function createFileViewCache(scope: ServerScope) {
|
||||
const cache = createScopedCache(
|
||||
(key) => {
|
||||
const split = key.lastIndexOf("\n")
|
||||
const dir = split >= 0 ? key.slice(0, split) : key
|
||||
const id = split >= 0 ? key.slice(split + 1) : WORKSPACE_KEY
|
||||
return createRoot((dispose) => ({
|
||||
value: createViewSession(dir, id === WORKSPACE_KEY ? undefined : id),
|
||||
value: createViewSession(scope, dir, id === WORKSPACE_KEY ? undefined : id),
|
||||
dispose,
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -3,8 +3,9 @@ import { createStore } from "solid-js/store"
|
||||
import { QueryClient } from "@tanstack/solid-query"
|
||||
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import { bootstrapDirectory } from "./bootstrap"
|
||||
import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
||||
import type { State, VcsCache } from "./types"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||
|
||||
@@ -45,6 +46,7 @@ describe("bootstrapDirectory", () => {
|
||||
|
||||
await bootstrapDirectory({
|
||||
directory: "/project",
|
||||
scope: ServerScope.local,
|
||||
mcp: false,
|
||||
global: {
|
||||
config: {} satisfies Config,
|
||||
@@ -89,3 +91,18 @@ describe("bootstrapDirectory", () => {
|
||||
expect(mcpReads).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("query keys", () => {
|
||||
test("partitions identical directories by server scope", () => {
|
||||
const client = {} as OpencodeClient
|
||||
const remote = "https://debian.example" as typeof ServerScope.local
|
||||
|
||||
expect([...loadPathQuery(ServerScope.local, "/repo", client).queryKey]).toEqual(["local", "/repo", "path"])
|
||||
expect([...loadPathQuery(remote, "/repo", client).queryKey]).toEqual(["https://debian.example", "/repo", "path"])
|
||||
expect([...loadProvidersQuery(remote, null, client).queryKey]).toEqual([
|
||||
"https://debian.example",
|
||||
null,
|
||||
"providers",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||
import { loadMcpQuery } from "../server-sync"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -59,8 +60,8 @@ function errors(list: PromiseSettledResult<unknown>[]) {
|
||||
|
||||
const providerRev = new Map<string, number>()
|
||||
|
||||
export function clearProviderRev(directory: string) {
|
||||
providerRev.delete(directory)
|
||||
export function clearProviderRev(scope: ServerScope, directory: string) {
|
||||
providerRev.delete(ScopedKey.from(scope, directory))
|
||||
}
|
||||
|
||||
function runAll(list: Array<() => Promise<unknown>>) {
|
||||
@@ -83,15 +84,15 @@ function showErrors(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export const loadGlobalConfigQuery = (sdk: OpencodeClient) =>
|
||||
export const loadGlobalConfigQuery = (scope: ServerScope, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: ["config"],
|
||||
queryKey: [scope, "config"],
|
||||
queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)),
|
||||
})
|
||||
|
||||
export const loadProjectsQuery = (sdk: OpencodeClient) =>
|
||||
export const loadProjectsQuery = (scope: ServerScope, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: ["project"],
|
||||
queryKey: [scope, "project"],
|
||||
queryFn: () =>
|
||||
retry(() =>
|
||||
sdk.project.list().then((x) => {
|
||||
@@ -106,6 +107,7 @@ export const loadProjectsQuery = (sdk: OpencodeClient) =>
|
||||
|
||||
export async function bootstrapGlobal(input: {
|
||||
serverSDK: OpencodeClient
|
||||
scope: ServerScope
|
||||
requestFailedTitle: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
@@ -113,12 +115,12 @@ export async function bootstrapGlobal(input: {
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
const slow = [
|
||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.serverSDK)),
|
||||
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.serverSDK)),
|
||||
() => input.queryClient.fetchQuery(loadPathQuery(null, input.serverSDK)),
|
||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.serverSDK)),
|
||||
() => input.queryClient.fetchQuery(loadProvidersQuery(input.scope, null, input.serverSDK)),
|
||||
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverSDK)),
|
||||
() =>
|
||||
input.queryClient
|
||||
.fetchQuery(loadProjectsQuery(input.serverSDK))
|
||||
.fetchQuery(loadProjectsQuery(input.scope, input.serverSDK))
|
||||
.then((data) => input.setGlobalStore("project", data)),
|
||||
]
|
||||
await runAll(slow)
|
||||
@@ -178,26 +180,27 @@ function warmSessions(input: {
|
||||
).then(() => undefined)
|
||||
}
|
||||
|
||||
export const loadProvidersQuery = (directory: string | null, sdk: OpencodeClient) =>
|
||||
export const loadProvidersQuery = (scope: ServerScope, directory: string | null, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: [directory, "providers"],
|
||||
queryKey: [scope, directory, "providers"],
|
||||
queryFn: () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))),
|
||||
})
|
||||
|
||||
export const loadAgentsQuery = (directory: string | null, sdk: OpencodeClient) =>
|
||||
export const loadAgentsQuery = (scope: ServerScope, directory: string | null, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: [directory, "agents"],
|
||||
queryKey: [scope, directory, "agents"],
|
||||
queryFn: () => retry(() => sdk.app.agents().then((x) => normalizeAgentList(x.data))),
|
||||
})
|
||||
|
||||
export const loadPathQuery = (directory: string | null, sdk: OpencodeClient) =>
|
||||
export const loadPathQuery = (scope: ServerScope, directory: string | null, sdk: OpencodeClient) =>
|
||||
queryOptions<Path>({
|
||||
queryKey: [directory, "path"],
|
||||
queryKey: [scope, directory, "path"],
|
||||
queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)),
|
||||
})
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
directory: string
|
||||
scope: ServerScope
|
||||
mcp: boolean
|
||||
sdk: OpencodeClient
|
||||
store: Store<State>
|
||||
@@ -223,14 +226,15 @@ export async function bootstrapDirectory(input: {
|
||||
}
|
||||
if (loading) input.setStore("status", "partial")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
const revKey = ScopedKey.from(input.scope, input.directory)
|
||||
const rev = (providerRev.get(revKey) ?? 0) + 1
|
||||
providerRev.set(revKey, rev)
|
||||
;(async () => {
|
||||
const slow = [
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
input.queryClient
|
||||
.ensureQueryData(loadAgentsQuery(input.directory, input.sdk))
|
||||
.ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.sdk))
|
||||
.then((data) => input.setStore("agent", data)),
|
||||
() =>
|
||||
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
||||
@@ -239,7 +243,7 @@ export async function bootstrapDirectory(input: {
|
||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
||||
!seededPath &&
|
||||
(() =>
|
||||
input.queryClient.ensureQueryData(loadPathQuery(input.directory, input.sdk)).then((data) => {
|
||||
input.queryClient.ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk)).then((data) => {
|
||||
const next = projectID(data.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
})),
|
||||
@@ -305,9 +309,9 @@ export async function bootstrapDirectory(input: {
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk))),
|
||||
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.sdk))),
|
||||
() =>
|
||||
input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
|
||||
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.sdk)).catch((err) => {
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
|
||||
@@ -4,9 +4,16 @@ import { createStore } from "solid-js/store"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import type { State } from "./types"
|
||||
import type { QueryOptionsApi } from "../server-sync"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
|
||||
const querySingles: Array<() => { queryKey?: unknown[]; enabled?: boolean }> = []
|
||||
const persist: typeof import("@/utils/persist").persisted = (_target, store) => [
|
||||
store[0],
|
||||
store[1],
|
||||
null,
|
||||
Object.assign(() => true, { promise: undefined }),
|
||||
]
|
||||
|
||||
const child = () => createStore({} as State)
|
||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||
@@ -42,12 +49,6 @@ function createOwner(callback: (owner: Owner) => void) {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
mock.module("@/utils/persist", () => ({
|
||||
Persist: {
|
||||
workspace: (...parts: string[]) => parts.join(":"),
|
||||
},
|
||||
persisted: (_target: string, store: unknown[]) => [store[0], store[1], null, () => true],
|
||||
}))
|
||||
mock.module("@tanstack/solid-query", () => ({
|
||||
useQuery: (options: () => { queryKey?: unknown[]; enabled?: boolean }) => {
|
||||
querySingles.push(options)
|
||||
@@ -80,6 +81,8 @@ describe("createChildStoreManager", () => {
|
||||
|
||||
const manager = createChildStoreManager({
|
||||
owner,
|
||||
scope: ServerScope.local,
|
||||
persist,
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap() {},
|
||||
@@ -109,6 +112,8 @@ describe("createChildStoreManager", () => {
|
||||
const dispose = createOwner((owner) => {
|
||||
manager = createChildStoreManager({
|
||||
owner,
|
||||
scope: ServerScope.local,
|
||||
persist,
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap(directory) {
|
||||
@@ -140,6 +145,8 @@ describe("createChildStoreManager", () => {
|
||||
const dispose = createOwner((owner) => {
|
||||
manager = createChildStoreManager({
|
||||
owner,
|
||||
scope: ServerScope.local,
|
||||
persist,
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap() {},
|
||||
@@ -171,6 +178,8 @@ describe("createChildStoreManager", () => {
|
||||
const dispose = createOwner((owner) => {
|
||||
manager = createChildStoreManager({
|
||||
owner,
|
||||
scope: ServerScope.local,
|
||||
persist,
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap() {},
|
||||
|
||||
@@ -18,9 +18,12 @@ import { useQuery } from "@tanstack/solid-query"
|
||||
import { QueryOptionsApi } from "../server-sync"
|
||||
import { directoryKey, type DirectoryKey } from "./utils"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
owner: Owner
|
||||
scope: ServerScope
|
||||
persist: typeof persisted
|
||||
isBooting: (directory: string) => boolean
|
||||
isLoadingSessions: (directory: string) => boolean
|
||||
onBootstrap: (directory: string) => void
|
||||
@@ -147,8 +150,8 @@ export function createChildStoreManager(input: {
|
||||
if (!key) console.error("No directory provided")
|
||||
if (!children[key]) {
|
||||
const vcs = runWithOwner(input.owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||
input.persist(
|
||||
Persist.serverWorkspace(input.scope, directory, "vcs", ["vcs.v1"]),
|
||||
createStore({ value: undefined as VcsInfo | undefined }),
|
||||
),
|
||||
)
|
||||
@@ -157,8 +160,8 @@ export function createChildStoreManager(input: {
|
||||
vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
||||
|
||||
const meta = runWithOwner(input.owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "project", ["project.v1"]),
|
||||
input.persist(
|
||||
Persist.serverWorkspace(input.scope, directory, "project", ["project.v1"]),
|
||||
createStore({ value: undefined as ProjectMeta | undefined }),
|
||||
),
|
||||
)
|
||||
@@ -166,8 +169,8 @@ export function createChildStoreManager(input: {
|
||||
metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
||||
|
||||
const icon = runWithOwner(input.owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "icon", ["icon.v1"]),
|
||||
input.persist(
|
||||
Persist.serverWorkspace(input.scope, directory, "icon", ["icon.v1"]),
|
||||
createStore({ value: undefined as string | undefined }),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,14 +7,18 @@ import {
|
||||
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("/tmp/a", ["ses_1"])
|
||||
clearSessionPrefetch("/tmp/b", ["ses_1"])
|
||||
clearSessionPrefetch(scope, "/tmp/a", ["ses_1"])
|
||||
clearSessionPrefetch(scope, "/tmp/b", ["ses_1"])
|
||||
|
||||
setSessionPrefetch({
|
||||
directory: "/tmp/a",
|
||||
scope,
|
||||
sessionID: "ses_1",
|
||||
limit: 200,
|
||||
cursor: "abc",
|
||||
@@ -22,21 +26,27 @@ describe("session prefetch", () => {
|
||||
at: 123,
|
||||
})
|
||||
|
||||
expect(getSessionPrefetch("/tmp/a", "ses_1")).toEqual({ limit: 200, cursor: "abc", complete: false, at: 123 })
|
||||
expect(getSessionPrefetch("/tmp/b", "ses_1")).toBeUndefined()
|
||||
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("/tmp/a", ["ses_1"])
|
||||
clearSessionPrefetch(scope, "/tmp/a", ["ses_1"])
|
||||
|
||||
expect(getSessionPrefetch("/tmp/a", "ses_1")).toBeUndefined()
|
||||
expect(getSessionPrefetch(scope, "/tmp/a", "ses_1")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("dedupes inflight work", async () => {
|
||||
clearSessionPrefetch("/tmp/c", ["ses_2"])
|
||||
clearSessionPrefetch(scope, "/tmp/c", ["ses_2"])
|
||||
|
||||
let calls = 0
|
||||
const run = () =>
|
||||
runSessionPrefetch({
|
||||
directory: "/tmp/c",
|
||||
scope,
|
||||
sessionID: "ses_2",
|
||||
task: async () => {
|
||||
calls += 1
|
||||
@@ -52,15 +62,48 @@ describe("session prefetch", () => {
|
||||
})
|
||||
|
||||
test("clears a whole directory", () => {
|
||||
setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_1", limit: 10, cursor: "a", complete: true, at: 1 })
|
||||
setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_2", limit: 20, cursor: "b", complete: false, at: 2 })
|
||||
setSessionPrefetch({ directory: "/tmp/e", sessionID: "ses_1", limit: 30, cursor: "c", complete: true, at: 3 })
|
||||
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("/tmp/d")
|
||||
clearSessionPrefetchDirectory(scope, "/tmp/d")
|
||||
|
||||
expect(getSessionPrefetch("/tmp/d", "ses_1")).toBeUndefined()
|
||||
expect(getSessionPrefetch("/tmp/d", "ses_2")).toBeUndefined()
|
||||
expect(getSessionPrefetch("/tmp/e", "ses_1")).toEqual({ limit: 30, cursor: "c", complete: true, at: 3 })
|
||||
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", () => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const key = (directory: string, sessionID: string) => `${directory}\n${sessionID}`
|
||||
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
|
||||
|
||||
@@ -27,28 +29,32 @@ const rev = new Map<string, number>()
|
||||
|
||||
const version = (id: string) => rev.get(id) ?? 0
|
||||
|
||||
export function getSessionPrefetch(directory: string, sessionID: string) {
|
||||
return cache.get(key(directory, sessionID))
|
||||
export function getSessionPrefetch(scope: ServerScope, directory: string, sessionID: string) {
|
||||
return cache.get(key(scope, directory, sessionID))
|
||||
}
|
||||
|
||||
export function getSessionPrefetchPromise(directory: string, sessionID: string) {
|
||||
return inflight.get(key(directory, sessionID))
|
||||
export function getSessionPrefetchPromise(scope: ServerScope, directory: string, sessionID: string) {
|
||||
return inflight.get(key(scope, directory, sessionID))
|
||||
}
|
||||
|
||||
export function clearSessionPrefetchInflight() {
|
||||
inflight.clear()
|
||||
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(directory: string, sessionID: string, value: number) {
|
||||
return version(key(directory, sessionID)) === value
|
||||
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.directory, input.sessionID)
|
||||
const id = key(input.scope, input.directory, input.sessionID)
|
||||
const pending = inflight.get(id)
|
||||
if (pending) return pending
|
||||
|
||||
@@ -64,13 +70,14 @@ export function runSessionPrefetch(input: {
|
||||
|
||||
export function setSessionPrefetch(input: {
|
||||
directory: string
|
||||
scope: ServerScope
|
||||
sessionID: string
|
||||
limit: number
|
||||
cursor?: string
|
||||
complete: boolean
|
||||
at?: number
|
||||
}) {
|
||||
cache.set(key(input.directory, input.sessionID), {
|
||||
cache.set(key(input.scope, input.directory, input.sessionID), {
|
||||
limit: input.limit,
|
||||
cursor: input.cursor,
|
||||
complete: input.complete,
|
||||
@@ -78,18 +85,18 @@ export function setSessionPrefetch(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export function clearSessionPrefetch(directory: string, sessionIDs: Iterable<string>) {
|
||||
export function clearSessionPrefetch(scope: ServerScope, directory: string, sessionIDs: Iterable<string>) {
|
||||
for (const sessionID of sessionIDs) {
|
||||
if (!sessionID) continue
|
||||
const id = key(directory, sessionID)
|
||||
const id = key(scope, directory, sessionID)
|
||||
rev.set(id, version(id) + 1)
|
||||
cache.delete(id)
|
||||
inflight.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSessionPrefetchDirectory(directory: string) {
|
||||
const prefix = `${directory}\n`
|
||||
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
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createEffect, createMemo, createRoot } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { createServerProjects, ServerConnection, useServer } from "./server"
|
||||
import { useServerHealth } from "@/utils/server-health"
|
||||
import { QueryClient } from "@tanstack/solid-query"
|
||||
import { createServerSdkContext } from "./server-sdk"
|
||||
import { createServerSyncContext } from "./server-sync"
|
||||
import { getOwner } from "solid-js/web"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { QueryClient } from "@tanstack/solid-query"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({
|
||||
name: "Global",
|
||||
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
|
||||
init: () => {
|
||||
const server = useServer()
|
||||
const serverHealth = useServerHealth(
|
||||
() => server.list,
|
||||
@@ -23,8 +23,6 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
||||
},
|
||||
})
|
||||
|
||||
const serversAndProjects = createServersAndProjectStore()
|
||||
|
||||
const settingsServer = createMemo(() => {
|
||||
const list = server.list
|
||||
return list.find((conn) => ServerConnection.key(conn) === store.settings.serverKey) ?? list[0]
|
||||
@@ -43,18 +41,25 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
||||
|
||||
const owner = getOwner()
|
||||
|
||||
const ensureServerCtx = (conn: ServerConnection.Any) => {
|
||||
const key = ServerConnection.key(conn)
|
||||
const existing = serverCtxs.get(key)
|
||||
if (existing) return existing.serverCtx
|
||||
const root = createRoot((dispose) => {
|
||||
const serverCtx = createServerCtx(conn, server.scope(key), server.projects.forServer(key))
|
||||
return { dispose, serverCtx }
|
||||
}, owner as any)
|
||||
serverCtxs.set(key, root)
|
||||
return root.serverCtx
|
||||
}
|
||||
|
||||
createMemo(() => {
|
||||
for (const conn of server.list) {
|
||||
const key = ServerConnection.key(conn)
|
||||
if (!serverCtxs.has(key)) {
|
||||
const root = createRoot((dispose) => {
|
||||
const serverCtx = createServerCtx(conn, serversAndProjects)
|
||||
return { dispose, serverCtx }
|
||||
}, owner as any)
|
||||
serverCtxs.set(key, root)
|
||||
}
|
||||
ensureServerCtx(conn)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
for (const [key] of serverCtxs) {
|
||||
if (!server.list.find((conn) => ServerConnection.key(conn) === key)) {
|
||||
const { dispose } = serverCtxs.get(key)!
|
||||
@@ -64,14 +69,9 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
||||
}
|
||||
})
|
||||
|
||||
const allServers = createMemo(
|
||||
(): Array<ServerConnection.Any> =>
|
||||
resolveServerList({ stored: serversAndProjects.store.list, props: props.servers }),
|
||||
)
|
||||
|
||||
return {
|
||||
servers: {
|
||||
list: allServers,
|
||||
list: () => server.list,
|
||||
health: serverHealth,
|
||||
},
|
||||
settings: {
|
||||
@@ -86,33 +86,16 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
||||
},
|
||||
},
|
||||
createServerCtx(conn: ServerConnection.Any) {
|
||||
const key = ServerConnection.key(conn)
|
||||
const ctx = serverCtxs.get(key)
|
||||
if (!ctx) return createServerCtx(conn, serversAndProjects)
|
||||
return ctx.serverCtx
|
||||
return ensureServerCtx(conn)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
type StoredProject = { worktree: string; expanded: boolean }
|
||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
||||
|
||||
const createServersAndProjectStore = () => {
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("server", ["server.v3"]),
|
||||
createStore({
|
||||
list: [] as StoredServer[],
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
lastProject: {} as Record<string, string>,
|
||||
}),
|
||||
)
|
||||
return { store, setStore, ready }
|
||||
}
|
||||
|
||||
function createServerCtx(
|
||||
conn: ServerConnection.Any,
|
||||
{ store, setStore }: ReturnType<typeof createServersAndProjectStore>,
|
||||
scope: ServerScope,
|
||||
projects: ReturnType<typeof createServerProjects>,
|
||||
) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -123,13 +106,9 @@ function createServerCtx(
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const sdk = createServerSdkContext(conn)
|
||||
const sdk = createServerSdkContext(conn, scope)
|
||||
const sync = createServerSyncContext(sdk)
|
||||
|
||||
const key = ServerConnection.key(conn)
|
||||
const storeKey = projectsKey(key)
|
||||
|
||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||
const [childStore] = sync.child(project.worktree, { bootstrap: false })
|
||||
const projectID = childStore.project
|
||||
@@ -147,7 +126,7 @@ function createServerCtx(
|
||||
return base
|
||||
}
|
||||
|
||||
const projectsList = createMemo(() => (store.projects[storeKey] ?? []).map(enrich))
|
||||
const projectsList = createMemo(() => projects.list().map(enrich))
|
||||
|
||||
const isLocal =
|
||||
(conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url))
|
||||
@@ -158,45 +137,8 @@ function createServerCtx(
|
||||
sync,
|
||||
isLocal,
|
||||
projects: {
|
||||
...projects,
|
||||
list: projectsList,
|
||||
open(directory: string) {
|
||||
const current = store.projects[storeKey] ?? []
|
||||
if (current.find((x) => x.worktree === directory)) return
|
||||
setStore("projects", storeKey, [{ worktree: directory, expanded: true }, ...current])
|
||||
},
|
||||
close(directory: string) {
|
||||
const current = store.projects[storeKey] ?? []
|
||||
setStore(
|
||||
"projects",
|
||||
storeKey,
|
||||
current.filter((x) => x.worktree !== directory),
|
||||
)
|
||||
},
|
||||
expand(directory: string) {
|
||||
const current = store.projects[storeKey] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", storeKey, index, "expanded", true)
|
||||
},
|
||||
collapse(directory: string) {
|
||||
const current = store.projects[storeKey] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", storeKey, index, "expanded", false)
|
||||
},
|
||||
move(directory: string, toIndex: number) {
|
||||
const current = store.projects[storeKey] ?? []
|
||||
const fromIndex = current.findIndex((x) => x.worktree === directory)
|
||||
if (fromIndex === -1 || fromIndex === toIndex) return
|
||||
const result = [...current]
|
||||
const [item] = result.splice(fromIndex, 1)
|
||||
result.splice(toIndex, 0, item)
|
||||
setStore("projects", storeKey, result)
|
||||
},
|
||||
last() {
|
||||
return store.lastProject[storeKey]
|
||||
},
|
||||
touch(directory: string) {
|
||||
setStore("lastProject", storeKey, directory)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -207,42 +149,3 @@ function isLocalHost(url: string) {
|
||||
const host = url.replace(/^https?:\/\//, "").split(":")[0]
|
||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||
}
|
||||
|
||||
function projectsKey(key: ServerConnection.Key) {
|
||||
if (key === "sidecar") return "local"
|
||||
if (isLocalHost(key)) return "local"
|
||||
return key
|
||||
}
|
||||
|
||||
export function resolveServerList(input: {
|
||||
props?: Array<ServerConnection.Any>
|
||||
stored: StoredServer[]
|
||||
}): Array<ServerConnection.Any> {
|
||||
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>(
|
||||
input.props?.map((v) => [ServerConnection.key(v), v]) ?? [],
|
||||
)
|
||||
|
||||
for (const value of input.stored) {
|
||||
const conn: ServerConnection.Http =
|
||||
typeof value === "string"
|
||||
? {
|
||||
type: "http" as const,
|
||||
http: { url: value },
|
||||
}
|
||||
: "http" in value
|
||||
? value
|
||||
: { type: "http", http: value }
|
||||
const key = ServerConnection.key(conn)
|
||||
|
||||
const existing = deduped.get(key)
|
||||
if (existing)
|
||||
deduped.set(key, {
|
||||
...existing,
|
||||
...conn,
|
||||
http: { ...existing.http, ...conn.http },
|
||||
})
|
||||
else deduped.set(key, conn)
|
||||
}
|
||||
|
||||
return [...deduped.values()]
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ if (warm !== "en") void loadDict(warm)
|
||||
|
||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||
name: "Language",
|
||||
gate: false,
|
||||
init: (props: { locale?: Locale }) => {
|
||||
const initial = props.locale ?? readStoredLocale() ?? detectLocale()
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Accessor } from "solid-js"
|
||||
|
||||
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
|
||||
touch(key)
|
||||
seed(key)
|
||||
return key
|
||||
}
|
||||
|
||||
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
|
||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||
return () => {
|
||||
const value = key()
|
||||
ensure(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function pruneSessionKeys(input: {
|
||||
keep?: string
|
||||
max: number
|
||||
used: Map<string, number>
|
||||
view: string[]
|
||||
tabs: string[]
|
||||
}) {
|
||||
if (!input.keep) return []
|
||||
|
||||
const keys = new Set<string>([...input.view, ...input.tabs])
|
||||
if (keys.size <= input.max) return []
|
||||
|
||||
const score = (key: string) => {
|
||||
if (key === input.keep) return Number.MAX_SAFE_INTEGER
|
||||
return input.used.get(key) ?? 0
|
||||
}
|
||||
|
||||
return Array.from(keys)
|
||||
.sort((a, b) => score(b) - score(a))
|
||||
.slice(input.max)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createRoot, createSignal } from "solid-js"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
||||
|
||||
describe("layout session-key helpers", () => {
|
||||
test("couples touch and scroll seed in order", () => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { useLocation } from "@solidjs/router"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { useServer } from "./server"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { usePlatform } from "./platform"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||
@@ -13,6 +14,10 @@ import { same } from "@/utils/same"
|
||||
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
||||
import { createPathHelpers } from "./file/path"
|
||||
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||
import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/utils/server-scope"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
|
||||
|
||||
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
|
||||
|
||||
export type { ProjectAvatarVariant }
|
||||
|
||||
@@ -60,6 +65,7 @@ type SessionView = {
|
||||
}
|
||||
|
||||
type TabHandoff = {
|
||||
scope: ServerScope
|
||||
dir: string
|
||||
id: string
|
||||
at: number
|
||||
@@ -69,42 +75,10 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
|
||||
|
||||
export type ReviewDiffStyle = "unified" | "split"
|
||||
|
||||
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
|
||||
touch(key)
|
||||
seed(key)
|
||||
return key
|
||||
}
|
||||
|
||||
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
|
||||
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||
return () => {
|
||||
const value = key()
|
||||
ensure(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function pruneSessionKeys(input: {
|
||||
keep?: string
|
||||
max: number
|
||||
used: Map<string, number>
|
||||
view: string[]
|
||||
tabs: string[]
|
||||
}) {
|
||||
if (!input.keep) return []
|
||||
|
||||
const keys = new Set<string>([...input.view, ...input.tabs])
|
||||
if (keys.size <= input.max) return []
|
||||
|
||||
const score = (key: string) => {
|
||||
if (key === input.keep) return Number.MAX_SAFE_INTEGER
|
||||
return input.used.get(key) ?? 0
|
||||
}
|
||||
|
||||
return Array.from(keys)
|
||||
.sort((a, b) => score(b) - score(a))
|
||||
.slice(input.max)
|
||||
}
|
||||
export type LayoutRoute =
|
||||
| { type: "home" }
|
||||
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
|
||||
| { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key }
|
||||
|
||||
function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs {
|
||||
const all = current?.all ?? []
|
||||
@@ -115,7 +89,7 @@ function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string):
|
||||
}
|
||||
|
||||
const sessionPath = (key: string) => {
|
||||
const dir = key.split("/")[0]
|
||||
const dir = SessionStateKey.route(key).split("/")[0]
|
||||
if (!dir) return
|
||||
const root = decode64(dir)
|
||||
if (!root) return
|
||||
@@ -146,13 +120,35 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
||||
}
|
||||
}
|
||||
|
||||
const currentRoute = (pathname: string): LayoutRoute => {
|
||||
const parts = pathname.split("/").filter(Boolean)
|
||||
if (parts.length === 0) return { type: "home" }
|
||||
|
||||
const dirBase64 = parts[0]
|
||||
const dir = decode64(dirBase64)
|
||||
if (!dir) return { type: "home" }
|
||||
|
||||
if (parts[1] !== "session") return { type: "home" }
|
||||
|
||||
const id = parts[2]
|
||||
if (id) return { type: "session", dir, dirBase64, sessionId: id }
|
||||
return { type: "dir-new-sesssion", dir, dirBase64 }
|
||||
}
|
||||
|
||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||
name: "Layout",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const globalSdk = useServerSDK()
|
||||
const serverSdk = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const location = useLocation()
|
||||
const route = createMemo(() => {
|
||||
const value = currentRoute(location.pathname)
|
||||
if (value.type === "home") return value
|
||||
return { ...value, server: server.key }
|
||||
})
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
@@ -197,7 +193,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
})()
|
||||
|
||||
const sessionTabs = value.sessionTabs
|
||||
const sessionTabs = migrateLegacySessionStateKeys(value.sessionTabs)
|
||||
const sessionView = migrateLegacySessionStateKeys(value.sessionView)
|
||||
const migratedSessionTabs = (() => {
|
||||
if (!isRecord(sessionTabs)) return sessionTabs
|
||||
|
||||
@@ -226,7 +223,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
migratedSidebar === sidebar &&
|
||||
migratedReview === review &&
|
||||
migratedFileTree === fileTree &&
|
||||
migratedSessionTabs === sessionTabs
|
||||
migratedSessionTabs === value.sessionTabs &&
|
||||
sessionView === value.sessionView
|
||||
) {
|
||||
return value
|
||||
}
|
||||
@@ -237,10 +235,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
review: migratedReview,
|
||||
fileTree: migratedFileTree,
|
||||
sessionTabs: migratedSessionTabs,
|
||||
sessionView,
|
||||
}
|
||||
}
|
||||
|
||||
const target = Persist.global("layout", ["layout.v6"])
|
||||
const target = Persist.serverGlobal(serverSdk.scope, "layout", ["layout.v6"])
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{ ...target, migrate },
|
||||
createStore({
|
||||
@@ -293,15 +292,19 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
const dropSessionState = (keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const parts = key.split("/")
|
||||
const scope = SessionStateKey.scope(key)
|
||||
const parts = SessionStateKey.route(key).split("/")
|
||||
const dir = parts[0]
|
||||
const session = parts[1]
|
||||
if (!dir) continue
|
||||
|
||||
for (const entry of SESSION_STATE_KEYS) {
|
||||
const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key)
|
||||
const target = session
|
||||
? Persist.serverSession(scope, dir, session, entry.key)
|
||||
: Persist.serverWorkspace(scope, dir, entry.key)
|
||||
void removePersisted(target, platform)
|
||||
|
||||
if (scope !== ServerScope.local) continue
|
||||
const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}`
|
||||
void removePersisted({ key: legacyKey }, platform)
|
||||
}
|
||||
@@ -526,7 +529,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
continue
|
||||
}
|
||||
|
||||
void globalSdk.client.project
|
||||
void serverSdk.client.project
|
||||
.update({ projectID: project.id, directory: worktree, icon: { color } })
|
||||
.catch(() => {
|
||||
if (colorRequested.get(worktree) === color) colorRequested.delete(worktree)
|
||||
@@ -557,11 +560,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
})
|
||||
|
||||
return {
|
||||
route,
|
||||
ready,
|
||||
handoff: {
|
||||
tabs: createMemo(() => store.handoff?.tabs),
|
||||
setTabs(dir: string, id: string) {
|
||||
setStore("handoff", "tabs", { dir, id, at: Date.now() })
|
||||
setStore("handoff", "tabs", { scope: server.scope(), dir, id, at: Date.now() })
|
||||
},
|
||||
clearTabs() {
|
||||
if (!store.handoff?.tabs) return
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||
|
||||
export type ModelKey = { providerID: string; modelID: string; variant?: string }
|
||||
|
||||
@@ -25,7 +27,7 @@ type Saved = {
|
||||
const WORKSPACE_KEY = "__workspace__"
|
||||
const handoff = new Map<string, State>()
|
||||
|
||||
const handoffKey = (dir: string, id: string) => `${dir}\n${id}`
|
||||
const handoffKey = (scope: ServerScope, dir: string, id: string) => ScopedKey.from(scope, dir, id)
|
||||
|
||||
const migrate = (value: unknown) => {
|
||||
if (!value || typeof value !== "object") return { session: {} }
|
||||
@@ -57,6 +59,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const params = useParams()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const providers = useProviders()
|
||||
const models = useModels()
|
||||
|
||||
@@ -66,7 +69,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const [saved, setSaved] = persisted(
|
||||
{
|
||||
...Persist.workspace(sdk.directory, "model-selection", ["model-selection.v1"]),
|
||||
...Persist.serverWorkspace(serverSDK.scope, sdk.directory, "model-selection", ["model-selection.v1"]),
|
||||
migrate,
|
||||
},
|
||||
createStore<Saved>({
|
||||
@@ -121,14 +124,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const scope = createMemo<State | undefined>(() => {
|
||||
const session = id()
|
||||
if (!session) return store.draft
|
||||
return saved.session[session] ?? handoff.get(handoffKey(sdk.directory, session))
|
||||
return saved.session[session] ?? handoff.get(handoffKey(serverSDK.scope, sdk.directory, session))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const session = id()
|
||||
if (!session) return
|
||||
|
||||
const key = handoffKey(sdk.directory, session)
|
||||
const key = handoffKey(serverSDK.scope, sdk.directory, session)
|
||||
const next = handoff.get(key)
|
||||
if (!next) return
|
||||
if (saved.session[session] !== undefined) {
|
||||
@@ -377,7 +380,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
return
|
||||
}
|
||||
|
||||
handoff.set(handoffKey(dir, session), next)
|
||||
handoff.set(handoffKey(serverSDK.scope, dir, session), next)
|
||||
setStore("draft", undefined)
|
||||
},
|
||||
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
||||
@@ -385,7 +388,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
if (!session) return
|
||||
if (msg.sessionID !== session) return
|
||||
if (saved.session[session] !== undefined) return
|
||||
if (handoff.has(handoffKey(sdk.directory, session))) return
|
||||
if (handoff.has(handoffKey(serverSDK.scope, sdk.directory, session))) return
|
||||
|
||||
setSaved("session", session, {
|
||||
agent: msg.agent,
|
||||
|
||||
@@ -24,6 +24,7 @@ function modelKey(model: ModelKey) {
|
||||
|
||||
export const { use: useModels, provider: ModelsProvider } = createSimpleContext({
|
||||
name: "Models",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const providers = useProviders()
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ function buildNotificationIndex(list: Notification[]) {
|
||||
|
||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||
name: "Notification",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const serverSDK = useServerSDK()
|
||||
@@ -124,7 +125,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const currentSession = createMemo(() => params.id)
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("notification", ["notification.v1"]),
|
||||
Persist.serverGlobal(serverSDK.scope, "notification", ["notification.v1"]),
|
||||
createStore({
|
||||
list: [] as Notification[],
|
||||
}),
|
||||
|
||||
@@ -46,6 +46,7 @@ function hasPermissionPromptRules(permission: unknown) {
|
||||
|
||||
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
|
||||
name: "Permission",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const serverSDK = useServerSDK()
|
||||
@@ -60,7 +61,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{
|
||||
...Persist.global("permission", ["permission.v3"]),
|
||||
...Persist.serverGlobal(serverSDK.scope, "permission", ["permission.v3"]),
|
||||
migrate(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
|
||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||
import type { FileSelection } from "@/context/file"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
interface PartBase {
|
||||
content: string
|
||||
@@ -161,11 +163,11 @@ type PromptCacheEntry = {
|
||||
dispose: VoidFunction
|
||||
}
|
||||
|
||||
function createPromptSession(dir: string, id: string | undefined) {
|
||||
function createPromptSession(scope: ServerScope, dir: string, id: string | undefined) {
|
||||
const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2`
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.scoped(dir, id, "prompt", [legacy]),
|
||||
Persist.serverScoped(scope, dir, id, "prompt", [legacy]),
|
||||
createStore<{
|
||||
prompt: Prompt
|
||||
cursor?: number
|
||||
@@ -229,6 +231,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
gate: false,
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const serverSDK = useServerSDK()
|
||||
const cache = new Map<string, PromptCacheEntry>()
|
||||
|
||||
const disposeAll = () => {
|
||||
@@ -262,7 +265,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
|
||||
const entry = createRoot(
|
||||
(dispose) => ({
|
||||
value: createPromptSession(dir, id),
|
||||
value: createPromptSession(serverSDK.scope, dir, id),
|
||||
dispose,
|
||||
}),
|
||||
owner,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { resumeStreamAfterPageShow } from "./server-sdk"
|
||||
|
||||
describe("resumeStreamAfterPageShow", () => {
|
||||
test("restarts a stream only after a back-forward cache restore", () => {
|
||||
let starts = 0
|
||||
const start = () => starts++
|
||||
|
||||
resumeStreamAfterPageShow({ persisted: false } as PageTransitionEvent, start)
|
||||
resumeStreamAfterPageShow({ persisted: true } as PageTransitionEvent, start)
|
||||
|
||||
expect(starts).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -9,11 +9,19 @@ import { usePlatform } from "./platform"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { createRefCountMap } from "@/utils/refcount"
|
||||
import { useGlobal } from "./global"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
const isAbortError = (error: unknown) =>
|
||||
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
||||
|
||||
export function createServerSdkContext(server: ServerConnection.Any) {
|
||||
const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true
|
||||
|
||||
export function resumeStreamAfterPageShow(event: PageTransitionEvent, start: () => unknown) {
|
||||
if (!event.persisted) return
|
||||
start()
|
||||
}
|
||||
|
||||
export function createServerSdkContext(server: ServerConnection.Any, scope: ServerScope) {
|
||||
const platform = usePlatform()
|
||||
const abort = new AbortController()
|
||||
|
||||
@@ -96,11 +104,10 @@ export function createServerSdkContext(server: ServerConnection.Any) {
|
||||
|
||||
let streamErrorLogged = false
|
||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
const aborted = isAbortError
|
||||
|
||||
let attempt: AbortController | undefined
|
||||
let run: Promise<void> | undefined
|
||||
let started = false
|
||||
let generation = 0
|
||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||
let lastEventAt = Date.now()
|
||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||
@@ -120,9 +127,12 @@ export function createServerSdkContext(server: ServerConnection.Any) {
|
||||
const start = () => {
|
||||
if (started) return run
|
||||
started = true
|
||||
run = (async () => {
|
||||
const active = ++generation
|
||||
const previous = run
|
||||
const current = (async () => {
|
||||
if (previous) await previous
|
||||
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
||||
while (!abort.signal.aborted && started) {
|
||||
while (!abort.signal.aborted && started && generation === active) {
|
||||
attempt = new AbortController()
|
||||
lastEventAt = Date.now()
|
||||
const onAbort = () => {
|
||||
@@ -133,7 +143,7 @@ export function createServerSdkContext(server: ServerConnection.Any) {
|
||||
const events = await eventSdk.global.event({
|
||||
signal: attempt.signal,
|
||||
onSseError: (error) => {
|
||||
if (aborted(error)) return
|
||||
if (isStreamClosed(error, attempt?.signal)) return
|
||||
if (streamErrorLogged) return
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream error", {
|
||||
@@ -176,7 +186,7 @@ export function createServerSdkContext(server: ServerConnection.Any) {
|
||||
await wait(0)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!aborted(error) && !streamErrorLogged) {
|
||||
if (!isStreamClosed(error, attempt?.signal) && !streamErrorLogged) {
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream failed", {
|
||||
url: server.http.url,
|
||||
@@ -190,23 +200,28 @@ export function createServerSdkContext(server: ServerConnection.Any) {
|
||||
clearHeartbeat()
|
||||
}
|
||||
|
||||
if (abort.signal.aborted || !started) return
|
||||
if (abort.signal.aborted || !started || generation !== active) return
|
||||
await wait(RECONNECT_DELAY_MS)
|
||||
}
|
||||
})().finally(() => {
|
||||
if (run !== current) return
|
||||
run = undefined
|
||||
flush()
|
||||
})
|
||||
run = current
|
||||
return run
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
started = false
|
||||
generation++
|
||||
attempt?.abort()
|
||||
clearHeartbeat()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
makeEventListener(window, "pagehide", stop)
|
||||
makeEventListener(window, "pageshow", (event) => resumeStreamAfterPageShow(event, start))
|
||||
makeEventListener(document, "visibilitychange", () => {
|
||||
if (document.visibilityState !== "visible") return
|
||||
if (!started) return
|
||||
@@ -228,6 +243,7 @@ export function createServerSdkContext(server: ServerConnection.Any) {
|
||||
})
|
||||
|
||||
return {
|
||||
scope,
|
||||
url: server.http.url,
|
||||
client: sdk,
|
||||
event: {
|
||||
@@ -282,6 +298,7 @@ function createDirSdkContext(directory: string, serverSDK: ServerSDK) {
|
||||
onCleanup(unsub)
|
||||
|
||||
return {
|
||||
scope: serverSDK.scope,
|
||||
directory,
|
||||
client,
|
||||
event: emitter,
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
getOwner,
|
||||
onCleanup,
|
||||
onMount,
|
||||
type ParentProps,
|
||||
untrack,
|
||||
useContext,
|
||||
} from "solid-js"
|
||||
import { batch, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { InitError } from "../pages/error"
|
||||
@@ -44,6 +34,8 @@ import { createRefCountMap } from "@/utils/refcount"
|
||||
import { useGlobal } from "./global"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { retry } from "@opencode-ai/core/util/retry"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
import { persisted } from "@/utils/persist"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -59,34 +51,39 @@ type GlobalStore = {
|
||||
reload: undefined | "pending" | "complete"
|
||||
}
|
||||
|
||||
export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
|
||||
export const loadMcpQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: [directory, "mcp"] as const,
|
||||
queryKey: [scope, directory, "mcp"] as const,
|
||||
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
|
||||
})
|
||||
|
||||
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
|
||||
export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: [directory, "lsp"] as const,
|
||||
queryKey: [scope, directory, "lsp"] as const,
|
||||
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
|
||||
})
|
||||
|
||||
function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
|
||||
function makeQueryOptionsApi(
|
||||
scope: ServerScope,
|
||||
serverSDK: () => OpencodeClient,
|
||||
sdkFor: (dir: PathKey) => OpencodeClient,
|
||||
) {
|
||||
return {
|
||||
globalConfig: () => loadGlobalConfigQuery(serverSDK()),
|
||||
projects: () => loadProjectsQuery(serverSDK()),
|
||||
globalConfig: () => loadGlobalConfigQuery(scope, serverSDK()),
|
||||
projects: () => loadProjectsQuery(scope, serverSDK()),
|
||||
providers: (directory: PathKey | null) =>
|
||||
loadProvidersQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
|
||||
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
|
||||
agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
|
||||
mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
|
||||
lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
|
||||
sessions: (directory: PathKey) => ({ queryKey: [directory, "loadSessions"] as const }),
|
||||
loadProvidersQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)),
|
||||
path: (directory: PathKey | null) =>
|
||||
loadPathQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)),
|
||||
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, sdkFor(directory)),
|
||||
mcp: (directory: PathKey) => loadMcpQuery(scope, directory, sdkFor(directory)),
|
||||
lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)),
|
||||
sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
|
||||
}
|
||||
}
|
||||
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
||||
|
||||
export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
|
||||
const serverSDK: ServerSDK = _serverSDK ?? useServerSDK()
|
||||
const language = useLanguage()
|
||||
const owner = getOwner()
|
||||
@@ -109,7 +106,7 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
return sdk
|
||||
}
|
||||
|
||||
const queryOptionsApi = makeQueryOptionsApi(() => serverSDK.client, sdkFor)
|
||||
const queryOptionsApi = makeQueryOptionsApi(serverSDK.scope, () => serverSDK.client, sdkFor)
|
||||
|
||||
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
||||
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
|
||||
@@ -166,10 +163,11 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
}) as typeof setGlobalStore
|
||||
|
||||
const bootstrap = useQuery(() => ({
|
||||
queryKey: ["bootstrap"],
|
||||
queryKey: [serverSDK.scope, "bootstrap"],
|
||||
queryFn: async () => {
|
||||
await bootstrapGlobal({
|
||||
serverSDK: serverSDK.client,
|
||||
scope: serverSDK.scope,
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
@@ -208,12 +206,14 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
const queue = createRefreshQueue({
|
||||
paused,
|
||||
key: directoryKey,
|
||||
bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
|
||||
bootstrap: () => queryClient.fetchQuery({ queryKey: [serverSDK.scope, "bootstrap"] }),
|
||||
bootstrapInstance,
|
||||
})
|
||||
|
||||
const children = createChildStoreManager({
|
||||
owner,
|
||||
scope: serverSDK.scope,
|
||||
persist: persisted,
|
||||
isBooting: (directory) => booting.has(directory),
|
||||
isLoadingSessions: (directory) => sessionLoads.has(directory),
|
||||
onBootstrap: (directory) => {
|
||||
@@ -237,8 +237,8 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
queue.clear(key)
|
||||
sessionMeta.delete(key)
|
||||
sdkCache.delete(key)
|
||||
clearProviderRev(key)
|
||||
clearSessionPrefetchDirectory(key)
|
||||
clearProviderRev(serverSDK.scope, key)
|
||||
clearSessionPrefetchDirectory(serverSDK.scope, key)
|
||||
},
|
||||
translate: language.t,
|
||||
queryOptions: queryOptionsApi,
|
||||
@@ -338,6 +338,7 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
const sdk = sdkFor(directory)
|
||||
await bootstrapDirectory({
|
||||
directory,
|
||||
scope: serverSDK.scope,
|
||||
mcp: children.mcp(key),
|
||||
global: {
|
||||
config: globalStore.config,
|
||||
@@ -449,8 +450,10 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
bootstrap.refetch()
|
||||
// Invalidate all provider queries so newly configured custom providers
|
||||
// appear immediately in the available provider list across all directories.
|
||||
queryClient.invalidateQueries({ queryKey: [null, "providers"] })
|
||||
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[1] === "providers" })
|
||||
queryClient.invalidateQueries({ queryKey: [serverSDK.scope, null, "providers"] })
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers",
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -476,8 +479,20 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
}
|
||||
}
|
||||
|
||||
export function createServerSyncContext(_serverSDK?: ServerSDK) {
|
||||
const inner = createServerSyncContextInner(_serverSDK)
|
||||
return Object.assign(inner, {
|
||||
createDirSyncContext: createRefCountMap(
|
||||
(dir) => createDirSyncContext(dir, inner, _serverSDK),
|
||||
(dir) => inner.disableMcp(dir),
|
||||
directoryKey,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
|
||||
name: "ServerSync",
|
||||
gate: false,
|
||||
init: (props: { server?: ServerConnection.Any }) => {
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
@@ -487,13 +502,7 @@ export const { use: useServerSync, provider: ServerSyncProvider } = createSimple
|
||||
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
||||
const ctx = global.createServerCtx(conn)
|
||||
|
||||
return Object.assign(ctx.sync, {
|
||||
createDirSyncContext: createRefCountMap(
|
||||
(dir) => createDirSyncContext(dir, ctx.sync),
|
||||
(dir) => ctx.sync.disableMcp(dir),
|
||||
directoryKey,
|
||||
),
|
||||
})
|
||||
return ctx.sync
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { resolveServerList, ServerConnection } from "./server"
|
||||
import { createRoot, createSignal } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createServerProjects, migrateCanonicalLocalServerState, resolveServerList, ServerConnection } from "./server"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
describe("resolveServerList", () => {
|
||||
test("lets startup auth_token credentials override a persisted same-url server", () => {
|
||||
@@ -51,3 +54,70 @@ describe("resolveServerList", () => {
|
||||
expect(list[0]?.type === "http" ? list[0].authToken : true).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createServerProjects", () => {
|
||||
test("keeps active and explicit server buckets in one reactive store", () => {
|
||||
createRoot((dispose) => {
|
||||
const [scope] = createSignal(ServerScope.local)
|
||||
const [store, setStore] = createStore({ projects: {}, lastProject: {} })
|
||||
const active = createServerProjects({ scope, store, setStore })
|
||||
const remote = createServerProjects({ scope: () => "https://debian.example" as ServerScope, store, setStore })
|
||||
|
||||
remote.open("/repo")
|
||||
expect(remote.list()).toEqual([{ worktree: "/repo", expanded: true }])
|
||||
expect(active.list()).toEqual([])
|
||||
|
||||
const adopted = createServerProjects({ scope: () => "https://debian.example" as ServerScope, store, setStore })
|
||||
expect(adopted.list()).toEqual([{ worktree: "/repo", expanded: true }])
|
||||
|
||||
adopted.close("/repo")
|
||||
expect(remote.list()).toEqual([])
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateCanonicalLocalServerState", () => {
|
||||
test("moves an existing canonical web bucket into local scope", () => {
|
||||
expect(
|
||||
migrateCanonicalLocalServerState(
|
||||
{
|
||||
list: [],
|
||||
projects: { "https://opencode.example.com": [{ worktree: "/remote", expanded: true }] },
|
||||
lastProject: { "https://opencode.example.com": "/remote" },
|
||||
},
|
||||
ServerConnection.Key.make("https://opencode.example.com"),
|
||||
),
|
||||
).toEqual({
|
||||
list: [],
|
||||
projects: { local: [{ worktree: "/remote", expanded: true }] },
|
||||
lastProject: { local: "/remote" },
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves existing local state while merging a canonical web bucket", () => {
|
||||
expect(
|
||||
migrateCanonicalLocalServerState(
|
||||
{
|
||||
projects: {
|
||||
local: [{ worktree: "/local", expanded: false }],
|
||||
"https://opencode.example.com": [
|
||||
{ worktree: "/local", expanded: true },
|
||||
{ worktree: "/remote", expanded: true },
|
||||
],
|
||||
},
|
||||
lastProject: { local: "/local", "https://opencode.example.com": "/remote" },
|
||||
},
|
||||
ServerConnection.Key.make("https://opencode.example.com"),
|
||||
),
|
||||
).toEqual({
|
||||
projects: {
|
||||
local: [
|
||||
{ worktree: "/local", expanded: false },
|
||||
{ worktree: "/remote", expanded: true },
|
||||
],
|
||||
},
|
||||
lastProject: { local: "/local" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { type Accessor, batch, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
type StoredProject = { worktree: string; expanded: boolean }
|
||||
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
|
||||
type ServerProjectState = { projects: Record<string, StoredProject[]>; lastProject: Record<string, string> }
|
||||
const HEALTH_POLL_INTERVAL_MS = 10_000
|
||||
|
||||
export function normalizeServerUrl(input: string) {
|
||||
@@ -20,18 +22,95 @@ export function serverName(conn?: ServerConnection.Any, ignoreDisplayName = fals
|
||||
return conn.http.url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
function projectsKey(key: ServerConnection.Key) {
|
||||
if (!key) return ""
|
||||
if (key === "sidecar") return "local"
|
||||
if (isLocalHost(key)) return "local"
|
||||
return key
|
||||
}
|
||||
|
||||
function isLocalHost(url: string) {
|
||||
const host = url.replace(/^https?:\/\//, "").split(":")[0]
|
||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export function migrateCanonicalLocalServerState(value: unknown, canonicalLocalServer?: ServerConnection.Key) {
|
||||
if (!canonicalLocalServer || canonicalLocalServer === "local") return value
|
||||
if (!isRecord(value)) return value
|
||||
const projects = isRecord(value.projects) ? value.projects : undefined
|
||||
const lastProject = isRecord(value.lastProject) ? value.lastProject : undefined
|
||||
const previousProjects = projects?.[canonicalLocalServer]
|
||||
const previousLastProject = lastProject?.[canonicalLocalServer]
|
||||
if (!Array.isArray(previousProjects) && typeof previousLastProject !== "string") return value
|
||||
|
||||
const next = { ...value }
|
||||
if (projects && Array.isArray(previousProjects)) {
|
||||
const local = Array.isArray(projects.local) ? projects.local : []
|
||||
const worktrees = new Set(
|
||||
local.flatMap((project) => (isRecord(project) && typeof project.worktree === "string" ? [project.worktree] : [])),
|
||||
)
|
||||
const migrated = previousProjects.filter((project) => {
|
||||
if (!isRecord(project) || typeof project.worktree !== "string") return true
|
||||
if (worktrees.has(project.worktree)) return false
|
||||
worktrees.add(project.worktree)
|
||||
return true
|
||||
})
|
||||
const nextProjects: Record<string, unknown> = { ...projects, local: [...local, ...migrated] }
|
||||
delete nextProjects[canonicalLocalServer]
|
||||
next.projects = nextProjects
|
||||
}
|
||||
if (lastProject && typeof previousLastProject === "string") {
|
||||
const nextLastProject = { ...lastProject }
|
||||
if (typeof nextLastProject.local !== "string") nextLastProject.local = previousLastProject
|
||||
delete nextLastProject[canonicalLocalServer]
|
||||
next.lastProject = nextLastProject
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
export function createServerProjects<T extends ServerProjectState>(input: {
|
||||
scope: Accessor<ServerScope>
|
||||
store: Store<T>
|
||||
setStore: SetStoreFunction<T>
|
||||
}) {
|
||||
const setStore = input.setStore as unknown as SetStoreFunction<ServerProjectState>
|
||||
const current = () => input.store.projects[input.scope()] ?? []
|
||||
return {
|
||||
list: current,
|
||||
open(directory: string) {
|
||||
const scope = input.scope()
|
||||
if (current().some((project) => project.worktree === directory)) return
|
||||
setStore("projects", scope, [{ worktree: directory, expanded: true }, ...current()])
|
||||
},
|
||||
close(directory: string) {
|
||||
setStore(
|
||||
"projects",
|
||||
input.scope(),
|
||||
current().filter((project) => project.worktree !== directory),
|
||||
)
|
||||
},
|
||||
expand(directory: string) {
|
||||
const index = current().findIndex((project) => project.worktree === directory)
|
||||
if (index !== -1) setStore("projects", input.scope(), index, "expanded", true)
|
||||
},
|
||||
collapse(directory: string) {
|
||||
const index = current().findIndex((project) => project.worktree === directory)
|
||||
if (index !== -1) setStore("projects", input.scope(), index, "expanded", false)
|
||||
},
|
||||
move(directory: string, toIndex: number) {
|
||||
const fromIndex = current().findIndex((project) => project.worktree === directory)
|
||||
if (fromIndex === -1 || fromIndex === toIndex) return
|
||||
const next = [...current()]
|
||||
const [item] = next.splice(fromIndex, 1)
|
||||
next.splice(toIndex, 0, item)
|
||||
setStore("projects", input.scope(), next)
|
||||
},
|
||||
last() {
|
||||
return input.store.lastProject[input.scope()]
|
||||
},
|
||||
touch(directory: string) {
|
||||
setStore("lastProject", input.scope(), directory)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveServerList(input: {
|
||||
props?: Array<ServerConnection.Any>
|
||||
stored: StoredServer[]
|
||||
@@ -127,9 +206,17 @@ export namespace ServerConnection {
|
||||
|
||||
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
||||
name: "Server",
|
||||
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
|
||||
gate: true,
|
||||
init: (props: {
|
||||
defaultServer: ServerConnection.Key
|
||||
canonicalLocalServer?: ServerConnection.Key
|
||||
servers?: Array<ServerConnection.Any>
|
||||
}) => {
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("server", ["server.v3"]),
|
||||
{
|
||||
...Persist.global("server", ["server.v3"]),
|
||||
migrate: (value) => migrateCanonicalLocalServerState(value, props.canonicalLocalServer),
|
||||
},
|
||||
createStore({
|
||||
list: [] as StoredServer[],
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
@@ -180,8 +267,16 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
|
||||
const isReady = createMemo(() => ready() && !!state.active)
|
||||
|
||||
const origin = createMemo(() => projectsKey(state.active))
|
||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||
const scope = (key = state.active) => ServerScope.fromServerKey(key, props.canonicalLocalServer)
|
||||
const projects = createServerProjects({ scope, store, setStore })
|
||||
const projectStores = new Map<ServerConnection.Key, ReturnType<typeof createServerProjects>>()
|
||||
const projectsForServer = (key: ServerConnection.Key) => {
|
||||
const existing = projectStores.get(key)
|
||||
if (existing) return existing
|
||||
const next = createServerProjects({ scope: () => scope(key), store, setStore })
|
||||
projectStores.set(key, next)
|
||||
return next
|
||||
}
|
||||
const current: Accessor<ServerConnection.Any | undefined> = createMemo(
|
||||
() => allServers().find((s) => ServerConnection.key(s) === state.active) ?? allServers()[0],
|
||||
)
|
||||
@@ -208,60 +303,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
setActive,
|
||||
add,
|
||||
remove,
|
||||
scope,
|
||||
projects: {
|
||||
list: projectsList,
|
||||
open(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
if (current.find((x) => x.worktree === directory)) return
|
||||
setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
|
||||
},
|
||||
close(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
setStore(
|
||||
"projects",
|
||||
key,
|
||||
current.filter((x) => x.worktree !== directory),
|
||||
)
|
||||
},
|
||||
expand(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", key, index, "expanded", true)
|
||||
},
|
||||
collapse(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", key, index, "expanded", false)
|
||||
},
|
||||
move(directory: string, toIndex: number) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
const fromIndex = current.findIndex((x) => x.worktree === directory)
|
||||
if (fromIndex === -1 || fromIndex === toIndex) return
|
||||
const result = [...current]
|
||||
const [item] = result.splice(fromIndex, 1)
|
||||
result.splice(toIndex, 0, item)
|
||||
setStore("projects", key, result)
|
||||
},
|
||||
last() {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
return store.lastProject[key]
|
||||
},
|
||||
touch(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
setStore("lastProject", key, directory)
|
||||
},
|
||||
...projects,
|
||||
forServer: projectsForServer,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -156,13 +156,10 @@ function withFallback<T>(read: () => T | undefined, fallback: T) {
|
||||
|
||||
export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
|
||||
name: "Settings",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
|
||||
|
||||
createEffect(() => {
|
||||
console.log("settings", { ready: ready() })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
const root = document.documentElement
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { createEffect, startTransition } from "solid-js"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
|
||||
|
||||
export type SessionTab = {
|
||||
type: "session"
|
||||
server: ServerConnection.Key
|
||||
dirBase64: string
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
export type Tab = SessionTab
|
||||
|
||||
export const tabHref = (tab: Tab) => `/${tab.dirBase64}/session/${tab.sessionId}`
|
||||
export const tabKey = (tab: Tab) => `${tab.server}\n${tabHref(tab)}`
|
||||
|
||||
export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
name: "Tabs",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const server = useServer()
|
||||
const fallback = server.key
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{
|
||||
...Persist.global("tabs"),
|
||||
migrate: (value: unknown) => {
|
||||
if (!Array.isArray(value)) return value
|
||||
return value.map((tab) => {
|
||||
if (!tab || typeof tab !== "object" || "server" in tab) return tab
|
||||
return { ...tab, server: fallback }
|
||||
})
|
||||
},
|
||||
},
|
||||
createStore<Tab[]>([]),
|
||||
)
|
||||
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const closing = new Set<string>()
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
const servers = new Set(server.list.map(ServerConnection.key))
|
||||
if (store.every((tab) => servers.has(tab.server))) return
|
||||
setStore((tabs) => tabs.filter((tab) => servers.has(tab.server)))
|
||||
})
|
||||
|
||||
const navigateTab = (tab: Tab) => {
|
||||
const href = tabHref(tab)
|
||||
if (tab.server === server.key) {
|
||||
navigate(href)
|
||||
return
|
||||
}
|
||||
void startTransition(() => {
|
||||
server.setActive(tab.server)
|
||||
navigate(href)
|
||||
})
|
||||
}
|
||||
|
||||
const actions = {
|
||||
addSessionTab: (tab: Omit<SessionTab, "type">) => {
|
||||
const next = { type: "session" as const, ...tab }
|
||||
if (closing.has(tabKey(next))) return
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
if (tabs.some((item) => tabKey(item) === tabKey(next))) return
|
||||
tabs.push(next)
|
||||
}),
|
||||
)
|
||||
},
|
||||
removeTab: (index: number) => {
|
||||
const tab = store[index]
|
||||
if (!tab) return
|
||||
const key = tabKey(tab)
|
||||
const nextTab = store[index + 1] ?? store[index - 1]
|
||||
closing.add(key)
|
||||
void startTransition(() => {
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
tabs.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
if (nextTab) navigateTab(nextTab)
|
||||
else navigate("/")
|
||||
}).finally(() => closing.delete(key))
|
||||
},
|
||||
removeServer(key: ServerConnection.Key) {
|
||||
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
||||
if (server.key === key) navigate("/")
|
||||
},
|
||||
removeSessions: (input: SessionTabsRemovedDetail) => {
|
||||
void startTransition(() => {
|
||||
setStore(
|
||||
produce((tabs) => {
|
||||
const sessionIDs = new Set(input.sessionIDs)
|
||||
const currentHref =
|
||||
params.dir && params.id
|
||||
? tabHref({ type: "session", server: server.key, dirBase64: params.dir, sessionId: params.id })
|
||||
: undefined
|
||||
const currentIndex = currentHref
|
||||
? tabs.findIndex(
|
||||
(tab) => tab.type === "session" && tab.server === server.key && tabHref(tab) === currentHref,
|
||||
)
|
||||
: -1
|
||||
const currentTab = tabs[currentIndex]
|
||||
const removedCurrent =
|
||||
currentTab?.type === "session" &&
|
||||
currentTab.server === server.key &&
|
||||
atob(currentTab.dirBase64) === input.directory &&
|
||||
sessionIDs.has(currentTab.sessionId)
|
||||
|
||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
||||
const tab = tabs[i]
|
||||
if (!tab || tab.type !== "session") continue
|
||||
if (tab.server !== server.key) continue
|
||||
if (atob(tab.dirBase64) !== input.directory) continue
|
||||
if (!sessionIDs.has(tab.sessionId)) continue
|
||||
tabs.splice(i, 1)
|
||||
}
|
||||
|
||||
if (!removedCurrent) return
|
||||
const nextTab =
|
||||
tabs.slice(currentIndex).find((tab) => tab.type === "session") ??
|
||||
tabs.slice(0, currentIndex).findLast((tab) => tab.type === "session")
|
||||
if (nextTab) navigateTab(nextTab)
|
||||
else navigate("/")
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return { ...actions, store, ready }
|
||||
},
|
||||
})
|
||||
@@ -1,9 +1,7 @@
|
||||
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
|
||||
type ServerKey = Parameters<typeof import("./terminal").getTerminalServerScope>[1]
|
||||
|
||||
let getWorkspaceTerminalCacheKey: (dir: string, scope?: string) => string
|
||||
let getTerminalServerScope: typeof import("./terminal").getTerminalServerScope
|
||||
let getWorkspaceTerminalCacheKey: typeof import("./terminal").getWorkspaceTerminalCacheKey
|
||||
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
|
||||
let migrateTerminalState: (value: unknown) => unknown
|
||||
|
||||
@@ -20,53 +18,19 @@ beforeAll(async () => {
|
||||
}))
|
||||
const mod = await import("./terminal")
|
||||
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
|
||||
getTerminalServerScope = mod.getTerminalServerScope
|
||||
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
|
||||
migrateTerminalState = mod.migrateTerminalState
|
||||
})
|
||||
|
||||
describe("getWorkspaceTerminalCacheKey", () => {
|
||||
test("uses workspace-only directory cache key", () => {
|
||||
expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
|
||||
expect(String(getWorkspaceTerminalCacheKey("/repo"))).toBe("local\u0000/repo\u0000__workspace__")
|
||||
})
|
||||
|
||||
test("can include a server scope", () => {
|
||||
expect(getWorkspaceTerminalCacheKey("/repo", "wsl:Debian")).toBe("wsl:Debian:/repo:__workspace__")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTerminalServerScope", () => {
|
||||
test("preserves local server keys", () => {
|
||||
expect(
|
||||
getTerminalServerScope(
|
||||
{ type: "sidecar", variant: "base", http: { url: "http://127.0.0.1:4096" } },
|
||||
"sidecar" as ServerKey,
|
||||
),
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
getTerminalServerScope(
|
||||
{ type: "http", http: { url: "http://localhost:4096" } },
|
||||
"http://localhost:4096" as ServerKey,
|
||||
),
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
getTerminalServerScope({ type: "http", http: { url: "http://[::1]:4096" } }, "http://[::1]:4096" as ServerKey),
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
test("scopes non-local server keys", () => {
|
||||
expect(
|
||||
getTerminalServerScope(
|
||||
{ type: "sidecar", variant: "wsl", distro: "Debian", http: { url: "http://127.0.0.1:4096" } },
|
||||
"wsl:Debian" as ServerKey,
|
||||
),
|
||||
).toBe("wsl:Debian" as ServerKey)
|
||||
expect(
|
||||
getTerminalServerScope(
|
||||
{ type: "http", http: { url: "https://example.com" } },
|
||||
"https://example.com" as ServerKey,
|
||||
),
|
||||
).toBe("https://example.com" as ServerKey)
|
||||
expect(String(getWorkspaceTerminalCacheKey("/repo", "ssh:debian" as ServerScope))).toBe(
|
||||
"ssh:debian\u0000/repo\u0000__workspace__",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Platform } from "./platform"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { useServer } from "./server"
|
||||
import { defaultTitle, titleNumber } from "./terminal-title"
|
||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||
import { ScopedKey, ServerScope, type ServerScope as ServerScopeValue } from "@/utils/server-scope"
|
||||
|
||||
export type LocalPTY = {
|
||||
id: string
|
||||
@@ -83,29 +84,8 @@ export function migrateTerminalState(value: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceTerminalCacheKey(dir: string, scope?: string) {
|
||||
if (scope) return `${scope}:${dir}:${WORKSPACE_KEY}`
|
||||
return `${dir}:${WORKSPACE_KEY}`
|
||||
}
|
||||
|
||||
export function getTerminalServerScope(conn: ServerConnection.Any | undefined, key: ServerConnection.Key) {
|
||||
if (!conn) return
|
||||
if (conn.type === "sidecar" && conn.variant === "base") return
|
||||
if (conn.type === "http") {
|
||||
try {
|
||||
const url = new URL(conn.http.url)
|
||||
if (
|
||||
url.hostname === "localhost" ||
|
||||
url.hostname === "127.0.0.1" ||
|
||||
url.hostname === "::1" ||
|
||||
url.hostname === "[::1]"
|
||||
)
|
||||
return
|
||||
} catch {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return key
|
||||
export function getWorkspaceTerminalCacheKey(dir: string, scope: ServerScopeValue = ServerScope.local) {
|
||||
return ScopedKey.from(scope, dir, WORKSPACE_KEY)
|
||||
}
|
||||
|
||||
export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
|
||||
@@ -132,16 +112,25 @@ const trimTerminal = (pty: LocalPTY) => {
|
||||
}
|
||||
}
|
||||
|
||||
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform, scope?: string) {
|
||||
function terminalPersistTarget(scope: ServerScopeValue, dir: string, legacy?: string[]) {
|
||||
return Persist.serverWorkspace(scope, dir, "terminal", legacy)
|
||||
}
|
||||
|
||||
export function clearWorkspaceTerminals(
|
||||
dir: string,
|
||||
sessionIDs?: string[],
|
||||
platform?: Platform,
|
||||
scope: ServerScopeValue = ServerScope.local,
|
||||
) {
|
||||
const key = getWorkspaceTerminalCacheKey(dir, scope)
|
||||
for (const cache of caches) {
|
||||
const entry = cache.get(key)
|
||||
entry?.value.clear()
|
||||
}
|
||||
|
||||
void removePersisted(Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal"), platform)
|
||||
void removePersisted(terminalPersistTarget(scope, dir), platform)
|
||||
|
||||
if (scope) return
|
||||
if (scope !== ServerScope.local) return
|
||||
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
||||
for (const id of sessionIDs ?? []) {
|
||||
for (const key of getLegacyTerminalStorageKeys(dir, id)) {
|
||||
@@ -156,14 +145,14 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
||||
function createWorkspaceTerminalSession(
|
||||
sdk: ReturnType<typeof useSDK>,
|
||||
dir: string,
|
||||
scope: ServerScopeValue,
|
||||
legacySessionID?: string,
|
||||
scope?: string,
|
||||
) {
|
||||
const legacy = scope ? [] : getLegacyTerminalStorageKeys(dir, legacySessionID)
|
||||
const legacy = scope === ServerScope.local ? getLegacyTerminalStorageKeys(dir, legacySessionID) : []
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{
|
||||
...Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal", legacy),
|
||||
...terminalPersistTarget(scope, dir, legacy),
|
||||
migrate: migrateTerminalState,
|
||||
},
|
||||
createStore<{
|
||||
@@ -388,9 +377,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
const server = useServer()
|
||||
const params = useParams()
|
||||
const cache = new Map<string, TerminalCacheEntry>()
|
||||
const scope = createMemo(() => {
|
||||
return getTerminalServerScope(server.current, server.key)
|
||||
})
|
||||
const scope = server.scope()
|
||||
|
||||
caches.add(cache)
|
||||
onCleanup(() => caches.delete(cache))
|
||||
@@ -414,7 +401,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
}
|
||||
}
|
||||
|
||||
const loadWorkspace = (dir: string, legacySessionID: string | undefined, serverScope: string | undefined) => {
|
||||
const loadWorkspace = (dir: string, legacySessionID: string | undefined, serverScope: ServerScopeValue) => {
|
||||
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
|
||||
const key = getWorkspaceTerminalCacheKey(dir, serverScope)
|
||||
const existing = cache.get(key)
|
||||
@@ -425,7 +412,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
}
|
||||
|
||||
const entry = createRoot((dispose) => ({
|
||||
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID, serverScope),
|
||||
value: createWorkspaceTerminalSession(sdk, dir, serverScope, legacySessionID),
|
||||
dispose,
|
||||
}))
|
||||
|
||||
@@ -434,11 +421,11 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
return entry.value
|
||||
}
|
||||
|
||||
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id, scope()))
|
||||
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id, scope))
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => ({ dir: params.dir, id: params.id, scope: scope() }),
|
||||
() => ({ dir: params.dir, id: params.id, scope }),
|
||||
(next, prev) => {
|
||||
if (!prev?.dir) return
|
||||
if (next.dir === prev.dir && next.id === prev.id && next.scope === prev.scope) return
|
||||
|
||||
@@ -170,6 +170,7 @@ if (root instanceof HTMLElement) {
|
||||
<AppBaseProviders>
|
||||
<AppInterface
|
||||
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
||||
canonicalLocalServer={ServerConnection.key(server)}
|
||||
servers={[server]}
|
||||
disableHealthCheck
|
||||
/>
|
||||
|
||||
@@ -467,6 +467,7 @@ export const dict = {
|
||||
|
||||
"error.page.title": "Something went wrong",
|
||||
"error.page.description": "An error occurred while loading the application.",
|
||||
"error.page.description.localServerStartup": "An error occurred while starting the local server.",
|
||||
"error.page.details.label": "Error Details",
|
||||
"error.page.action.restart": "Restart",
|
||||
"error.page.action.report": "Report Error",
|
||||
|
||||
@@ -26,7 +26,7 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
|
||||
createResource(
|
||||
() => params.id,
|
||||
(id) => sync.session.sync(id),
|
||||
(id) => sync.session.sync(id).catch(() => {}),
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { errorDescriptionKey } from "./error-description"
|
||||
|
||||
describe("error description", () => {
|
||||
test("describes local server startup errors", () => {
|
||||
expect(errorDescriptionKey(Object.assign(new Error("migration failed"), { localServerStartup: true }))).toBe(
|
||||
"error.page.description.localServerStartup",
|
||||
)
|
||||
})
|
||||
|
||||
test("uses the generic description for other errors", () => {
|
||||
expect(errorDescriptionKey(new Error("unknown"))).toBe("error.page.description")
|
||||
expect(errorDescriptionKey(Object.assign(new Error("unknown"), { localServerStartup: false }))).toBe(
|
||||
"error.page.description",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
export function errorDescriptionKey(error: unknown) {
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"localServerStartup" in error &&
|
||||
error.localServerStartup === true
|
||||
) {
|
||||
return "error.page.description.localServerStartup" as const
|
||||
}
|
||||
return "error.page.description" as const
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { createStore } from "solid-js/store"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { errorDescriptionKey } from "./error-description"
|
||||
|
||||
export type InitError = {
|
||||
name: string
|
||||
@@ -289,7 +290,7 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
<Logo class="w-58.5 opacity-12 shrink-0" />
|
||||
<div class="flex flex-col items-center gap-2 text-center">
|
||||
<h1 class="text-lg font-medium text-text-strong">{language.t("error.page.title")}</h1>
|
||||
<p class="text-sm text-text-weak">{language.t("error.page.description")}</p>
|
||||
<p class="text-sm text-text-weak">{language.t(errorDescriptionKey(props.error))}</p>
|
||||
</div>
|
||||
<TextField
|
||||
value={formattedError()}
|
||||
|
||||
+297
-181
@@ -1,5 +1,5 @@
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { createEffect, createMemo, For, Match, on, onCleanup, onMount, Show, Switch } from "solid-js"
|
||||
import { batch, createEffect, createMemo, For, Match, on, onCleanup, onMount, Show, Switch } from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
@@ -26,7 +26,18 @@ import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { displayName, getProjectAvatarSource, projectForSession, sortedRootSessions } from "@/pages/layout/helpers"
|
||||
import {
|
||||
closeHomeProject,
|
||||
displayName,
|
||||
getProjectAvatarSource,
|
||||
homeProjectDirectories,
|
||||
homeProjectNavigation,
|
||||
homeSessionServerStatus,
|
||||
type HomeProjectSelection,
|
||||
projectForSession,
|
||||
sortedRootSessions,
|
||||
toggleHomeProjectSelection,
|
||||
} from "@/pages/layout/helpers"
|
||||
import { sessionTitle } from "@/utils/session-title"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { messageAgentColor } from "@/utils/agent"
|
||||
@@ -51,6 +62,8 @@ type HomeSessionRecord = {
|
||||
projectName: string
|
||||
}
|
||||
|
||||
type HomeSessionSync = Pick<ReturnType<typeof useServerSync>, "child">
|
||||
|
||||
type HomeSessionGroup = {
|
||||
id: "today" | "yesterday" | "older"
|
||||
title: string
|
||||
@@ -65,8 +78,10 @@ const HOME_SEARCH_RESULT_TITLE =
|
||||
const HOME_SEARCH_RESULT_META =
|
||||
"min-w-0 flex-[1_1_auto] overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]"
|
||||
|
||||
let pendingHomeNavigation: { server: ServerConnection.Key; href: string } | undefined
|
||||
|
||||
function buildHomeSessionRecords(input: {
|
||||
sync: ReturnType<typeof useServerSync>
|
||||
sync: Pick<ReturnType<typeof useServerSync>, "child">
|
||||
projectDirectories: () => string[]
|
||||
projects: () => LocalProject[]
|
||||
projectByID: () => Map<string, LocalProject>
|
||||
@@ -95,6 +110,53 @@ function matchesHomeSessionSearch(record: HomeSessionRecord, query: string) {
|
||||
return `${record.session.title} ${record.projectName}`.toLowerCase().includes(query)
|
||||
}
|
||||
|
||||
function createHomeSessionStatus(input: {
|
||||
record: () => HomeSessionRecord
|
||||
sync: () => HomeSessionSync
|
||||
activeServer: () => boolean
|
||||
}) {
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const sessionStore = createMemo(() => input.sync().child(input.record().session.directory, { bootstrap: false })[0])
|
||||
const unseenCount = createMemo(() =>
|
||||
input.activeServer() ? notification.session.unseenCount(input.record().session.id) : 0,
|
||||
)
|
||||
const hasError = createMemo(
|
||||
() => input.activeServer() && notification.session.unseenHasError(input.record().session.id),
|
||||
)
|
||||
const hasPermissions = createMemo(
|
||||
() =>
|
||||
input.activeServer() &&
|
||||
!!sessionPermissionRequest(
|
||||
sessionStore().session,
|
||||
sessionStore().permission,
|
||||
input.record().session.id,
|
||||
(item) => {
|
||||
return !permission.autoResponds(item, input.record().session.directory)
|
||||
},
|
||||
),
|
||||
)
|
||||
const serverStatus = createMemo(() =>
|
||||
homeSessionServerStatus(input.activeServer(), () => ({
|
||||
working: sessionStore().session_working(input.record().session.id),
|
||||
tint: messageAgentColor(sessionStore().message[input.record().session.id], sessionStore().agent),
|
||||
})),
|
||||
)
|
||||
const isWorking = createMemo(() => {
|
||||
if (hasPermissions()) return false
|
||||
return serverStatus().working
|
||||
})
|
||||
const tint = createMemo(() => serverStatus().tint)
|
||||
return {
|
||||
unseenCount,
|
||||
hasError,
|
||||
hasPermissions,
|
||||
isWorking,
|
||||
tint,
|
||||
show: createMemo(() => isWorking() || hasPermissions() || hasError() || unseenCount() > 0),
|
||||
}
|
||||
}
|
||||
|
||||
function homeSessionSearchKey(record: HomeSessionRecord) {
|
||||
return `${pathKey(record.session.directory)}:${record.session.id}`
|
||||
}
|
||||
@@ -122,12 +184,27 @@ function HomeDesign() {
|
||||
let focusSessionSearch: (() => void) | undefined
|
||||
const [state, setState] = createStore({
|
||||
search: "",
|
||||
project: undefined as string | undefined,
|
||||
selection: { server: server.key } as HomeProjectSelection,
|
||||
searchFocused: false,
|
||||
})
|
||||
|
||||
const projects = createMemo(() => layout.projects.list())
|
||||
const selectedProject = createMemo(() => projects().find((project) => project.worktree === state.project))
|
||||
const focusedServer = createMemo(
|
||||
() => global.servers.list().find((conn) => ServerConnection.key(conn) === state.selection.server) ?? server.current,
|
||||
)
|
||||
const focusedServerCtx = createMemo(() => {
|
||||
const conn = focusedServer()
|
||||
if (!conn) return
|
||||
return global.createServerCtx(conn)
|
||||
})
|
||||
const focusedSync = () => focusedServerCtx()?.sync ?? sync
|
||||
const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list())
|
||||
const selectedProject = createMemo(() => projects().find((project) => project.worktree === state.selection.directory))
|
||||
const newSessionProject = createMemo(
|
||||
() =>
|
||||
selectedProject() ??
|
||||
projects().find((project) => project.worktree === focusedServerCtx()?.projects.last()) ??
|
||||
projects()[0],
|
||||
)
|
||||
const directories = (project: LocalProject) => [project.worktree, ...(project.sandboxes ?? [])]
|
||||
const projectDirectories = createMemo(() => {
|
||||
const project = selectedProject()
|
||||
@@ -136,9 +213,9 @@ function HomeDesign() {
|
||||
})
|
||||
const search = createMemo(() => state.search.trim())
|
||||
const sessionLoad = useQuery(() => ({
|
||||
queryKey: ["home", "sessions", ...projectDirectories()] as const,
|
||||
queryKey: ["home", "sessions", state.selection.server, ...projectDirectories()] as const,
|
||||
queryFn: async () => {
|
||||
await Promise.all(projectDirectories().map((directory) => sync.project.loadSessions(directory)))
|
||||
await Promise.all(projectDirectories().map((directory) => focusedSync().project.loadSessions(directory)))
|
||||
return null
|
||||
},
|
||||
}))
|
||||
@@ -148,7 +225,7 @@ function HomeDesign() {
|
||||
)
|
||||
const allRecords = createMemo(() =>
|
||||
buildHomeSessionRecords({
|
||||
sync,
|
||||
sync: focusedSync(),
|
||||
projectDirectories,
|
||||
projects,
|
||||
projectByID,
|
||||
@@ -163,6 +240,13 @@ function HomeDesign() {
|
||||
const searchOpen = createMemo(() => state.searchFocused && search().length > 0)
|
||||
const groups = createMemo(() => groupSessions(records(), language))
|
||||
|
||||
function setSelection(next: HomeProjectSelection) {
|
||||
batch(() => {
|
||||
if (state.selection.server !== next.server) setState("selection", "server", next.server)
|
||||
if (state.selection.directory !== next.directory) setState("selection", "directory", next.directory)
|
||||
})
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
setState("search", "")
|
||||
setState("searchFocused", false)
|
||||
@@ -183,47 +267,82 @@ function HomeDesign() {
|
||||
},
|
||||
])
|
||||
|
||||
function selectProject(directory: string) {
|
||||
if (!projects().some((project) => project.worktree === directory)) return
|
||||
setState("project", state.project === directory ? undefined : directory)
|
||||
createEffect(() => {
|
||||
const list = global.servers.list()
|
||||
if (list.some((conn) => ServerConnection.key(conn) === state.selection.server)) return
|
||||
const conn = list.find((conn) => ServerConnection.key(conn) === server.key) ?? list[0]
|
||||
if (conn) setSelection({ server: ServerConnection.key(conn) })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const pending = pendingHomeNavigation
|
||||
if (!pending || pending.server !== server.key) return
|
||||
pendingHomeNavigation = undefined
|
||||
navigate(pending.href)
|
||||
})
|
||||
|
||||
function focusServer(conn: ServerConnection.Any) {
|
||||
setSelection({ server: ServerConnection.key(conn) })
|
||||
}
|
||||
|
||||
function addProject(conn: ServerConnection.Any, directory: string) {
|
||||
const server = global.createServerCtx(conn)
|
||||
server.projects.open(directory)
|
||||
server.projects.touch(directory)
|
||||
setState("project", directory)
|
||||
function selectProject(conn: ServerConnection.Any, directory: string) {
|
||||
const key = ServerConnection.key(conn)
|
||||
if (
|
||||
!global
|
||||
.createServerCtx(conn)
|
||||
.projects.list()
|
||||
.some((project) => project.worktree === directory)
|
||||
)
|
||||
return
|
||||
setSelection(toggleHomeProjectSelection(state.selection, key, directory))
|
||||
}
|
||||
|
||||
function addProjects(conn: ServerConnection.Any, directories: string[]) {
|
||||
const directory = directories[0]
|
||||
if (!directory) return
|
||||
const ctx = global.createServerCtx(conn)
|
||||
directories.forEach(ctx.projects.open)
|
||||
ctx.projects.touch(directory)
|
||||
setSelection({ server: ServerConnection.key(conn), directory })
|
||||
}
|
||||
|
||||
function openNewSession() {
|
||||
const project = selectedProject()
|
||||
if (!project) {
|
||||
const conn = server.current
|
||||
if (conn) void chooseProject(conn)
|
||||
const conn = focusedServer()
|
||||
const project = newSessionProject()
|
||||
if (!conn || !project) return
|
||||
openProjectNewSession(conn, project.worktree)
|
||||
}
|
||||
|
||||
function navigateOnServer(conn: ServerConnection.Any, href: string) {
|
||||
const next = homeProjectNavigation(server.key, ServerConnection.key(conn), href)
|
||||
if (!next.server) {
|
||||
navigate(next.href)
|
||||
return
|
||||
}
|
||||
layout.projects.open(project.worktree)
|
||||
server.projects.touch(project.worktree)
|
||||
navigate(`/${base64Encode(project.worktree)}/session`)
|
||||
pendingHomeNavigation = next
|
||||
server.setActive(next.server)
|
||||
}
|
||||
|
||||
function openProjectNewSession(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
server.projects.touch(directory)
|
||||
navigate(`/${base64Encode(directory)}/session`)
|
||||
function openProjectNewSession(conn: ServerConnection.Any, directory: string) {
|
||||
const ctx = global.createServerCtx(conn)
|
||||
ctx.projects.open(directory)
|
||||
ctx.projects.touch(directory)
|
||||
navigateOnServer(conn, `/${base64Encode(directory)}/session`)
|
||||
}
|
||||
|
||||
function editProject(project: LocalProject) {
|
||||
function editProject(conn: ServerConnection.Any, project: LocalProject) {
|
||||
void import("@/components/dialog-edit-project").then((x) => {
|
||||
dialog.show(() => <x.DialogEditProject project={project} />)
|
||||
dialog.show(() => <x.DialogEditProject server={conn} project={project} />)
|
||||
})
|
||||
}
|
||||
|
||||
function unseenCount(project: LocalProject) {
|
||||
function unseenCount(conn: ServerConnection.Any, project: LocalProject) {
|
||||
if (ServerConnection.key(conn) !== server.key) return 0
|
||||
return directories(project).reduce((total, directory) => total + notification.project.unseenCount(directory), 0)
|
||||
}
|
||||
|
||||
function clearNotifications(project: LocalProject) {
|
||||
function clearNotifications(conn: ServerConnection.Any, project: LocalProject) {
|
||||
if (ServerConnection.key(conn) !== server.key) return
|
||||
directories(project)
|
||||
.filter((directory) => notification.project.unseenCount(directory) > 0)
|
||||
.forEach((directory) => notification.project.markViewed(directory))
|
||||
@@ -231,19 +350,18 @@ function HomeDesign() {
|
||||
|
||||
function openSession(session: Session) {
|
||||
const project = projectForSession(session, projects(), projectByID())
|
||||
layout.projects.open(project?.worktree ?? session.directory)
|
||||
server.projects.touch(project?.worktree ?? session.directory)
|
||||
navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
|
||||
const conn = focusedServer()
|
||||
if (!conn) return
|
||||
const directory = project?.worktree ?? session.directory
|
||||
const ctx = global.createServerCtx(conn)
|
||||
ctx.projects.open(directory)
|
||||
ctx.projects.touch(directory)
|
||||
navigateOnServer(conn, `/${base64Encode(session.directory)}/session/${session.id}`)
|
||||
}
|
||||
|
||||
async function chooseProject(conn: ServerConnection.Any) {
|
||||
function resolve(result: string | string[] | null) {
|
||||
if (Array.isArray(result)) {
|
||||
result.forEach((r) => addProject(conn, r))
|
||||
if (result[0]) setState("project", result[0])
|
||||
return
|
||||
}
|
||||
if (result) addProject(conn, result)
|
||||
addProjects(conn, homeProjectDirectories(result))
|
||||
}
|
||||
|
||||
const server = global.createServerCtx(conn)
|
||||
@@ -270,18 +388,24 @@ function HomeDesign() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 bg-background-base self-stretch flex-1">
|
||||
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 bg-v2-background-bg-base self-stretch flex-1">
|
||||
<div class="mx-auto grid w-full h-full max-w-[1080px] gap-8 px-6 pb-16 lg:grid-cols-[280px_minmax(0,720px)]">
|
||||
<HomeProjectColumn
|
||||
projects={projects()}
|
||||
selected={selectedProject()?.worktree}
|
||||
selected={state.selection}
|
||||
focusServer={focusServer}
|
||||
selectProject={selectProject}
|
||||
openNewSession={openProjectNewSession}
|
||||
chooseProject={(conn) => void chooseProject(conn)}
|
||||
editProject={editProject}
|
||||
closeProject={(directory) => {
|
||||
layout.projects.close(directory)
|
||||
if (state.project === directory) setState("project", undefined)
|
||||
closeProject={(conn, directory) => {
|
||||
const next = closeHomeProject(
|
||||
state.selection,
|
||||
ServerConnection.key(conn),
|
||||
global.createServerCtx(conn).projects,
|
||||
directory,
|
||||
)
|
||||
if (next) setSelection(next)
|
||||
}}
|
||||
clearNotifications={clearNotifications}
|
||||
unseenCount={unseenCount}
|
||||
@@ -297,6 +421,8 @@ function HomeDesign() {
|
||||
open={searchOpen()}
|
||||
loading={sessionLoad.isLoading}
|
||||
results={searchResults()}
|
||||
sync={focusedSync()}
|
||||
activeServer={state.selection.server === server.key}
|
||||
noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
|
||||
bindFocus={(focus) => {
|
||||
focusSessionSearch = focus
|
||||
@@ -316,7 +442,10 @@ function HomeDesign() {
|
||||
when={groups().length > 0}
|
||||
fallback={
|
||||
<div class="flex min-w-0 flex-col gap-4">
|
||||
<HomeSessionGroupHeader title={language.t("home.sessions.empty")} onNewSession={openNewSession} />
|
||||
<HomeSessionGroupHeader
|
||||
title={language.t("home.sessions.empty")}
|
||||
onNewSession={newSessionProject() ? openNewSession : undefined}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -325,11 +454,18 @@ function HomeDesign() {
|
||||
<div class="flex min-w-0 flex-col gap-4">
|
||||
<HomeSessionGroupHeader
|
||||
title={group.title}
|
||||
onNewSession={index() === 0 ? openNewSession : undefined}
|
||||
onNewSession={index() === 0 && newSessionProject() ? openNewSession : undefined}
|
||||
/>
|
||||
<div class="flex min-w-0 flex-col gap-px">
|
||||
<For each={group.sessions}>
|
||||
{(record) => <HomeSessionRow record={record} openSession={openSession} />}
|
||||
{(record) => (
|
||||
<HomeSessionRow
|
||||
record={record}
|
||||
sync={focusedSync()}
|
||||
activeServer={state.selection.server === server.key}
|
||||
openSession={openSession}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,14 +483,15 @@ function HomeDesign() {
|
||||
|
||||
function HomeProjectColumn(props: {
|
||||
projects: LocalProject[]
|
||||
selected?: string
|
||||
selectProject: (directory: string) => void
|
||||
openNewSession: (directory: string) => void
|
||||
selected: HomeProjectSelection
|
||||
focusServer: (server: ServerConnection.Any) => void
|
||||
selectProject: (server: ServerConnection.Any, directory: string) => void
|
||||
openNewSession: (server: ServerConnection.Any, directory: string) => void
|
||||
chooseProject: (server: ServerConnection.Any) => void
|
||||
editProject: (project: LocalProject) => void
|
||||
closeProject: (directory: string) => void
|
||||
clearNotifications: (project: LocalProject) => void
|
||||
unseenCount: (project: LocalProject) => number
|
||||
editProject: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
closeProject: (server: ServerConnection.Any, directory: string) => void
|
||||
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
|
||||
openSettings: () => void
|
||||
openHelp: () => void
|
||||
language: ReturnType<typeof useLanguage>
|
||||
@@ -378,41 +515,41 @@ function HomeProjectColumn(props: {
|
||||
</div>
|
||||
<Show
|
||||
when={global.servers.list().length > 1}
|
||||
fallback={<HomeProjectList {...props} chooseProject={() => props.chooseProject(global.servers.list()[0]!)} />}
|
||||
fallback={<HomeProjectList {...props} server={global.servers.list()[0]!} />}
|
||||
>
|
||||
<For each={global.servers.list()}>
|
||||
{(item) => {
|
||||
const key = ServerConnection.key(item)
|
||||
const healthy = () => !!global.servers.health[key]?.healthy
|
||||
const [state, setState] = createStore({ open: true })
|
||||
const serverCtx = global.createServerCtx(item)
|
||||
return (
|
||||
<div class="flex max-h-[min(572px,calc(100vh_-_300px))] min-w-0 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60`}
|
||||
disabled={!healthy()}
|
||||
onClick={() => setState("open", !state.open)}
|
||||
>
|
||||
<div class="flex size-4 shrink-0 items-center justify-center">
|
||||
<ServerHealthIndicator health={global.servers.health[key]} />
|
||||
</div>
|
||||
<span class={HOME_PROJECT_NAV_LABEL}>{item.displayName ?? new URL(item.http.url).host}</span>
|
||||
<Show when={healthy()}>
|
||||
<IconV2
|
||||
name="outline-chevron-down"
|
||||
class="shrink-0 text-v2-icon-icon-muted data-[open=false]:-rotate-90"
|
||||
data-open={state.open}
|
||||
/>
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={healthy() && state.open}>
|
||||
<div class="mx-3 h-px bg-v2-border-border-base" />
|
||||
<HomeProjectList
|
||||
{...props}
|
||||
projects={serverCtx.projects.list()}
|
||||
chooseProject={() => props.chooseProject(item)}
|
||||
<div class="group/server relative flex h-7 min-w-0 items-center rounded-[6px]">
|
||||
<button
|
||||
type="button"
|
||||
class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
|
||||
data-selected={props.selected.server === key && !props.selected.directory ? "" : undefined}
|
||||
disabled={!healthy()}
|
||||
onClick={() => props.focusServer(item)}
|
||||
>
|
||||
<div class="flex size-4 shrink-0 items-center justify-center">
|
||||
<ServerHealthIndicator health={global.servers.health[key]} />
|
||||
</div>
|
||||
<span class={HOME_PROJECT_NAV_LABEL}>{item.displayName ?? new URL(item.http.url).host}</span>
|
||||
</button>
|
||||
<IconButtonV2
|
||||
data-action="home-add-project"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
class="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 transition-opacity group-hover/server:opacity-100 focus:opacity-100"
|
||||
icon={<IconV2 name="folder-add-left" />}
|
||||
aria-label={props.language.t("home.project.add")}
|
||||
onClick={() => props.chooseProject(item)}
|
||||
/>
|
||||
</div>
|
||||
<Show when={healthy()}>
|
||||
<div class="mx-3 h-px bg-v2-border-border-base" />
|
||||
<HomeProjectList {...props} server={item} projects={serverCtx.projects.list()} />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
@@ -442,61 +579,52 @@ function HomeProjectColumn(props: {
|
||||
}
|
||||
|
||||
function HomeProjectList(props: {
|
||||
server: ServerConnection.Any
|
||||
projects: LocalProject[]
|
||||
selected?: string
|
||||
selectProject: (directory: string) => void
|
||||
openNewSession: (directory: string) => void
|
||||
chooseProject: () => void
|
||||
editProject: (project: LocalProject) => void
|
||||
closeProject: (directory: string) => void
|
||||
clearNotifications: (project: LocalProject) => void
|
||||
unseenCount: (project: LocalProject) => number
|
||||
selected: HomeProjectSelection
|
||||
selectProject: (server: ServerConnection.Any, directory: string) => void
|
||||
openNewSession: (server: ServerConnection.Any, directory: string) => void
|
||||
editProject: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
closeProject: (server: ServerConnection.Any, directory: string) => void
|
||||
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
return (
|
||||
<Show
|
||||
when={props.projects.length > 0}
|
||||
fallback={
|
||||
<button
|
||||
type="button"
|
||||
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
||||
onClick={props.chooseProject}
|
||||
>
|
||||
<IconV2 name="folder-add-left" size="small" />
|
||||
<span>{props.language.t("home.project.add")}</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<For each={props.projects}>
|
||||
{(project) => (
|
||||
<HomeProjectRow
|
||||
project={project}
|
||||
selected={props.selected === project.worktree}
|
||||
unseenCount={props.unseenCount(project)}
|
||||
selectProject={props.selectProject}
|
||||
openNewSession={props.openNewSession}
|
||||
editProject={props.editProject}
|
||||
closeProject={props.closeProject}
|
||||
clearNotifications={props.clearNotifications}
|
||||
language={props.language}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<For each={props.projects}>
|
||||
{(project) => (
|
||||
<HomeProjectRow
|
||||
project={project}
|
||||
server={props.server}
|
||||
selected={
|
||||
props.selected.server === ServerConnection.key(props.server) &&
|
||||
props.selected.directory === project.worktree
|
||||
}
|
||||
unseenCount={props.unseenCount(props.server, project)}
|
||||
selectProject={props.selectProject}
|
||||
openNewSession={props.openNewSession}
|
||||
editProject={props.editProject}
|
||||
closeProject={props.closeProject}
|
||||
clearNotifications={props.clearNotifications}
|
||||
language={props.language}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeProjectRow(props: {
|
||||
project: LocalProject
|
||||
server: ServerConnection.Any
|
||||
selected: boolean
|
||||
unseenCount: number
|
||||
selectProject: (directory: string) => void
|
||||
openNewSession: (directory: string) => void
|
||||
editProject: (project: LocalProject) => void
|
||||
closeProject: (directory: string) => void
|
||||
clearNotifications: (project: LocalProject) => void
|
||||
selectProject: (server: ServerConnection.Any, directory: string) => void
|
||||
openNewSession: (server: ServerConnection.Any, directory: string) => void
|
||||
editProject: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
closeProject: (server: ServerConnection.Any, directory: string) => void
|
||||
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const [state, setState] = createStore({ menuOpen: false })
|
||||
@@ -508,7 +636,7 @@ function HomeProjectRow(props: {
|
||||
class={`${HOME_PROJECT_NAV_ROW} pr-16`}
|
||||
data-selected={props.selected ? "" : undefined}
|
||||
aria-current={props.selected ? "page" : undefined}
|
||||
onClick={() => props.selectProject(props.project.worktree)}
|
||||
onClick={() => props.selectProject(props.server, props.project.worktree)}
|
||||
>
|
||||
<HomeProjectAvatar project={props.project} />
|
||||
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
|
||||
@@ -523,7 +651,7 @@ function HomeProjectRow(props: {
|
||||
size="small"
|
||||
icon={<IconV2 name="edit" />}
|
||||
aria-label={props.language.t("command.session.new")}
|
||||
onClick={() => props.openNewSession(props.project.worktree)}
|
||||
onClick={() => props.openNewSession(props.server, props.project.worktree)}
|
||||
/>
|
||||
<MenuV2
|
||||
gutter={4}
|
||||
@@ -542,17 +670,20 @@ function HomeProjectRow(props: {
|
||||
/>
|
||||
<MenuV2.Portal>
|
||||
<MenuV2.Content>
|
||||
<MenuV2.Item onSelect={() => props.openNewSession(props.project.worktree)}>
|
||||
<MenuV2.Item onSelect={() => props.openNewSession(props.server, props.project.worktree)}>
|
||||
{props.language.t("command.session.new")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item onSelect={() => props.editProject(props.project)}>
|
||||
<MenuV2.Item onSelect={() => props.editProject(props.server, props.project)}>
|
||||
{props.language.t("common.edit")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item disabled={props.unseenCount === 0} onSelect={() => props.clearNotifications(props.project)}>
|
||||
<MenuV2.Item
|
||||
disabled={props.unseenCount === 0}
|
||||
onSelect={() => props.clearNotifications(props.server, props.project)}
|
||||
>
|
||||
{props.language.t("sidebar.project.clearNotifications")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Separator />
|
||||
<MenuV2.Item onSelect={() => props.closeProject(props.project.worktree)}>
|
||||
<MenuV2.Item onSelect={() => props.closeProject(props.server, props.project.worktree)}>
|
||||
{props.language.t("common.close")}
|
||||
</MenuV2.Item>
|
||||
</MenuV2.Content>
|
||||
@@ -580,6 +711,8 @@ function HomeSessionSearch(props: {
|
||||
open: boolean
|
||||
loading: boolean
|
||||
results: HomeSessionRecord[]
|
||||
sync: HomeSessionSync
|
||||
activeServer: boolean
|
||||
noResultsLabel: string
|
||||
bindFocus: (focus: () => void) => void
|
||||
onInput: (value: string) => void
|
||||
@@ -694,6 +827,8 @@ function HomeSessionSearch(props: {
|
||||
{(record) => (
|
||||
<HomeSessionSearchResultRow
|
||||
record={record}
|
||||
sync={props.sync}
|
||||
activeServer={props.activeServer}
|
||||
selected={store.active === homeSessionSearchKey(record)}
|
||||
onHighlight={() => setStore("active", homeSessionSearchKey(record))}
|
||||
onSelect={(session) => props.onSelect(session)}
|
||||
@@ -778,29 +913,18 @@ function HomeSessionSearch(props: {
|
||||
|
||||
function HomeSessionSearchResultRow(props: {
|
||||
record: HomeSessionRecord
|
||||
sync: HomeSessionSync
|
||||
activeServer: boolean
|
||||
selected: boolean
|
||||
onHighlight: () => void
|
||||
onSelect: (session: Session) => void
|
||||
}) {
|
||||
const globalSync = useServerSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const [sessionStore] = globalSync.child(props.record.session.directory, { bootstrap: false })
|
||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id))
|
||||
const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id))
|
||||
const hasPermissions = createMemo(
|
||||
() =>
|
||||
!!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.record.session.id, (item) => {
|
||||
return !permission.autoResponds(item, props.record.session.directory)
|
||||
}),
|
||||
)
|
||||
const isWorking = createMemo(() => {
|
||||
if (hasPermissions()) return false
|
||||
return sessionStore.session_working(props.record.session.id)
|
||||
const status = createHomeSessionStatus({
|
||||
record: () => props.record,
|
||||
sync: () => props.sync,
|
||||
activeServer: () => props.activeServer,
|
||||
})
|
||||
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.record.session.id], sessionStore.agent))
|
||||
const showStatus = createMemo(() => isWorking() || hasPermissions() || hasError() || unseenCount() > 0)
|
||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||
|
||||
const key = () => homeSessionSearchKey(props.record)
|
||||
|
||||
@@ -820,7 +944,7 @@ function HomeSessionSearchResultRow(props: {
|
||||
onClick={() => props.onSelect(props.record.session)}
|
||||
>
|
||||
<Show
|
||||
when={showStatus()}
|
||||
when={status.show()}
|
||||
fallback={
|
||||
<div class="flex size-4 shrink-0 items-center justify-center">
|
||||
<TabStateIndicator />
|
||||
@@ -829,19 +953,19 @@ function HomeSessionSearchResultRow(props: {
|
||||
>
|
||||
<div
|
||||
class="flex size-4 shrink-0 items-center justify-center"
|
||||
style={{ color: tint() ?? "var(--icon-interactive-base)" }}
|
||||
style={{ color: status.tint() ?? "var(--icon-interactive-base)" }}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={isWorking()}>
|
||||
<Match when={status.isWorking()}>
|
||||
<Spinner class="size-[15px]" />
|
||||
</Match>
|
||||
<Match when={hasPermissions()}>
|
||||
<Match when={status.hasPermissions()}>
|
||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||
</Match>
|
||||
<Match when={hasError()}>
|
||||
<Match when={status.hasError()}>
|
||||
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
||||
</Match>
|
||||
<Match when={unseenCount() > 0}>
|
||||
<Match when={status.unseenCount() > 0}>
|
||||
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -884,26 +1008,18 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
|
||||
)
|
||||
}
|
||||
|
||||
function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) {
|
||||
const globalSync = useServerSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const [sessionStore] = globalSync.child(props.record.session.directory, { bootstrap: false })
|
||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id))
|
||||
const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id))
|
||||
const hasPermissions = createMemo(
|
||||
() =>
|
||||
!!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.record.session.id, (item) => {
|
||||
return !permission.autoResponds(item, props.record.session.directory)
|
||||
}),
|
||||
)
|
||||
const isWorking = createMemo(() => {
|
||||
if (hasPermissions()) return false
|
||||
return sessionStore.session_working(props.record.session.id)
|
||||
function HomeSessionRow(props: {
|
||||
record: HomeSessionRecord
|
||||
sync: HomeSessionSync
|
||||
activeServer: boolean
|
||||
openSession: (session: Session) => void
|
||||
}) {
|
||||
const status = createHomeSessionStatus({
|
||||
record: () => props.record,
|
||||
sync: () => props.sync,
|
||||
activeServer: () => props.activeServer,
|
||||
})
|
||||
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.record.session.id], sessionStore.agent))
|
||||
const showStatus = createMemo(() => isWorking() || hasPermissions() || hasError() || unseenCount() > 0)
|
||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -913,7 +1029,7 @@ function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (sessio
|
||||
onClick={() => props.openSession(props.record.session)}
|
||||
>
|
||||
<Show
|
||||
when={showStatus()}
|
||||
when={status.show()}
|
||||
fallback={
|
||||
<div class="flex size-4 shrink-0 items-center justify-center">
|
||||
<TabStateIndicator />
|
||||
@@ -922,19 +1038,19 @@ function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (sessio
|
||||
>
|
||||
<div
|
||||
class="flex size-4 shrink-0 items-center justify-center"
|
||||
style={{ color: tint() ?? "var(--icon-interactive-base)" }}
|
||||
style={{ color: status.tint() ?? "var(--icon-interactive-base)" }}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={isWorking()}>
|
||||
<Match when={status.isWorking()}>
|
||||
<Spinner class="size-[15px]" />
|
||||
</Match>
|
||||
<Match when={hasPermissions()}>
|
||||
<Match when={status.hasPermissions()}>
|
||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||
</Match>
|
||||
<Match when={hasError()}>
|
||||
<Match when={status.hasError()}>
|
||||
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
||||
</Match>
|
||||
<Match when={unseenCount() > 0}>
|
||||
<Match when={status.unseenCount() > 0}>
|
||||
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -37,7 +37,7 @@ import { useProviders } from "@/hooks/use-providers"
|
||||
import { toaster } from "@opencode-ai/ui/toast"
|
||||
import { setV2Toast, showToast, ToastRegion } from "@/utils/toast"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
||||
import { clearWorkspaceTerminals } from "@/context/terminal"
|
||||
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
||||
import {
|
||||
clearSessionPrefetchInflight,
|
||||
@@ -57,6 +57,7 @@ import { createAim } from "@/utils/aim"
|
||||
import { setNavigate } from "@/utils/notification-click"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope"
|
||||
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||
@@ -64,7 +65,7 @@ import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { DebugBar } from "@/components/debug-bar"
|
||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||
import { useServer } from "@/context/server"
|
||||
import { ServerConnection, useServer } from "@/context/server"
|
||||
import { useLanguage, type Locale } from "@/context/language"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import {
|
||||
@@ -92,8 +93,9 @@ import { SidebarContent } from "./layout/sidebar-shell"
|
||||
import { runUpdateAndRestart } from "./layout/update"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const serverSDK = useServerSDK()
|
||||
const [store, setStore, , ready] = persisted(
|
||||
Persist.global("layout.page", ["layout.page.v1"]),
|
||||
Persist.serverGlobal(serverSDK.scope, "layout.page", ["layout.page.v1"]),
|
||||
createStore({
|
||||
lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } },
|
||||
activeProject: undefined as string | undefined,
|
||||
@@ -113,7 +115,6 @@ export default function Layout(props: ParentProps) {
|
||||
let dialogDead = false
|
||||
|
||||
const params = useParams()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const layout = useLayout()
|
||||
const layoutReady = createMemo(() => layout.ready())
|
||||
@@ -411,13 +412,17 @@ export default function Layout(props: ParentProps) {
|
||||
const unsub = serverSDK.event.listen((e) => {
|
||||
if (e.details?.type === "worktree.ready") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.ready(e.name)
|
||||
WorktreeState.ready(serverSDK.scope, e.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.details?.type === "worktree.failed") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed"))
|
||||
WorktreeState.failed(
|
||||
serverSDK.scope,
|
||||
e.name,
|
||||
e.details.properties?.message ?? language.t("common.requestFailed"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -693,7 +698,7 @@ export default function Layout(props: ParentProps) {
|
||||
serverSDK.url
|
||||
|
||||
prefetchToken.value += 1
|
||||
clearSessionPrefetchInflight()
|
||||
clearSessionPrefetchInflight(serverSDK.scope)
|
||||
prefetchQueues.clear()
|
||||
})
|
||||
|
||||
@@ -740,13 +745,14 @@ export default function Layout(props: ParentProps) {
|
||||
const [store, setStore] = serverSync.child(directory, { bootstrap: false })
|
||||
|
||||
return runSessionPrefetch({
|
||||
scope: serverSDK.scope,
|
||||
directory,
|
||||
sessionID,
|
||||
task: (rev) =>
|
||||
retry(() => serverSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
||||
.then((messages) => {
|
||||
if (prefetchToken.value !== token) return
|
||||
if (!isSessionPrefetchCurrent(directory, sessionID, rev)) 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)
|
||||
@@ -761,7 +767,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
if (stale.length > 0) {
|
||||
clearSessionPrefetch(directory, stale)
|
||||
clearSessionPrefetch(serverSDK.scope, directory, stale)
|
||||
for (const id of stale) {
|
||||
serverSync.todo.set(id, undefined)
|
||||
}
|
||||
@@ -773,7 +779,7 @@ export default function Layout(props: ParentProps) {
|
||||
sorted,
|
||||
)
|
||||
|
||||
if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return
|
||||
if (!isSessionPrefetchCurrent(serverSDK.scope, directory, sessionID, rev)) return
|
||||
|
||||
batch(() => {
|
||||
if (stale.length > 0) {
|
||||
@@ -785,7 +791,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
setStore("message", sessionID, reconcile(merged, { key: "id" }))
|
||||
setSessionPrefetch({ directory, sessionID, ...meta })
|
||||
setSessionPrefetch({ scope: serverSDK.scope, directory, sessionID, ...meta })
|
||||
|
||||
for (const message of items) {
|
||||
const currentParts = store.part[message.info.id] ?? []
|
||||
@@ -830,7 +836,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
||||
const cached = untrack(() => {
|
||||
const info = getSessionPrefetch(directory, session.id)
|
||||
const info = getSessionPrefetch(serverSDK.scope, directory, session.id)
|
||||
return shouldSkipSessionPrefetch({
|
||||
message: store.message[session.id] !== undefined,
|
||||
info,
|
||||
@@ -1381,7 +1387,9 @@ export default function Layout(props: ParentProps) {
|
||||
void openProject(link.directory, false)
|
||||
const slug = base64Encode(link.directory)
|
||||
if (link.prompt) {
|
||||
setSessionHandoff(slug, { prompt: link.prompt })
|
||||
setSessionHandoff(SessionStateKey.from(server.scope(), SessionRouteKey.fromLegacy(slug)), {
|
||||
prompt: link.prompt,
|
||||
})
|
||||
}
|
||||
const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session`
|
||||
navigateWithSidebarReset(href)
|
||||
@@ -1456,11 +1464,11 @@ export default function Layout(props: ParentProps) {
|
||||
layout.sidebar.toggleWorkspaces(project.worktree)
|
||||
}
|
||||
|
||||
const showEditProjectDialog = (project: LocalProject) => {
|
||||
const showEditProjectDialog = (conn: ServerConnection.Any, project: LocalProject) => {
|
||||
const run = ++dialogRun
|
||||
void import("@/components/dialog-edit-project").then((x) => {
|
||||
if (dialogDead || dialogRun !== run) return
|
||||
dialog.show(() => <x.DialogEditProject project={project} />)
|
||||
dialog.show(() => <x.DialogEditProject server={conn} project={project} />)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1576,7 +1584,7 @@ export default function Layout(props: ParentProps) {
|
||||
directory,
|
||||
sessions.map((s) => s.id),
|
||||
platform,
|
||||
getTerminalServerScope(server.current, server.key),
|
||||
serverSDK.scope,
|
||||
)
|
||||
await serverSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||
|
||||
@@ -1890,7 +1898,7 @@ export default function Layout(props: ParentProps) {
|
||||
directory && pathKey(directory) !== pathKey(local) && !dirs.some((item) => pathKey(item) === pathKey(directory))
|
||||
? directory
|
||||
: undefined
|
||||
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
|
||||
const pending = extra ? WorktreeState.get(serverSDK.scope, extra)?.status === "pending" : false
|
||||
|
||||
const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
|
||||
if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)]
|
||||
@@ -1962,7 +1970,7 @@ export default function Layout(props: ParentProps) {
|
||||
const root = pathKey(local)
|
||||
|
||||
setBusy(created.directory, true)
|
||||
WorktreeState.pending(created.directory)
|
||||
WorktreeState.pending(serverSDK.scope, created.directory)
|
||||
setStore("workspaceExpanded", key, true)
|
||||
if (key !== created.directory) {
|
||||
setStore("workspaceExpanded", created.directory, true)
|
||||
@@ -2023,7 +2031,7 @@ export default function Layout(props: ParentProps) {
|
||||
navigateToProject,
|
||||
openSidebar: () => layout.sidebar.open(),
|
||||
closeProject,
|
||||
showEditProjectDialog,
|
||||
showEditProjectDialog: (proj) => showEditProjectDialog(server.current!, proj),
|
||||
toggleProjectWorkspaces,
|
||||
workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(),
|
||||
workspaceIds,
|
||||
@@ -2170,7 +2178,7 @@ export default function Layout(props: ParentProps) {
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
showEditProjectDialog(project)
|
||||
showEditProjectDialog(server.current!, project)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||
|
||||
@@ -9,13 +9,21 @@ import {
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
childSessionOnPath,
|
||||
closeHomeProject,
|
||||
displayName,
|
||||
effectiveWorkspaceOrder,
|
||||
errorMessage,
|
||||
hasProjectPermissions,
|
||||
homeProjectNavigation,
|
||||
homeProjectDirectories,
|
||||
homeSessionServerStatus,
|
||||
latestRootSession,
|
||||
toggleHomeProjectSelection,
|
||||
} from "./helpers"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
|
||||
const serverKey = ServerConnection.Key.make
|
||||
|
||||
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
||||
({
|
||||
@@ -215,6 +223,94 @@ describe("layout workspace helpers", () => {
|
||||
test("formats fallback project display name", () => {
|
||||
expect(displayName({ worktree: "/tmp/app" })).toBe("app")
|
||||
expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App")
|
||||
expect(displayName({ worktree: "/" })).toBe("/")
|
||||
})
|
||||
|
||||
test("scopes home project selection by server", () => {
|
||||
expect(
|
||||
toggleHomeProjectSelection(undefined, serverKey("https://debian.example"), "/home/luke/repos/amazon"),
|
||||
).toEqual({
|
||||
server: serverKey("https://debian.example"),
|
||||
directory: "/home/luke/repos/amazon",
|
||||
})
|
||||
expect(
|
||||
toggleHomeProjectSelection(
|
||||
{ server: serverKey("https://windows.example"), directory: "/home/luke/repos/amazon" },
|
||||
serverKey("https://debian.example"),
|
||||
"/home/luke/repos/amazon",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://debian.example"), directory: "/home/luke/repos/amazon" })
|
||||
expect(
|
||||
toggleHomeProjectSelection(
|
||||
{ server: serverKey("https://debian.example"), directory: "/home/luke/repos/amazon" },
|
||||
serverKey("https://debian.example"),
|
||||
"/home/luke/repos/amazon",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://debian.example") })
|
||||
})
|
||||
|
||||
test("closes a home project through its server context", () => {
|
||||
const closed: string[] = []
|
||||
|
||||
expect(
|
||||
closeHomeProject(
|
||||
{ server: serverKey("https://windows.example"), directory: "/shared" },
|
||||
serverKey("https://debian.example"),
|
||||
{ close: (directory) => closed.push(directory) },
|
||||
"/shared",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://windows.example"), directory: "/shared" })
|
||||
expect(closed).toEqual(["/shared"])
|
||||
expect(
|
||||
closeHomeProject(
|
||||
{ server: serverKey("https://debian.example"), directory: "/shared" },
|
||||
serverKey("https://debian.example"),
|
||||
{ close: (directory) => closed.push(directory) },
|
||||
"/shared",
|
||||
),
|
||||
).toEqual({ server: serverKey("https://debian.example") })
|
||||
})
|
||||
|
||||
test("defers home project navigation until its server is active", () => {
|
||||
expect(
|
||||
homeProjectNavigation(serverKey("sidecar"), serverKey("https://debian.example"), "/YW1hem9u/session"),
|
||||
).toEqual({
|
||||
server: serverKey("https://debian.example"),
|
||||
href: "/YW1hem9u/session",
|
||||
})
|
||||
expect(
|
||||
homeProjectNavigation(
|
||||
serverKey("https://debian.example"),
|
||||
serverKey("https://debian.example"),
|
||||
"/YW1hem9u/session",
|
||||
),
|
||||
).toEqual({
|
||||
href: "/YW1hem9u/session",
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves picker order when adding multiple projects", () => {
|
||||
expect(homeProjectDirectories(["/first", "/second"])).toEqual(["/first", "/second"])
|
||||
expect(homeProjectDirectories("/only")).toEqual(["/only"])
|
||||
expect(homeProjectDirectories(null)).toEqual([])
|
||||
})
|
||||
|
||||
test("hides status derived from an inactive server", () => {
|
||||
let reads = 0
|
||||
const status = () => {
|
||||
reads++
|
||||
return { working: true, tint: "red" }
|
||||
}
|
||||
expect(homeSessionServerStatus(false, status)).toEqual({
|
||||
working: false,
|
||||
tint: undefined,
|
||||
})
|
||||
expect(reads).toBe(0)
|
||||
expect(homeSessionServerStatus(true, status)).toEqual({
|
||||
working: true,
|
||||
tint: "red",
|
||||
})
|
||||
expect(reads).toBe(1)
|
||||
})
|
||||
|
||||
test("extracts api error message and fallback", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import type { ServerConnection } from "@/context/server"
|
||||
|
||||
type SessionStore = {
|
||||
session?: Session[]
|
||||
@@ -53,7 +54,44 @@ export const childSessionOnPath = (sessions: Session[] | undefined, rootID: stri
|
||||
}
|
||||
|
||||
export const displayName = (project: { name?: string; worktree: string }) =>
|
||||
project.name || getFilename(project.worktree)
|
||||
project.name || getFilename(project.worktree) || project.worktree
|
||||
|
||||
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
|
||||
|
||||
export function toggleHomeProjectSelection(
|
||||
current: HomeProjectSelection | undefined,
|
||||
server: ServerConnection.Key,
|
||||
directory: string,
|
||||
): HomeProjectSelection {
|
||||
if (current?.server === server && current.directory === directory) return { server }
|
||||
return { server, directory }
|
||||
}
|
||||
|
||||
export function closeHomeProject(
|
||||
selected: HomeProjectSelection | undefined,
|
||||
server: ServerConnection.Key,
|
||||
projects: { close: (directory: string) => void },
|
||||
directory: string,
|
||||
) {
|
||||
projects.close(directory)
|
||||
if (selected?.server === server && selected.directory === directory) return { server }
|
||||
return selected
|
||||
}
|
||||
|
||||
export function homeProjectNavigation(active: ServerConnection.Key, server: ServerConnection.Key, href: string) {
|
||||
if (active === server) return { href }
|
||||
return { server, href }
|
||||
}
|
||||
|
||||
export function homeProjectDirectories(result: string | string[] | null) {
|
||||
if (!result) return []
|
||||
return Array.isArray(result) ? result : [result]
|
||||
}
|
||||
|
||||
export function homeSessionServerStatus(active: boolean, status: () => { working: boolean; tint?: string }) {
|
||||
if (!active) return { working: false, tint: undefined }
|
||||
return status()
|
||||
}
|
||||
|
||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
@@ -67,7 +105,7 @@ export function getProjectAvatarSource(id?: string, icon?: { color?: string; url
|
||||
export function projectForSession<T extends { id?: string; worktree: string; sandboxes?: string[] }>(
|
||||
session: Session,
|
||||
projects: T[],
|
||||
byID: Map<string, T>,
|
||||
byID: Map<string, T> = new Map(projects.flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
|
||||
) {
|
||||
const direct = byID.get(session.projectID)
|
||||
if (direct) return direct
|
||||
|
||||
@@ -4,18 +4,24 @@ import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
|
||||
|
||||
export function useSessionTabAvatarState(directory: Accessor<string>, sessionId: Accessor<string>) {
|
||||
export function useSessionTabAvatarState(
|
||||
directory: Accessor<string>,
|
||||
sessionId: Accessor<string>,
|
||||
active: Accessor<boolean> = () => true,
|
||||
) {
|
||||
const globalSync = useServerSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const hasPermissions = createMemo(() => {
|
||||
if (!active()) return false
|
||||
const [store] = globalSync.child(directory(), { bootstrap: false })
|
||||
return !!sessionPermissionRequest(store.session, store.permission, sessionId(), (item) => {
|
||||
return !permission.autoResponds(item, directory())
|
||||
})
|
||||
})
|
||||
const unread = createMemo(() => hasPermissions() || notification.session.unseenCount(sessionId()) > 0)
|
||||
const unread = createMemo(() => active() && (hasPermissions() || notification.session.unseenCount(sessionId()) > 0))
|
||||
const loading = createMemo(() => {
|
||||
if (!active()) return false
|
||||
if (hasPermissions()) return false
|
||||
const [store] = globalSync.child(directory(), { bootstrap: false })
|
||||
return store.session_working(sessionId())
|
||||
|
||||
@@ -39,6 +39,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { useServer } from "@/context/server"
|
||||
import { syncSessionModel } from "@/pages/session/session-model-helpers"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
@@ -190,13 +192,15 @@ export default function Page() {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const sdk = useSDK()
|
||||
const serverSDK = useServerSDK()
|
||||
const settings = useSettings()
|
||||
const prompt = usePrompt()
|
||||
const comments = useComments()
|
||||
const terminal = useTerminal()
|
||||
const server = useServer()
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||
const location = useLocation()
|
||||
const { params, sessionKey, tabs, view } = useSessionLayout()
|
||||
const { params, sessionKey, workspaceKey, tabs, view } = useSessionLayout()
|
||||
const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns())
|
||||
|
||||
createEffect(() => {
|
||||
@@ -223,7 +227,6 @@ export default function Page() {
|
||||
|
||||
const composer = createSessionComposerState()
|
||||
|
||||
const workspaceKey = createMemo(() => params.dir ?? "")
|
||||
const workspaceTabs = createMemo(() => layout.tabs(workspaceKey))
|
||||
|
||||
createEffect(
|
||||
@@ -239,6 +242,7 @@ export default function Page() {
|
||||
layout.handoff.clearTabs()
|
||||
return
|
||||
}
|
||||
if (pending.scope !== server.scope()) return
|
||||
|
||||
if (pending.id !== id) return
|
||||
layout.handoff.clearTabs()
|
||||
@@ -386,7 +390,7 @@ export default function Page() {
|
||||
})
|
||||
|
||||
const [followup, setFollowup] = persisted(
|
||||
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
|
||||
Persist.serverWorkspace(serverSDK.scope, sdk.directory, "followup", ["followup.v1"]),
|
||||
createStore<{
|
||||
items: Record<string, FollowupItem[] | undefined>
|
||||
failed: Record<string, string | undefined>
|
||||
@@ -633,7 +637,7 @@ export default function Page() {
|
||||
const stale = !cached
|
||||
? false
|
||||
: (() => {
|
||||
const info = getSessionPrefetch(directory, id)
|
||||
const info = getSessionPrefetch(serverSDK.scope, directory, id)
|
||||
if (!info) return true
|
||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
||||
})()
|
||||
|
||||
@@ -10,6 +10,8 @@ import { useLanguage } from "@/context/language"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { ScopedKey } from "@/utils/server-scope"
|
||||
|
||||
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
|
||||
|
||||
@@ -60,12 +62,14 @@ function Option(props: {
|
||||
|
||||
export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => {
|
||||
const sdk = useSDK()
|
||||
const serverSDK = useServerSDK()
|
||||
const language = useLanguage()
|
||||
const cacheKey = ScopedKey.from(serverSDK.scope, props.request.id)
|
||||
|
||||
const questions = createMemo(() => props.request.questions)
|
||||
const total = createMemo(() => questions().length)
|
||||
|
||||
const cached = cache.get(props.request.id)
|
||||
const cached = cache.get(cacheKey)
|
||||
const [store, setStore] = createStore({
|
||||
tab: cached?.tab ?? 0,
|
||||
answers: cached?.answers ?? ([] as QuestionAnswer[]),
|
||||
@@ -191,7 +195,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
onCleanup(() => {
|
||||
if (focusFrame !== undefined) cancelAnimationFrame(focusFrame)
|
||||
if (replied) return
|
||||
cache.set(props.request.id, {
|
||||
cache.set(cacheKey, {
|
||||
tab: store.tab,
|
||||
answers: store.answers.map((a) => (a ? [...a] : [])),
|
||||
custom: store.custom.map((s) => s ?? ""),
|
||||
@@ -211,7 +215,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
},
|
||||
onSuccess: () => {
|
||||
replied = true
|
||||
cache.delete(props.request.id)
|
||||
cache.delete(cacheKey)
|
||||
},
|
||||
onError: fail,
|
||||
}))
|
||||
@@ -223,7 +227,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
},
|
||||
onSuccess: () => {
|
||||
replied = true
|
||||
cache.delete(props.request.id)
|
||||
cache.delete(cacheKey)
|
||||
},
|
||||
onError: fail,
|
||||
}))
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useServer } from "@/context/server"
|
||||
import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope"
|
||||
|
||||
export const useSessionKey = () => {
|
||||
const params = useParams()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
return { params, sessionKey }
|
||||
const server = useServer()
|
||||
const scope = createMemo(() => server.scope())
|
||||
const workspaceKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(params.dir)))
|
||||
const sessionKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(params.dir, params.id)))
|
||||
return { params, sessionKey, workspaceKey }
|
||||
}
|
||||
|
||||
export const useSessionLayout = () => {
|
||||
const layout = useLayout()
|
||||
const { params, sessionKey } = useSessionKey()
|
||||
const { params, sessionKey, workspaceKey } = useSessionKey()
|
||||
return {
|
||||
params,
|
||||
sessionKey,
|
||||
workspaceKey,
|
||||
tabs: createMemo(() => layout.tabs(sessionKey)),
|
||||
view: createMemo(() => layout.view(sessionKey)),
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function TerminalPanel() {
|
||||
const terminal = useTerminal()
|
||||
const language = useLanguage()
|
||||
const command = useCommand()
|
||||
const { params, view } = useSessionLayout()
|
||||
const { params, workspaceKey, view } = useSessionLayout()
|
||||
|
||||
const opened = createMemo(() => view().terminal.opened())
|
||||
const size = createSizing()
|
||||
@@ -126,7 +126,7 @@ export function TerminalPanel() {
|
||||
language.locale()
|
||||
|
||||
setTerminalHandoff(
|
||||
dir,
|
||||
workspaceKey(),
|
||||
terminal.all().map((pty) =>
|
||||
terminalTabLabel({
|
||||
title: pty.title,
|
||||
@@ -140,7 +140,7 @@ export function TerminalPanel() {
|
||||
const handoff = createMemo(() => {
|
||||
const dir = params.dir
|
||||
if (!dir) return []
|
||||
return getTerminalHandoff(dir) ?? []
|
||||
return getTerminalHandoff(workspaceKey()) ?? []
|
||||
})
|
||||
|
||||
const all = terminal.all
|
||||
|
||||
@@ -418,23 +418,26 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
}),
|
||||
]
|
||||
|
||||
const fileCmds = () => [
|
||||
fileCommand({
|
||||
id: "file.open",
|
||||
title: language.t("command.file.open"),
|
||||
description: language.t("palette.search.placeholder"),
|
||||
keybind: "mod+k,mod+p",
|
||||
slash: "open",
|
||||
onSelect: openFile,
|
||||
}),
|
||||
fileCommand({
|
||||
id: "tab.close",
|
||||
title: language.t("command.tab.close"),
|
||||
keybind: "mod+w",
|
||||
disabled: !closableTab(),
|
||||
onSelect: closeTab,
|
||||
}),
|
||||
]
|
||||
const fileCmds = () => {
|
||||
const tab = closableTab()
|
||||
return [
|
||||
fileCommand({
|
||||
id: "file.open",
|
||||
title: language.t("command.file.open"),
|
||||
description: language.t("palette.search.placeholder"),
|
||||
keybind: "mod+k,mod+p",
|
||||
slash: "open",
|
||||
onSelect: openFile,
|
||||
}),
|
||||
tab &&
|
||||
fileCommand({
|
||||
id: "tab.close",
|
||||
title: language.t("command.tab.close"),
|
||||
keybind: "mod+w",
|
||||
onSelect: closeTab,
|
||||
}),
|
||||
].filter((v) => !!v)
|
||||
}
|
||||
|
||||
const contextCmds = () => [
|
||||
contextCommand({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||
import { ServerScope } from "./server-scope"
|
||||
|
||||
type PersistTestingType = typeof import("./persist").PersistTesting
|
||||
type PersistType = typeof import("./persist").Persist
|
||||
@@ -164,4 +165,29 @@ describe("persist localStorage resilience", () => {
|
||||
expect(storage.getItem(`${target.storage}:${target.key}`)).toBeNull()
|
||||
expect(storage.getItem(`${target.legacyStorageNames![0]}:${target.key}`)).toBeNull()
|
||||
})
|
||||
|
||||
test("server workspace target preserves local storage and isolates remote storage", () => {
|
||||
const local = Persist.serverWorkspace(ServerScope.local, "/home/luke/repo", "prompt")
|
||||
const windows = Persist.serverWorkspace("https://windows.example" as ServerScope, "/home/luke/repo", "prompt")
|
||||
const debian = Persist.serverWorkspace("https://debian.example" as ServerScope, "/home/luke/repo", "prompt")
|
||||
|
||||
expect(local).toEqual(Persist.workspace("/home/luke/repo", "prompt"))
|
||||
expect(windows.storage).not.toBe(local.storage)
|
||||
expect(debian.storage).not.toBe(local.storage)
|
||||
expect(debian.storage).not.toBe(windows.storage)
|
||||
expect(windows.legacyStorageNames).toBeUndefined()
|
||||
expect(debian.legacyStorageNames).toBeUndefined()
|
||||
})
|
||||
|
||||
test("server global target preserves local key and isolates remote keys", () => {
|
||||
expect(Persist.serverGlobal(ServerScope.local, "notification")).toEqual(Persist.global("notification"))
|
||||
expect(Persist.serverGlobal("https://debian.example" as ServerScope, "notification")).toEqual({
|
||||
storage: "opencode.global.dat",
|
||||
key: "https://debian.example\0notification",
|
||||
})
|
||||
})
|
||||
|
||||
test("server global target cannot collide when scope and key contain colons", () => {
|
||||
expect(Persist.serverGlobal("a:b" as ServerScope, "c")).not.toEqual(Persist.serverGlobal("a" as ServerScope, "b:c"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { createResource, type Accessor } from "solid-js"
|
||||
import type { SetStoreFunction, Store } from "solid-js/store"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { ScopedKey, ServerScope, type ServerScope as ServerScopeValue } from "@/utils/server-scope"
|
||||
|
||||
type InitType = Promise<string> | string | null
|
||||
type PersistedWithReady<T> = [
|
||||
@@ -357,6 +358,11 @@ function legacyWorkspaceStorage(dir: string) {
|
||||
return [...result]
|
||||
}
|
||||
|
||||
function serverWorkspaceTarget(scope: ServerScopeValue, dir: string, key: string, legacy?: string[]): PersistTarget {
|
||||
if (scope !== ServerScope.local) return { storage: workspaceStorage(ScopedKey.from(scope, pathKey(dir))), key }
|
||||
return { storage: workspaceStorage(pathKey(dir)), legacyStorageNames: legacyWorkspaceStorage(dir), key, legacy }
|
||||
}
|
||||
|
||||
function localStorageWithPrefix(prefix: string): SyncStorage {
|
||||
const base = `${prefix}:`
|
||||
const scope = `prefix:${prefix}`
|
||||
@@ -456,23 +462,30 @@ export const Persist = {
|
||||
global(key: string, legacy?: string[]): PersistTarget {
|
||||
return { storage: GLOBAL_STORAGE, key, legacy }
|
||||
},
|
||||
serverGlobal(scope: ServerScopeValue, key: string, legacy?: string[]): PersistTarget {
|
||||
if (scope === ServerScope.local) return Persist.global(key, legacy)
|
||||
return { storage: GLOBAL_STORAGE, key: ScopedKey.from(scope, key) }
|
||||
},
|
||||
workspace(dir: string, key: string, legacy?: string[]): PersistTarget {
|
||||
const storage = workspaceStorage(pathKey(dir))
|
||||
return { storage, legacyStorageNames: legacyWorkspaceStorage(dir), key: `workspace:${key}`, legacy }
|
||||
return serverWorkspaceTarget(ServerScope.local, dir, `workspace:${key}`, legacy)
|
||||
},
|
||||
serverWorkspace(scope: ServerScopeValue, dir: string, key: string, legacy?: string[]): PersistTarget {
|
||||
return serverWorkspaceTarget(scope, dir, `workspace:${key}`, legacy)
|
||||
},
|
||||
session(dir: string, session: string, key: string, legacy?: string[]): PersistTarget {
|
||||
const storage = workspaceStorage(pathKey(dir))
|
||||
return {
|
||||
storage,
|
||||
legacyStorageNames: legacyWorkspaceStorage(dir),
|
||||
key: `session:${session}:${key}`,
|
||||
legacy,
|
||||
}
|
||||
return serverWorkspaceTarget(ServerScope.local, dir, `session:${session}:${key}`, legacy)
|
||||
},
|
||||
serverSession(scope: ServerScopeValue, dir: string, session: string, key: string, legacy?: string[]): PersistTarget {
|
||||
return serverWorkspaceTarget(scope, dir, `session:${session}:${key}`, legacy)
|
||||
},
|
||||
scoped(dir: string, session: string | undefined, key: string, legacy?: string[]): PersistTarget {
|
||||
if (session) return Persist.session(dir, session, key, legacy)
|
||||
return Persist.workspace(dir, key, legacy)
|
||||
},
|
||||
serverScoped(scope: ServerScopeValue, dir: string, session: string | undefined, key: string, legacy?: string[]) {
|
||||
if (session) return Persist.serverSession(scope, dir, session, key, legacy)
|
||||
return Persist.serverWorkspace(scope, dir, key, legacy)
|
||||
},
|
||||
}
|
||||
|
||||
export function removePersisted(
|
||||
|
||||
@@ -25,6 +25,31 @@ describe("checkServerHealth", () => {
|
||||
expect(result).toEqual({ healthy: true, version: "1.2.3" })
|
||||
})
|
||||
|
||||
test("allows slow servers thirty seconds by default", async () => {
|
||||
const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
|
||||
let timeoutMs = 0
|
||||
Object.defineProperty(AbortSignal, "timeout", {
|
||||
configurable: true,
|
||||
value: (ms: number) => {
|
||||
timeoutMs = ms
|
||||
return new AbortController().signal
|
||||
},
|
||||
})
|
||||
|
||||
const fetch = (async () =>
|
||||
new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as unknown as typeof globalThis.fetch
|
||||
|
||||
await checkServerHealth(server, fetch).finally(() => {
|
||||
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
|
||||
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
|
||||
})
|
||||
|
||||
expect(timeoutMs).toBe(30_000)
|
||||
})
|
||||
|
||||
test("returns unhealthy when request fails", async () => {
|
||||
const fetch = (async () => {
|
||||
throw new Error("network")
|
||||
|
||||
@@ -13,7 +13,7 @@ interface CheckServerHealthOptions {
|
||||
retryDelayMs?: number
|
||||
}
|
||||
|
||||
const defaultTimeoutMs = 3000
|
||||
const defaultTimeoutMs = 30_000
|
||||
const defaultRetryCount = 2
|
||||
const defaultRetryDelayMs = 100
|
||||
const cacheMs = 750
|
||||
@@ -132,7 +132,10 @@ export const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabl
|
||||
const results: Record<string, ServerHealth> = {}
|
||||
await Promise.all(
|
||||
list.map(async (conn) => {
|
||||
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
||||
const key = ServerConnection.key(conn)
|
||||
const result = await checkServerHealth(conn.http)
|
||||
results[key] = result
|
||||
if (!dead) setStatus(key, result)
|
||||
}),
|
||||
)
|
||||
if (dead) return
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ScopedKey, ServerScope, SessionRouteKey, SessionStateKey, migrateLegacySessionStateKeys } from "./server-scope"
|
||||
|
||||
describe("ServerScope", () => {
|
||||
test("uses a stable local scope for the canonical sidecar", () => {
|
||||
expect(String(ServerScope.fromServerKey("sidecar" as Parameters<typeof ServerScope.fromServerKey>[0]))).toBe(
|
||||
"local",
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps configured loopback servers distinct from the canonical sidecar", () => {
|
||||
expect(
|
||||
String(ServerScope.fromServerKey("http://localhost:4096" as Parameters<typeof ServerScope.fromServerKey>[0])),
|
||||
).toBe("http://localhost:4096")
|
||||
})
|
||||
|
||||
test("uses a stable local scope for an explicit canonical web server", () => {
|
||||
const key = "http://localhost:4096" as Parameters<typeof ServerScope.fromServerKey>[0]
|
||||
expect(String(ServerScope.fromServerKey(key, key))).toBe("local")
|
||||
})
|
||||
})
|
||||
|
||||
describe("SessionStateKey", () => {
|
||||
test("combines local and remote scope with route identity", () => {
|
||||
const route = SessionRouteKey.fromRoute("cmVwbw", "session-1")
|
||||
expect(String(SessionStateKey.from(ServerScope.local, route))).toBe("local\0cmVwbw/session-1")
|
||||
expect(String(SessionStateKey.from("https://windows.example" as ServerScope, route))).toBe(
|
||||
"https://windows.example\0cmVwbw/session-1",
|
||||
)
|
||||
expect(SessionStateKey.from("https://debian.example" as ServerScope, route)).not.toBe(
|
||||
SessionStateKey.from("https://windows.example" as ServerScope, route),
|
||||
)
|
||||
})
|
||||
|
||||
test("extracts route keys from scoped and legacy state keys", () => {
|
||||
expect(String(SessionStateKey.route("cmVwbw/session-1"))).toBe("cmVwbw/session-1")
|
||||
expect(String(SessionStateKey.route("local\0cmVwbw/session-1"))).toBe("cmVwbw/session-1")
|
||||
expect(String(SessionStateKey.route("https://debian.example\0cmVwbw/session-1"))).toBe("cmVwbw/session-1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateLegacySessionStateKeys", () => {
|
||||
test("copies legacy route keys into local scope without overwriting scoped state", () => {
|
||||
expect(
|
||||
migrateLegacySessionStateKeys({
|
||||
"cmVwbw/session-1": { active: "legacy" },
|
||||
"local\0cmVwbw/session-1": { active: "scoped" },
|
||||
"https://debian.example\0cmVwbw/session-1": { active: "remote" },
|
||||
}),
|
||||
).toEqual({
|
||||
"local\0cmVwbw/session-1": { active: "scoped" },
|
||||
"https://debian.example\0cmVwbw/session-1": { active: "remote" },
|
||||
})
|
||||
})
|
||||
|
||||
test("rejects invalid identity fragments", () => {
|
||||
expect(() => ScopedKey.from(ServerScope.local, "bad\0directory")).toThrow(
|
||||
"Scoped key part cannot contain null bytes",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { ServerConnection } from "@/context/server"
|
||||
|
||||
export type ServerScope = string & { readonly __brand: "ServerScope" }
|
||||
export type SessionRouteKey = string & { readonly __brand: "SessionRouteKey" }
|
||||
export type SessionStateKey = string & { readonly __brand: "SessionStateKey" }
|
||||
export type ScopedKey = string & { readonly __brand: "ScopedKey" }
|
||||
|
||||
const separator = "\u0000"
|
||||
|
||||
function fragment(label: string, value: string) {
|
||||
if (value.includes(separator)) throw new Error(`${label} cannot contain null bytes`)
|
||||
return value
|
||||
}
|
||||
|
||||
function compose(scope: ServerScope, parts: string[]) {
|
||||
return [fragment("Server scope", scope), ...parts.map((part) => fragment("Scoped key part", part))].join(separator)
|
||||
}
|
||||
|
||||
export const ServerScope = {
|
||||
local: "local" as ServerScope,
|
||||
fromServerKey(key: ServerConnection.Key, canonicalLocalServer?: ServerConnection.Key) {
|
||||
return fragment(
|
||||
"Server scope",
|
||||
key === "sidecar" || key === canonicalLocalServer ? ServerScope.local : key,
|
||||
) as ServerScope
|
||||
},
|
||||
}
|
||||
|
||||
export const SessionRouteKey = {
|
||||
fromRoute(dir: string | undefined, sessionID?: string) {
|
||||
return fragment("Session route", `${dir ?? ""}${sessionID ? "/" + sessionID : ""}`) as SessionRouteKey
|
||||
},
|
||||
fromLegacy(key: string) {
|
||||
return fragment("Legacy session route", key) as SessionRouteKey
|
||||
},
|
||||
}
|
||||
|
||||
export const SessionStateKey = {
|
||||
from(scope: ServerScope, route: SessionRouteKey) {
|
||||
return compose(scope, [route]) as SessionStateKey
|
||||
},
|
||||
route(key: string) {
|
||||
const split = key.lastIndexOf(separator)
|
||||
return SessionRouteKey.fromLegacy(split === -1 ? key : key.slice(split + 1))
|
||||
},
|
||||
scope(key: string) {
|
||||
const split = key.indexOf(separator)
|
||||
if (split === -1) return ServerScope.local
|
||||
return fragment("Stored server scope", key.slice(0, split)) as ServerScope
|
||||
},
|
||||
}
|
||||
|
||||
export const ScopedKey = {
|
||||
from(scope: ServerScope, ...parts: string[]) {
|
||||
return compose(scope, parts) as ScopedKey
|
||||
},
|
||||
prefix(scope: ServerScope, ...parts: string[]) {
|
||||
return `${ScopedKey.from(scope, ...parts)}${separator}`
|
||||
},
|
||||
}
|
||||
|
||||
export function migrateLegacySessionStateKeys(value: unknown) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
||||
const entries = Object.entries(value)
|
||||
if (entries.every(([key]) => key.includes(separator))) return value
|
||||
const scoped = Object.fromEntries(entries.filter(([key]) => key.includes(separator)))
|
||||
for (const [key, item] of entries) {
|
||||
if (key.includes(separator)) continue
|
||||
const next = SessionStateKey.from(ServerScope.local, SessionRouteKey.fromLegacy(key))
|
||||
if (!(next in scoped)) scoped[next] = item
|
||||
}
|
||||
return scoped
|
||||
}
|
||||
@@ -1,34 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Worktree } from "./worktree"
|
||||
import { ServerScope } from "./server-scope"
|
||||
|
||||
const dir = (name: string) => `/tmp/opencode-worktree-${name}-${crypto.randomUUID()}`
|
||||
|
||||
describe("Worktree", () => {
|
||||
const scope = ServerScope.local
|
||||
test("normalizes trailing slashes", () => {
|
||||
const key = dir("normalize")
|
||||
Worktree.ready(`${key}/`)
|
||||
Worktree.ready(scope, `${key}/`)
|
||||
|
||||
expect(Worktree.get(key)).toEqual({ status: "ready" })
|
||||
expect(Worktree.get(scope, key)).toEqual({ status: "ready" })
|
||||
})
|
||||
|
||||
test("pending does not overwrite a terminal state", () => {
|
||||
const key = dir("pending")
|
||||
Worktree.failed(key, "boom")
|
||||
Worktree.pending(key)
|
||||
Worktree.failed(scope, key, "boom")
|
||||
Worktree.pending(scope, key)
|
||||
|
||||
expect(Worktree.get(key)).toEqual({ status: "failed", message: "boom" })
|
||||
expect(Worktree.get(scope, key)).toEqual({ status: "failed", message: "boom" })
|
||||
})
|
||||
|
||||
test("wait resolves shared pending waiter when ready", async () => {
|
||||
const key = dir("wait-ready")
|
||||
Worktree.pending(key)
|
||||
Worktree.pending(scope, key)
|
||||
|
||||
const a = Worktree.wait(key)
|
||||
const b = Worktree.wait(`${key}/`)
|
||||
const a = Worktree.wait(scope, key)
|
||||
const b = Worktree.wait(scope, `${key}/`)
|
||||
|
||||
expect(a).toBe(b)
|
||||
|
||||
Worktree.ready(key)
|
||||
Worktree.ready(scope, key)
|
||||
|
||||
expect(await a).toEqual({ status: "ready" })
|
||||
expect(await b).toEqual({ status: "ready" })
|
||||
@@ -36,11 +38,21 @@ describe("Worktree", () => {
|
||||
|
||||
test("wait resolves with failure message", async () => {
|
||||
const key = dir("wait-failed")
|
||||
const waiting = Worktree.wait(key)
|
||||
const waiting = Worktree.wait(scope, key)
|
||||
|
||||
Worktree.failed(key, "permission denied")
|
||||
Worktree.failed(scope, key, "permission denied")
|
||||
|
||||
expect(await waiting).toEqual({ status: "failed", message: "permission denied" })
|
||||
expect(await Worktree.wait(key)).toEqual({ status: "failed", message: "permission denied" })
|
||||
expect(await Worktree.wait(scope, key)).toEqual({ status: "failed", message: "permission denied" })
|
||||
})
|
||||
|
||||
test("isolates identical directories by server scope", () => {
|
||||
const key = dir("scope")
|
||||
const remote = "https://debian.example" as ServerScope
|
||||
Worktree.ready(scope, key)
|
||||
Worktree.failed(remote, key, "remote failed")
|
||||
|
||||
expect(Worktree.get(scope, key)).toEqual({ status: "ready" })
|
||||
expect(Worktree.get(remote, key)).toEqual({ status: "failed", message: "remote failed" })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||
|
||||
const normalize = (directory: string) => directory.replace(/[\\/]+$/, "")
|
||||
const key = (scope: ServerScope, directory: string) => ScopedKey.from(scope, normalize(directory))
|
||||
|
||||
type State =
|
||||
| {
|
||||
@@ -30,44 +33,44 @@ function deferred() {
|
||||
}
|
||||
|
||||
export const Worktree = {
|
||||
get(directory: string) {
|
||||
return state.get(normalize(directory))
|
||||
get(scope: ServerScope, directory: string) {
|
||||
return state.get(key(scope, directory))
|
||||
},
|
||||
pending(directory: string) {
|
||||
const key = normalize(directory)
|
||||
const current = state.get(key)
|
||||
pending(scope: ServerScope, directory: string) {
|
||||
const id = key(scope, directory)
|
||||
const current = state.get(id)
|
||||
if (current && current.status !== "pending") return
|
||||
state.set(key, { status: "pending" })
|
||||
state.set(id, { status: "pending" })
|
||||
},
|
||||
ready(directory: string) {
|
||||
const key = normalize(directory)
|
||||
ready(scope: ServerScope, directory: string) {
|
||||
const id = key(scope, directory)
|
||||
const next = { status: "ready" } as const
|
||||
state.set(key, next)
|
||||
const waiter = waiters.get(key)
|
||||
state.set(id, next)
|
||||
const waiter = waiters.get(id)
|
||||
if (!waiter) return
|
||||
waiters.delete(key)
|
||||
waiters.delete(id)
|
||||
waiter.resolve(next)
|
||||
},
|
||||
failed(directory: string, message: string) {
|
||||
const key = normalize(directory)
|
||||
failed(scope: ServerScope, directory: string, message: string) {
|
||||
const id = key(scope, directory)
|
||||
const next = { status: "failed", message } as const
|
||||
state.set(key, next)
|
||||
const waiter = waiters.get(key)
|
||||
state.set(id, next)
|
||||
const waiter = waiters.get(id)
|
||||
if (!waiter) return
|
||||
waiters.delete(key)
|
||||
waiters.delete(id)
|
||||
waiter.resolve(next)
|
||||
},
|
||||
wait(directory: string) {
|
||||
const key = normalize(directory)
|
||||
const current = state.get(key)
|
||||
wait(scope: ServerScope, directory: string) {
|
||||
const id = key(scope, directory)
|
||||
const current = state.get(id)
|
||||
if (current && current.status !== "pending") return Promise.resolve(current)
|
||||
|
||||
const existing = waiters.get(key)
|
||||
const existing = waiters.get(id)
|
||||
if (existing) return existing.promise
|
||||
|
||||
const waiter = deferred()
|
||||
|
||||
waiters.set(key, waiter)
|
||||
waiters.set(id, waiter)
|
||||
return waiter.promise
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const scriptDir = path.dirname(fs.realpathSync(__filename))
|
||||
const cached = path.join(scriptDir, ".lildax")
|
||||
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
||||
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
||||
const base = "@opencode-ai/lildax-" + platform + "-" + arch
|
||||
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
||||
const binary = platform === "windows" ? "lildax.exe" : "lildax"
|
||||
|
||||
function supportsAvx2() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -44,7 +44,7 @@ const targets = singleFlag
|
||||
: allTargets
|
||||
|
||||
for (const item of targets) {
|
||||
const name = [
|
||||
const target = [
|
||||
binary,
|
||||
item.os === "win32" ? "windows" : item.os,
|
||||
item.arch,
|
||||
@@ -53,6 +53,7 @@ for (const item of targets) {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("-")
|
||||
const name = target.replace(binary, "cli")
|
||||
console.log(`building ${name}`)
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
@@ -67,7 +68,7 @@ for (const item of targets) {
|
||||
autoloadDotenv: false,
|
||||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
target: name.replace(binary, "bun") as Bun.Build.CompileTarget,
|
||||
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
|
||||
outfile: `./dist/${name}/bin/${binary}`,
|
||||
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
|
||||
windows: {},
|
||||
@@ -93,6 +94,7 @@ for (const item of targets) {
|
||||
name: `@opencode-ai/${name}`,
|
||||
version: Script.version,
|
||||
license: "MIT",
|
||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||
os: [item.os],
|
||||
cpu: [item.arch],
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
bin: { lildax: "./bin/lildax" },
|
||||
version,
|
||||
license: pkg.license,
|
||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||
os: ["darwin", "linux", "win32"],
|
||||
cpu: ["arm64", "x64"],
|
||||
optionalDependencies: binaries,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { randomBytes } from "crypto"
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import path from "path"
|
||||
|
||||
export interface Interface {
|
||||
@@ -17,6 +18,18 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Daemon") {}
|
||||
|
||||
const Registration = Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
version: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
pid: Schema.Int.check(Schema.isGreaterThan(0)),
|
||||
})
|
||||
type Registration = typeof Registration.Type
|
||||
|
||||
function sameRegistration(left: Registration, right: Registration) {
|
||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
@@ -24,9 +37,7 @@ export const layer = Layer.effect(
|
||||
const directory = Global.Path.state
|
||||
const file = path.join(directory, "server.json")
|
||||
const passwordFile = path.join(directory, "password")
|
||||
const decodeRegistration = Schema.decodeUnknownEffect(
|
||||
Schema.fromJsonString(Schema.Struct({ url: Schema.String, pid: Schema.Number })),
|
||||
)
|
||||
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
||||
|
||||
const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
|
||||
const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
@@ -53,42 +64,15 @@ export const layer = Layer.effect(
|
||||
const healthy = Effect.fnUntraced(function* () {
|
||||
const info = yield* registration()
|
||||
const client = yield* createClient(info.url)
|
||||
const response = yield* Effect.tryPromise(() => client.v2.health.get())
|
||||
const response = yield* Effect.tryPromise(() => client.v2.health.get({ signal: AbortSignal.timeout(2_000) }))
|
||||
if (response.data?.healthy === true) return info
|
||||
return yield* Effect.fail(new Error("Registered server is not healthy"))
|
||||
})
|
||||
|
||||
const start = Effect.fn("cli.daemon.start")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
const found = Option.getOrUndefined(existing)
|
||||
if (found) return found.url
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||
Bun.spawn([process.execPath, ...(compiled ? [] : [Bun.main]), "serve", "--register"], {
|
||||
stdin: "ignore",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
}).unref()
|
||||
})
|
||||
|
||||
return yield* healthy().pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
Effect.map((info) => info.url),
|
||||
Effect.mapError(() => new Error("Failed to start server")),
|
||||
)
|
||||
})
|
||||
|
||||
const client = Effect.fn("cli.daemon.client")(function* () {
|
||||
return yield* createClient(yield* start())
|
||||
})
|
||||
|
||||
const status = Effect.fn("cli.daemon.status")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
const found = Option.getOrUndefined(existing)
|
||||
if (found) return found.url
|
||||
yield* fs.remove(file).pipe(Effect.ignore)
|
||||
return undefined
|
||||
const compatible = Effect.fnUntraced(function* () {
|
||||
const info = yield* healthy()
|
||||
if (info.version === InstallationVersion) return info
|
||||
return yield* Effect.fail(new Error("Registered server version does not match the client"))
|
||||
})
|
||||
|
||||
const signal = (pid: number, signal: NodeJS.Signals) =>
|
||||
@@ -102,36 +86,91 @@ export const layer = Layer.effect(
|
||||
return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
|
||||
})
|
||||
|
||||
const stopProcess = Effect.fnUntraced(function* (info: Registration) {
|
||||
const current = yield* healthy().pipe(Effect.option)
|
||||
if (Option.isNone(current) || !sameRegistration(current.value, info)) return
|
||||
|
||||
yield* signal(info.pid, "SIGTERM")
|
||||
const stopped = yield* awaitStopped(info.pid).pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
Effect.option,
|
||||
)
|
||||
if (Option.isSome(stopped)) return
|
||||
|
||||
const latest = yield* healthy().pipe(Effect.option)
|
||||
if (Option.isNone(latest) || !sameRegistration(latest.value, info)) return
|
||||
yield* signal(info.pid, "SIGKILL")
|
||||
yield* awaitStopped(info.pid).pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
)
|
||||
})
|
||||
|
||||
const start = Effect.fn("cli.daemon.start")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
const found = Option.getOrUndefined(existing)
|
||||
if (found?.version === InstallationVersion) return found.url
|
||||
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||
Bun.spawn([process.execPath, ...(compiled ? [] : [Bun.main]), "serve", "--register"], {
|
||||
stdin: "ignore",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
}).unref()
|
||||
})
|
||||
|
||||
return yield* compatible().pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
Effect.map((info) => info.url),
|
||||
Effect.mapError(() => new Error("Failed to start server")),
|
||||
)
|
||||
})
|
||||
|
||||
const client = Effect.fn("cli.daemon.client")(function* () {
|
||||
return yield* createClient(yield* start())
|
||||
})
|
||||
|
||||
const status = Effect.fn("cli.daemon.status")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
const found = Option.getOrUndefined(existing)
|
||||
if (found?.version === InstallationVersion) return found.url
|
||||
if (found) return undefined
|
||||
yield* fs.remove(file).pipe(Effect.ignore)
|
||||
return undefined
|
||||
})
|
||||
|
||||
const stop = Effect.fn("cli.daemon.stop")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
// A stale registration may point at a PID that has since been reused by
|
||||
// another process. Only signal the PID after authenticating the server.
|
||||
if (Option.isNone(existing)) return yield* fs.remove(file).pipe(Effect.ignore)
|
||||
const pid = existing.value.pid
|
||||
yield* signal(pid, "SIGTERM")
|
||||
const stopped = yield* awaitStopped(pid).pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
Effect.option,
|
||||
)
|
||||
if (Option.isNone(stopped)) {
|
||||
yield* signal(pid, "SIGKILL")
|
||||
yield* awaitStopped(pid).pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
)
|
||||
}
|
||||
yield* stopProcess(existing.value)
|
||||
yield* fs.remove(file).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
const register = Effect.fn("cli.daemon.register")(function* (address: HttpServer.Address) {
|
||||
const temp = file + ".tmp"
|
||||
const id = randomUUID()
|
||||
const temp = file + "." + id + ".tmp"
|
||||
yield* fs.makeDirectory(directory, { recursive: true })
|
||||
yield* fs.writeFileString(temp, JSON.stringify({ url: HttpServer.formatAddress(address), pid: process.pid }), {
|
||||
mode: 0o600,
|
||||
})
|
||||
yield* fs.writeFileString(
|
||||
temp,
|
||||
JSON.stringify({ id, version: InstallationVersion, url: HttpServer.formatAddress(address), pid: process.pid }),
|
||||
{ mode: 0o600 },
|
||||
)
|
||||
yield* fs.rename(temp, file)
|
||||
// The metadata file represents this live listener, not persistent config.
|
||||
// Scope shutdown removes it when the server exits normally.
|
||||
yield* Effect.addFinalizer(() => fs.remove(file).pipe(Effect.ignore))
|
||||
yield* registration().pipe(
|
||||
Effect.flatMap((info) => (info.id === id ? Effect.void : signal(process.pid, "SIGTERM"))),
|
||||
Effect.catch(() => signal(process.pid, "SIGTERM")),
|
||||
Effect.repeat(Schedule.spaced("10 seconds")),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
registration().pipe(
|
||||
Effect.flatMap((info) => (info.id === id ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return Service.of({ client, start, status, stop, password, register })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
DELETE FROM `session_input`;--> statement-breakpoint
|
||||
DELETE FROM `session_message`;--> statement-breakpoint
|
||||
DELETE FROM `event`;--> statement-breakpoint
|
||||
DELETE FROM `event_sequence`;--> statement-breakpoint
|
||||
UPDATE `session` SET `workspace_id` = NULL;--> statement-breakpoint
|
||||
DELETE FROM `workspace`;--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS `event_aggregate_seq_idx`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `event_aggregate_seq_idx` ON `event` (`aggregate_id`,`seq`);--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS `session_message_session_seq_idx`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_session_input` (
|
||||
`id` text PRIMARY KEY,
|
||||
`session_id` text NOT NULL,
|
||||
`prompt` text NOT NULL,
|
||||
`delivery` text NOT NULL,
|
||||
`admitted_seq` integer NOT NULL,
|
||||
`promoted_seq` integer,
|
||||
`time_created` integer NOT NULL,
|
||||
CONSTRAINT `fk_session_input_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `session_input`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_session_input` RENAME TO `session_input`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE INDEX `session_input_session_pending_delivery_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`delivery`,`admitted_seq`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `session_input_session_admitted_seq_idx` ON `session_input` (`session_id`,`admitted_seq`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `session_input_session_promoted_seq_idx` ON `session_input` (`session_id`,`promoted_seq`);
|
||||
+1898
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE `session_context_epoch` (
|
||||
`session_id` text PRIMARY KEY,
|
||||
`baseline` text NOT NULL,
|
||||
`snapshot` text NOT NULL,
|
||||
`baseline_seq` integer NOT NULL,
|
||||
`replacement_seq` integer,
|
||||
`revision` integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT `fk_session_context_epoch_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
+1980
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.13",
|
||||
"version": "1.16.0",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -17,6 +17,7 @@
|
||||
"opencode": "./bin/opencode"
|
||||
},
|
||||
"exports": {
|
||||
"./public": "./src/public/index.ts",
|
||||
"./session/runner": "./src/session/runner/index.ts",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
@@ -74,7 +75,7 @@
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@ai-sdk/xai": "3.0.82",
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
@@ -89,7 +90,7 @@
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"bun-pty": "0.4.8",
|
||||
"cross-spawn": "catalog:",
|
||||
|
||||
@@ -25,6 +25,9 @@ type Active = {
|
||||
pending: number
|
||||
next: number
|
||||
output?: { sequence: number; text: string }
|
||||
tail: Deferred.Deferred<void>
|
||||
promoted: Deferred.Deferred<Info>
|
||||
onPromote?: Effect.Effect<void>
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -38,15 +41,31 @@ type FinishResult = {
|
||||
scope?: Scope.Closeable
|
||||
}
|
||||
|
||||
type PromoteResult = {
|
||||
info?: Info
|
||||
promoted?: Deferred.Deferred<Info>
|
||||
onPromote?: Effect.Effect<void>
|
||||
}
|
||||
|
||||
type StartResult = { info: Info } | { info: Info; scope: Scope.Closeable; token: object }
|
||||
|
||||
type ExtendResult = { extended: false } | { extended: true; scope: Scope.Closeable; token: object; sequence: number }
|
||||
type ExtendResult =
|
||||
| { extended: false }
|
||||
| {
|
||||
extended: true
|
||||
previous: Deferred.Deferred<void>
|
||||
scope: Scope.Closeable
|
||||
tail: Deferred.Deferred<void>
|
||||
token: object
|
||||
sequence: number
|
||||
}
|
||||
|
||||
export type StartInput = {
|
||||
id?: string
|
||||
type: string
|
||||
title?: string
|
||||
metadata?: Record<string, unknown>
|
||||
onPromote?: Effect.Effect<void>
|
||||
run: Effect.Effect<string, unknown>
|
||||
}
|
||||
|
||||
@@ -71,6 +90,8 @@ export interface Interface {
|
||||
readonly start: (input: StartInput) => Effect.Effect<Info>
|
||||
readonly extend: (input: ExtendInput) => Effect.Effect<boolean>
|
||||
readonly wait: (input: WaitInput) => Effect.Effect<WaitResult>
|
||||
readonly waitForPromotion: (id: string) => Effect.Effect<Info>
|
||||
readonly promote: (id: string) => Effect.Effect<Info | undefined>
|
||||
readonly cancel: (id: string) => Effect.Effect<Info | undefined>
|
||||
}
|
||||
|
||||
@@ -128,6 +149,7 @@ export const make = Effect.gen(function* () {
|
||||
: "error"
|
||||
const next = {
|
||||
...job,
|
||||
onPromote: undefined,
|
||||
pending: 0,
|
||||
output,
|
||||
info: {
|
||||
@@ -182,6 +204,8 @@ export const make = Effect.gen(function* () {
|
||||
const id = input.id ?? Identifier.ascending("job")
|
||||
const started_at = yield* Clock.currentTimeMillis
|
||||
const done = yield* Deferred.make<Info>()
|
||||
const promoted = yield* Deferred.make<Info>()
|
||||
const tail = yield* Deferred.make<void>()
|
||||
const result = yield* SynchronizedRef.modifyEffect(
|
||||
state.jobs,
|
||||
Effect.fnUntraced(function* (jobs) {
|
||||
@@ -205,6 +229,9 @@ export const make = Effect.gen(function* () {
|
||||
token,
|
||||
pending: 1,
|
||||
next: 1,
|
||||
tail,
|
||||
promoted,
|
||||
onPromote: input.onPromote,
|
||||
}
|
||||
return [{ info: snapshot(job), scope, token }, new Map(jobs).set(id, job)] as readonly [
|
||||
StartResult,
|
||||
@@ -212,7 +239,14 @@ export const make = Effect.gen(function* () {
|
||||
]
|
||||
}),
|
||||
)
|
||||
if ("scope" in result) yield* fork(result.scope, id, result.token, 0, restore(input.run))
|
||||
if ("scope" in result)
|
||||
yield* fork(
|
||||
result.scope,
|
||||
id,
|
||||
result.token,
|
||||
0,
|
||||
restore(input.run).pipe(Effect.ensuring(Deferred.succeed(tail, undefined))),
|
||||
)
|
||||
return result.info
|
||||
}),
|
||||
)
|
||||
@@ -221,23 +255,34 @@ export const make = Effect.gen(function* () {
|
||||
const extend: Interface["extend"] = Effect.fn("BackgroundJob.extend")(function* (input) {
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const tail = yield* Deferred.make<void>()
|
||||
const result = yield* SynchronizedRef.modify(
|
||||
state.jobs,
|
||||
(jobs): readonly [ExtendResult, Map<string, Active>] => {
|
||||
const job = jobs.get(input.id)
|
||||
if (!job || job.info.status !== "running") return [{ extended: false }, jobs]
|
||||
return [
|
||||
{ extended: true, scope: job.scope, token: job.token, sequence: job.next },
|
||||
{ extended: true, previous: job.tail, scope: job.scope, tail, token: job.token, sequence: job.next },
|
||||
new Map(jobs).set(input.id, {
|
||||
...job,
|
||||
pending: job.pending + 1,
|
||||
next: job.next + 1,
|
||||
tail,
|
||||
}),
|
||||
]
|
||||
},
|
||||
)
|
||||
if (!result.extended) return false
|
||||
yield* fork(result.scope, input.id, result.token, result.sequence, restore(input.run))
|
||||
yield* fork(
|
||||
result.scope,
|
||||
input.id,
|
||||
result.token,
|
||||
result.sequence,
|
||||
Deferred.await(result.previous).pipe(
|
||||
Effect.andThen(restore(input.run)),
|
||||
Effect.ensuring(Deferred.succeed(result.tail, undefined)),
|
||||
),
|
||||
)
|
||||
return true
|
||||
}),
|
||||
)
|
||||
@@ -254,6 +299,40 @@ export const make = Effect.gen(function* () {
|
||||
return { info: snapshot(job), timedOut: true }
|
||||
})
|
||||
|
||||
const waitForPromotion: Interface["waitForPromotion"] = Effect.fn("BackgroundJob.waitForPromotion")(function* (id) {
|
||||
const job = (yield* SynchronizedRef.get(state.jobs)).get(id)
|
||||
if (!job || job.info.status !== "running") return yield* Effect.never
|
||||
if (job.info.metadata?.background === true) return snapshot(job)
|
||||
return yield* Deferred.await(job.promoted)
|
||||
})
|
||||
|
||||
const promote: Interface["promote"] = Effect.fn("BackgroundJob.promote")(function* (id) {
|
||||
const result = yield* SynchronizedRef.modifyEffect(
|
||||
state.jobs,
|
||||
Effect.fnUntraced(function* (jobs) {
|
||||
const job = jobs.get(id)
|
||||
if (!job || job.info.status !== "running") return [{}, jobs] as readonly [PromoteResult, Map<string, Active>]
|
||||
if (job.info.metadata?.background === true)
|
||||
return [{ info: snapshot(job) }, jobs] as readonly [PromoteResult, Map<string, Active>]
|
||||
const next = {
|
||||
...job,
|
||||
onPromote: undefined,
|
||||
info: {
|
||||
...job.info,
|
||||
metadata: { ...job.info.metadata, background: true },
|
||||
},
|
||||
}
|
||||
return [
|
||||
{ info: snapshot(next), onPromote: job.onPromote, promoted: job.promoted },
|
||||
new Map(jobs).set(id, next),
|
||||
] as readonly [PromoteResult, Map<string, Active>]
|
||||
}),
|
||||
)
|
||||
if (result.info && result.promoted) yield* Deferred.succeed(result.promoted, result.info).pipe(Effect.ignore)
|
||||
if (result.onPromote) yield* result.onPromote.pipe(Effect.ignore)
|
||||
return result.info
|
||||
})
|
||||
|
||||
const cancel: Interface["cancel"] = Effect.fn("BackgroundJob.cancel")(function* (id) {
|
||||
const completed_at = yield* Clock.currentTimeMillis
|
||||
const result = yield* SynchronizedRef.modify(state.jobs, (jobs): readonly [FinishResult, Map<string, Active>] => {
|
||||
@@ -262,6 +341,7 @@ export const make = Effect.gen(function* () {
|
||||
if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs]
|
||||
const next = {
|
||||
...job,
|
||||
onPromote: undefined,
|
||||
pending: 0,
|
||||
info: {
|
||||
...job.info,
|
||||
@@ -276,7 +356,7 @@ export const make = Effect.gen(function* () {
|
||||
return result.info
|
||||
})
|
||||
|
||||
return Service.of({ list, get, start, extend, wait, cancel })
|
||||
return Service.of({ list, get, start, extend, wait, waitForPromotion, promote, cancel })
|
||||
})
|
||||
|
||||
export const layer = Layer.effect(Service, make)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
export * as MoveSession from "./move-session"
|
||||
|
||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { EventV2 } from "../event"
|
||||
import { Git } from "../git"
|
||||
import { Location } from "../location"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { SessionV2 } from "../session"
|
||||
import { SessionEvent } from "../session/event"
|
||||
import { SessionSchema } from "../session/schema"
|
||||
import { AbsolutePath, RelativePath } from "../schema"
|
||||
import path from "path"
|
||||
|
||||
export const Destination = Schema.Struct({
|
||||
directory: AbsolutePath,
|
||||
}).annotate({ identifier: "MoveSession.Destination" })
|
||||
export type Destination = typeof Destination.Type
|
||||
|
||||
export const Input = Schema.Struct({
|
||||
sessionID: SessionSchema.ID,
|
||||
destination: Destination,
|
||||
moveChanges: Schema.optional(Schema.Boolean),
|
||||
}).annotate({ identifier: "MoveSession.Input" })
|
||||
export type Input = typeof Input.Type
|
||||
|
||||
export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<DestinationProjectMismatchError>()(
|
||||
"MoveSession.DestinationProjectMismatchError",
|
||||
{
|
||||
expected: ProjectV2.ID,
|
||||
actual: ProjectV2.ID,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class ApplyChangesError extends Schema.TaggedErrorClass<ApplyChangesError>()("MoveSession.ApplyChangesError", {
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class CaptureChangesError extends Schema.TaggedErrorClass<CaptureChangesError>()(
|
||||
"MoveSession.CaptureChangesError",
|
||||
{
|
||||
message: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSourceChangesError>()(
|
||||
"MoveSession.ResetSourceChangesError",
|
||||
{
|
||||
directory: AbsolutePath,
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
},
|
||||
) {}
|
||||
|
||||
export type Error =
|
||||
| SessionV2.NotFoundError
|
||||
| DestinationProjectMismatchError
|
||||
| CaptureChangesError
|
||||
| ApplyChangesError
|
||||
| ResetSourceChangesError
|
||||
|
||||
export interface Interface {
|
||||
readonly moveSession: (input: Input) => Effect.Effect<void, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ControlPlaneMoveSession") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const git = yield* Git.Service
|
||||
const events = yield* EventV2.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
const session = yield* SessionV2.Service
|
||||
|
||||
const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) {
|
||||
const current = yield* session.get(input.sessionID)
|
||||
const directory = AbsolutePath.make(input.destination.directory)
|
||||
if (current.location.directory === directory) return
|
||||
|
||||
const source = yield* project.resolve(current.location.directory)
|
||||
const destination = yield* project.resolve(directory)
|
||||
if (current.projectID !== destination.id) {
|
||||
return yield* new DestinationProjectMismatchError({ expected: current.projectID, actual: destination.id })
|
||||
}
|
||||
|
||||
const patch =
|
||||
input.moveChanges && source.directory !== destination.directory
|
||||
? yield* git
|
||||
.patch(current.location.directory)
|
||||
.pipe(Effect.mapError((error) => new CaptureChangesError({ message: error.message })))
|
||||
: ""
|
||||
if (patch) {
|
||||
yield* git
|
||||
.applyPatch({ directory, patch })
|
||||
.pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message })))
|
||||
}
|
||||
|
||||
yield* events.publish(SessionEvent.Moved, {
|
||||
sessionID: input.sessionID,
|
||||
location: Location.Ref.make({ directory }),
|
||||
subdirectory: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
|
||||
timestamp: yield* DateTime.now,
|
||||
})
|
||||
|
||||
if (patch) {
|
||||
yield* git.softResetChanges(current.location.directory).pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new ResetSourceChangesError({
|
||||
directory: current.location.directory,
|
||||
message: error.message,
|
||||
cause: error.cause,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return Service.of({ moveSession })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(ProjectV2.defaultLayer),
|
||||
Layer.provide(SessionV2.defaultLayer),
|
||||
)
|
||||
+2
@@ -31,5 +31,7 @@ export const migrations = (
|
||||
import("./migration/20260603040000_session_message_projection_order"),
|
||||
import("./migration/20260603141458_session_input_inbox"),
|
||||
import("./migration/20260603160727_jittery_ezekiel_stane"),
|
||||
import("./migration/20260604172448_event_sourced_session_input"),
|
||||
import("./migration/20260605003541_add_session_context_snapshot"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260604172448_event_sourced_session_input",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||
yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL;`)
|
||||
yield* tx.run(`DELETE FROM \`workspace\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`event_aggregate_seq_idx\`;`)
|
||||
yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_seq_idx\`;`)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
|
||||
)
|
||||
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`__new_session_input\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`prompt\` text NOT NULL,
|
||||
\`delivery\` text NOT NULL,
|
||||
\`admitted_seq\` integer NOT NULL,
|
||||
\`promoted_seq\` integer,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`DROP TABLE \`session_input\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`__new_session_input\` RENAME TO \`session_input\`;`)
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`,
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260605003541_add_session_context_snapshot",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_context_epoch\` (
|
||||
\`session_id\` text PRIMARY KEY,
|
||||
\`baseline\` text NOT NULL,
|
||||
\`snapshot\` text NOT NULL,
|
||||
\`baseline_seq\` integer NOT NULL,
|
||||
\`replacement_seq\` integer,
|
||||
\`revision\` integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
+115
-41
@@ -1,14 +1,15 @@
|
||||
export * as EventV2 from "./event"
|
||||
|
||||
import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
|
||||
import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
|
||||
import { and, asc, eq, gt } from "drizzle-orm"
|
||||
import { Database } from "./database/database"
|
||||
import { EventSequenceTable, EventTable } from "./event/sql"
|
||||
import { Location } from "./location"
|
||||
import { externalID, type ExternalID, NonNegativeInt, withStatics } from "./schema"
|
||||
import { Identifier } from "./util/identifier"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
|
||||
Schema.brand("Event.ID"),
|
||||
withStatics((schema) => ({
|
||||
create: () => schema.make("evt_" + Identifier.ascending()),
|
||||
@@ -44,6 +45,8 @@ export type Payload<D extends Definition = Definition> = {
|
||||
readonly version?: number
|
||||
readonly location?: Location.Ref
|
||||
readonly metadata?: Record<string, unknown>
|
||||
/** Internal replay marker for projectors that own non-replicated operational state. */
|
||||
readonly replay?: boolean
|
||||
}
|
||||
|
||||
export type Projector<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
||||
@@ -136,6 +139,8 @@ export interface PublishOptions {
|
||||
readonly id?: ID
|
||||
readonly metadata?: Record<string, unknown>
|
||||
readonly location?: Location.Ref
|
||||
/** Local operational projection committed atomically with a new synchronized event. Not replayed or serialized. */
|
||||
readonly commit?: (seq: number) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
@@ -214,6 +219,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
readonly ownerID?: string
|
||||
readonly strictOwner?: boolean
|
||||
},
|
||||
commit?: (seq: number) => Effect.Effect<void>,
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const definition = registry.get(event.type)
|
||||
@@ -258,16 +264,47 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
const latest = row?.seq ?? -1
|
||||
if (input && input.seq <= latest) return
|
||||
if (input && row?.ownerID && row.ownerID !== input.ownerID) {
|
||||
if (input.strictOwner) {
|
||||
yield* Effect.die(
|
||||
new InvalidSyncEventError({
|
||||
type: event.type,
|
||||
message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`,
|
||||
}),
|
||||
)
|
||||
const encoded = syncRegistry
|
||||
.get(versionedType(definition.type, sync.version))!
|
||||
.encode(event.data) as Record<string, unknown>
|
||||
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
|
||||
yield* Effect.die(
|
||||
new InvalidSyncEventError({
|
||||
type: event.type,
|
||||
message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (input && input.seq <= latest) {
|
||||
const stored = yield* db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(and(eq(EventTable.aggregate_id, aggregateID), eq(EventTable.seq, input.seq)))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (
|
||||
stored?.id === event.id &&
|
||||
stored.type === versionedType(definition.type, sync.version) &&
|
||||
isDeepStrictEqual(stored.data, encoded)
|
||||
) {
|
||||
if (input.ownerID && row?.ownerID == null) {
|
||||
yield* db
|
||||
.update(EventSequenceTable)
|
||||
.set({ owner_id: input.ownerID })
|
||||
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
}
|
||||
return
|
||||
}
|
||||
yield* Effect.die(
|
||||
new InvalidSyncEventError({
|
||||
type: event.type,
|
||||
message: `Replay diverged at aggregate ${aggregateID} sequence ${input.seq}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (input && row?.ownerID && row.ownerID !== input.ownerID) {
|
||||
return
|
||||
}
|
||||
const seq = input?.seq ?? latest + 1
|
||||
@@ -279,15 +316,26 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
}),
|
||||
)
|
||||
}
|
||||
const stored = yield* db
|
||||
.select({ aggregateID: EventTable.aggregate_id, seq: EventTable.seq })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.id, event.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (stored)
|
||||
yield* Effect.die(
|
||||
new InvalidSyncEventError({
|
||||
type: event.type,
|
||||
message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
|
||||
}),
|
||||
)
|
||||
for (const guard of commitGuards) {
|
||||
yield* guard(event)
|
||||
}
|
||||
for (const projector of list) {
|
||||
yield* projector({ ...event, seq } as Payload)
|
||||
}
|
||||
const encoded = syncRegistry
|
||||
.get(versionedType(definition.type, sync.version))!
|
||||
.encode(event.data)
|
||||
if (commit) yield* commit(seq)
|
||||
yield* db
|
||||
.insert(EventSequenceTable)
|
||||
.values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }])
|
||||
@@ -308,7 +356,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
aggregate_id: aggregateID,
|
||||
seq,
|
||||
type: versionedType(definition.type, sync.version),
|
||||
data: encoded as Record<string, unknown>,
|
||||
data: encoded,
|
||||
},
|
||||
])
|
||||
.run()
|
||||
@@ -333,26 +381,54 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
})
|
||||
}
|
||||
|
||||
function publishEvent<D extends Definition>(event: Payload<D>) {
|
||||
function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) {
|
||||
return Effect.gen(function* () {
|
||||
const durable = registry.get(event.type)?.sync !== undefined
|
||||
if (!durable && commit)
|
||||
return yield* Effect.die(
|
||||
new InvalidSyncEventError({
|
||||
type: event.type,
|
||||
message: "Local commit hooks require a synchronized event",
|
||||
}),
|
||||
)
|
||||
if (durable) {
|
||||
for (const sync of syncHandlers) {
|
||||
yield* sync(event as Payload)
|
||||
const committed = yield* commitSyncEvent(event as Payload, undefined, commit)
|
||||
if (committed) {
|
||||
event = { ...event, seq: committed.seq }
|
||||
yield* Effect.forEach(syncHandlers, (sync) => observe(event as Payload, "sync", sync), { discard: true })
|
||||
yield* notify(event as Payload, true)
|
||||
return event
|
||||
}
|
||||
const committed = yield* commitSyncEvent(event as Payload)
|
||||
if (committed) event = { ...event, seq: committed.seq }
|
||||
}
|
||||
for (const listener of listeners) {
|
||||
yield* listener(event as Payload)
|
||||
}
|
||||
const pubsub = typed.get(event.type)
|
||||
if (pubsub) yield* PubSub.publish(pubsub, event as Payload)
|
||||
yield* PubSub.publish(all, event as Payload)
|
||||
yield* notify(event as Payload, false)
|
||||
return event
|
||||
})
|
||||
}
|
||||
|
||||
const observe = (event: Payload, kind: "sync" | "listener", observer: (event: Payload) => Effect.Effect<void>) =>
|
||||
Effect.suspend(() => observer(event)).pipe(
|
||||
Effect.catchCauseIf(
|
||||
(cause) => !Cause.hasInterrupts(cause),
|
||||
(cause) =>
|
||||
Effect.logError("Event observer failed").pipe(
|
||||
Effect.annotateLogs({ eventID: event.id, eventType: event.type, kind, cause }),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
function notify(event: Payload, isolateListeners: boolean) {
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.forEach(
|
||||
listeners,
|
||||
(listener) => (isolateListeners ? observe(event, "listener", listener) : listener(event)),
|
||||
{ discard: true },
|
||||
)
|
||||
const pubsub = typed.get(event.type)
|
||||
if (pubsub) yield* PubSub.publish(pubsub, event)
|
||||
yield* PubSub.publish(all, event)
|
||||
})
|
||||
}
|
||||
|
||||
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
|
||||
return Effect.gen(function* () {
|
||||
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
|
||||
@@ -361,14 +437,17 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
(serviceLocation
|
||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||
: undefined)
|
||||
return yield* publishEvent({
|
||||
id: options?.id ?? ID.create(),
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(definition.sync === undefined ? {} : { version: definition.sync.version }),
|
||||
...(location ? { location } : {}),
|
||||
data,
|
||||
} as Payload<D>)
|
||||
return yield* publishEvent(
|
||||
{
|
||||
id: options?.id ?? ID.create(),
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(definition.sync === undefined ? {} : { version: definition.sync.version }),
|
||||
...(location ? { location } : {}),
|
||||
data,
|
||||
} as Payload<D>,
|
||||
options?.commit,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -388,6 +467,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
type: definition.type,
|
||||
version: definition.sync.version,
|
||||
data: definition.decode(event.data),
|
||||
replay: true,
|
||||
} as Payload
|
||||
const committed = yield* commitSyncEvent(payload, {
|
||||
seq: event.seq,
|
||||
@@ -396,13 +476,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
strictOwner: options?.strictOwner,
|
||||
})
|
||||
if (committed && options?.publish) {
|
||||
const published = { ...payload, seq: committed.seq }
|
||||
for (const listener of listeners) {
|
||||
yield* listener(published)
|
||||
}
|
||||
const pubsub = typed.get(payload.type)
|
||||
if (pubsub) yield* PubSub.publish(pubsub, published)
|
||||
yield* PubSub.publish(all, published)
|
||||
yield* notify({ ...payload, seq: committed.seq }, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"
|
||||
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||
import type { EventV2 } from "../event"
|
||||
|
||||
export const EventSequenceTable = sqliteTable("event_sequence", {
|
||||
@@ -19,7 +19,7 @@ export const EventTable = sqliteTable(
|
||||
data: text({ mode: "json" }).$type<Record<string, unknown>>().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("event_aggregate_seq_idx").on(table.aggregate_id, table.seq),
|
||||
uniqueIndex("event_aggregate_seq_idx").on(table.aggregate_id, table.seq),
|
||||
index("event_aggregate_type_seq_idx").on(table.aggregate_id, table.type, table.seq),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -84,7 +84,10 @@ export namespace FSUtil {
|
||||
|
||||
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
|
||||
const text = yield* fs.readFileString(path)
|
||||
return JSON.parse(text)
|
||||
return yield* Effect.try({
|
||||
try: () => JSON.parse(text),
|
||||
catch: (cause) => new FileSystemError({ method: "readJson", cause }),
|
||||
})
|
||||
})
|
||||
|
||||
const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user