Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5f5eec7db | ||
|
|
a70132a7f3 | ||
|
|
59a5eba370 |
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -286,8 +286,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.70",
|
"@opentui/core": "0.1.69",
|
||||||
"@opentui/solid": "0.1.70",
|
"@opentui/solid": "0.1.69",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -370,7 +370,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.88.1",
|
"@hey-api/openapi-ts": "0.88.1",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -433,7 +433,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -444,7 +444,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1201,21 +1201,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.70", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.70", "@opentui/core-darwin-x64": "0.1.70", "@opentui/core-linux-arm64": "0.1.70", "@opentui/core-linux-x64": "0.1.70", "@opentui/core-win32-arm64": "0.1.70", "@opentui/core-win32-x64": "0.1.70", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-6cPAlbCnaiUUtQtvZNpkr0Xv8AdVAgJuy2VAwIsDN1pIv0zMpa0ZG+mr7afCGygw1eeDRveefrjfgFAB1r0SVw=="],
|
"@opentui/core": ["@opentui/core@0.1.69", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.69", "@opentui/core-darwin-x64": "0.1.69", "@opentui/core-linux-arm64": "0.1.69", "@opentui/core-linux-x64": "0.1.69", "@opentui/core-win32-arm64": "0.1.69", "@opentui/core-win32-x64": "0.1.69", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-BcEFnAuMq4vgfb+zxOP/l+NO1AS3fVHkYjn+E8Wpmaxr0AzWNTi2NPAMtQf+Wqufxo0NYh0gY4c9B6n8OxTjGw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.70", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rM8EnvW1tOAXWnp2Iy2M82I+ViSmRwUagx3v1/ni6N8GCcw/3mE0C6eB3sVlYNXVMwBEgiKpWFn85RCe4+qXQw=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.69", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d9RPAh84O2XIyMw+7+X0fEyi+4KH5sPk9AxLze8GHRBGOzkRunqagFCLBrN5VFs2e2nbhIYtjMszo7gcpWyh7g=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.70", "", { "os": "darwin", "cpu": "x64" }, "sha512-XdBgW+em8J+YGSUpaKF8/NxPjikJygK3dIkeMAw5xQ2lt7jXKxeM5MMmN/V4MfK3pLMtO56rLJlXaLH/h50uQA=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.69", "", { "os": "darwin", "cpu": "x64" }, "sha512-41K9zkL2IG0ahL+8Gd+e9ulMrnJF6lArPzG7grjWzo+FWEZwvw0WLCO1/Gn5K85G8Yx7gQXkZOUaw1BmHjxoRw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.70", "", { "os": "linux", "cpu": "arm64" }, "sha512-oSVWNMSOx0Na0M0LCqtWCxeh4SuLSK5lg8ZwVzsEoimIAxh0snp9nRUo/Qi8yD9BP0DSDmXuM/B3ONtzFaf0dw=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.69", "", { "os": "linux", "cpu": "arm64" }, "sha512-IcUjwjuIpX3BBG1a9kjMqWrHYCFHAVfjh5nIRozWZZoqaczLzJb3nJeF2eg8aDeIoGhXvERWB1r1gmqPW8u3vQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.70", "", { "os": "linux", "cpu": "x64" }, "sha512-WUrhukefMghcZ7sAjkxEy50vA6ii0X21xh7m8c4omXyYYfQXyDs25pNExB8cwoCrZEaC8RTlF4lRSNPIXsZKhA=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.69", "", { "os": "linux", "cpu": "x64" }, "sha512-5S9vqEIq7q+MEdp4cT0HLegBWu0pWLcletHZL80bsLbJt9OT8en3sQmL5bvas9sIuyeBFru9bfCmrQ/gnVTTiA=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.70", "", { "os": "win32", "cpu": "arm64" }, "sha512-p1K2VJXGmZqSV7mR61v7KJpT1Zth7DS99wEtaqqfK68OWt33K2XxLmGO0KD142R2JLfXu32NnRmBHxmVx8IjBA=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.69", "", { "os": "win32", "cpu": "arm64" }, "sha512-eSKcGwbcnJJPtrTFJI7STZ7inSYeedHS0swwjZhh9SADAruEz08intamunOslffv5+mnlvRp7UBGK35cMjbv/w=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.70", "", { "os": "win32", "cpu": "x64" }, "sha512-G6b8te1twMeDhjg1oZa0IcUjhOJZFCSdlQt+q5gu5vVtjCrIwAn9o7m5EwNMPakc31pDWUZ7v0ktgv0Xw1AQVA=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.69", "", { "os": "win32", "cpu": "x64" }, "sha512-OjG/0jqYXURqbbUwNgSPrBA6yuKF3OOFh8JSG7VvzoYHJFJRmwVWY0fztWv/hgGHe354ti37c7JDJBQ44HOCdA=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.70", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.70", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-8Cw/w4Of2OJhsFhcp/Wdj8cJRVaGvVsIiUoYiFtyToM01J4en0bg/vnbeZteyuZWeEtA4iz1/rSEQf7Dp+2FIQ=="],
|
"@opentui/solid": ["@opentui/solid@0.1.69", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.69", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-ls589N8P9gvcNW8uF+Il4xisF5Uouk0RRmSaLdzmItNJSW5J9Y0nPtMELta6hBp0yIRAurWUO1wtkKXVF+eaxg=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": "sha256-KjBAaI9Kv6huOmPvUbtyYsMhbScI91w1lOZyXpIWqI0="
|
"nodeModules": "sha256-rNGq0yjL5ZHYVg+zyV4nFPug4gqhKhyOnfebaufyd34="
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+12
-10
@@ -108,16 +108,18 @@ export function AppInterface() {
|
|||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
<Route path="/" component={() => <Navigate href="session" />} />
|
||||||
<Route
|
<Route
|
||||||
path="/session/:id?"
|
path="/session/:id?"
|
||||||
component={() => (
|
component={(p) => (
|
||||||
<TerminalProvider>
|
<Show when={p.params.id ?? "new"} keyed>
|
||||||
<FileProvider>
|
<TerminalProvider>
|
||||||
<PromptProvider>
|
<FileProvider>
|
||||||
<Suspense fallback={<Loading />}>
|
<PromptProvider>
|
||||||
<Session />
|
<Suspense fallback={<Loading />}>
|
||||||
</Suspense>
|
<Session />
|
||||||
</PromptProvider>
|
</Suspense>
|
||||||
</FileProvider>
|
</PromptProvider>
|
||||||
</TerminalProvider>
|
</FileProvider>
|
||||||
|
</TerminalProvider>
|
||||||
|
</Show>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import { createMemo, createSignal, For, Show } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
|
|
||||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
|
|
||||||
|
function getFilename(input: string) {
|
||||||
|
const parts = input.split("/")
|
||||||
|
return parts[parts.length - 1] || input
|
||||||
|
}
|
||||||
|
|
||||||
export function DialogEditProject(props: { project: LocalProject }) {
|
export function DialogEditProject(props: { project: LocalProject }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import type { FileContent } from "@opencode-ai/sdk/v2"
|
import type { FileContent } from "@opencode-ai/sdk/v2"
|
||||||
@@ -82,106 +82,8 @@ function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_KEY = "__workspace__"
|
|
||||||
const MAX_FILE_VIEW_SESSIONS = 20
|
|
||||||
const MAX_VIEW_FILES = 500
|
|
||||||
|
|
||||||
type ViewSession = ReturnType<typeof createViewSession>
|
|
||||||
|
|
||||||
type ViewCacheEntry = {
|
|
||||||
value: ViewSession
|
|
||||||
dispose: VoidFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
function createViewSession(dir: string, id: string | undefined) {
|
|
||||||
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
|
|
||||||
|
|
||||||
const [view, setView, _, ready] = persisted(
|
|
||||||
Persist.scoped(dir, id, "file-view", [legacyViewKey]),
|
|
||||||
createStore<{
|
|
||||||
file: Record<string, FileViewState>
|
|
||||||
}>({
|
|
||||||
file: {},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const meta = { pruned: false }
|
|
||||||
|
|
||||||
const pruneView = (keep?: string) => {
|
|
||||||
const keys = Object.keys(view.file)
|
|
||||||
if (keys.length <= MAX_VIEW_FILES) return
|
|
||||||
|
|
||||||
const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
|
|
||||||
if (drop.length === 0) return
|
|
||||||
|
|
||||||
setView(
|
|
||||||
produce((draft) => {
|
|
||||||
for (const key of drop) {
|
|
||||||
delete draft.file[key]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!ready()) return
|
|
||||||
if (meta.pruned) return
|
|
||||||
meta.pruned = true
|
|
||||||
pruneView()
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrollTop = (path: string) => view.file[path]?.scrollTop
|
|
||||||
const scrollLeft = (path: string) => view.file[path]?.scrollLeft
|
|
||||||
const selectedLines = (path: string) => view.file[path]?.selectedLines
|
|
||||||
|
|
||||||
const setScrollTop = (path: string, top: number) => {
|
|
||||||
setView("file", path, (current) => {
|
|
||||||
if (current?.scrollTop === top) return current
|
|
||||||
return {
|
|
||||||
...(current ?? {}),
|
|
||||||
scrollTop: top,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
pruneView(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setScrollLeft = (path: string, left: number) => {
|
|
||||||
setView("file", path, (current) => {
|
|
||||||
if (current?.scrollLeft === left) return current
|
|
||||||
return {
|
|
||||||
...(current ?? {}),
|
|
||||||
scrollLeft: left,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
pruneView(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSelectedLines = (path: string, range: SelectedLineRange | null) => {
|
|
||||||
const next = range ? normalizeSelectedLines(range) : null
|
|
||||||
setView("file", path, (current) => {
|
|
||||||
if (current?.selectedLines === next) return current
|
|
||||||
return {
|
|
||||||
...(current ?? {}),
|
|
||||||
selectedLines: next,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
pruneView(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
ready,
|
|
||||||
scrollTop,
|
|
||||||
scrollLeft,
|
|
||||||
selectedLines,
|
|
||||||
setScrollTop,
|
|
||||||
setScrollLeft,
|
|
||||||
setSelectedLines,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||||
name: "File",
|
name: "File",
|
||||||
gate: false,
|
|
||||||
init: () => {
|
init: () => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
@@ -232,45 +134,42 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
file: {},
|
file: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const viewCache = new Map<string, ViewCacheEntry>()
|
const legacyViewKey = createMemo(() => `${params.dir}/file${params.id ? "/" + params.id : ""}.v1`)
|
||||||
|
|
||||||
const disposeViews = () => {
|
const [view, setView, _, ready] = persisted(
|
||||||
for (const entry of viewCache.values()) {
|
Persist.scoped(params.dir!, params.id, "file-view", [legacyViewKey()]),
|
||||||
entry.dispose()
|
createStore<{
|
||||||
}
|
file: Record<string, FileViewState>
|
||||||
viewCache.clear()
|
}>({
|
||||||
|
file: {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_VIEW_FILES = 500
|
||||||
|
const viewMeta = { pruned: false }
|
||||||
|
|
||||||
|
const pruneView = (keep?: string) => {
|
||||||
|
const keys = Object.keys(view.file)
|
||||||
|
if (keys.length <= MAX_VIEW_FILES) return
|
||||||
|
|
||||||
|
const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
|
||||||
|
if (drop.length === 0) return
|
||||||
|
|
||||||
|
setView(
|
||||||
|
produce((draft) => {
|
||||||
|
for (const key of drop) {
|
||||||
|
delete draft.file[key]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pruneViews = () => {
|
createEffect(() => {
|
||||||
while (viewCache.size > MAX_FILE_VIEW_SESSIONS) {
|
if (!ready()) return
|
||||||
const first = viewCache.keys().next().value
|
if (viewMeta.pruned) return
|
||||||
if (!first) return
|
viewMeta.pruned = true
|
||||||
const entry = viewCache.get(first)
|
pruneView()
|
||||||
entry?.dispose()
|
})
|
||||||
viewCache.delete(first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadView = (dir: string, id: string | undefined) => {
|
|
||||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
|
||||||
const existing = viewCache.get(key)
|
|
||||||
if (existing) {
|
|
||||||
viewCache.delete(key)
|
|
||||||
viewCache.set(key, existing)
|
|
||||||
return existing.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
|
||||||
value: createViewSession(dir, id),
|
|
||||||
dispose,
|
|
||||||
}))
|
|
||||||
|
|
||||||
viewCache.set(key, entry)
|
|
||||||
pruneViews()
|
|
||||||
return entry.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const view = createMemo(() => loadView(params.dir!, params.id))
|
|
||||||
|
|
||||||
function ensure(path: string) {
|
function ensure(path: string) {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
@@ -347,32 +246,51 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const get = (input: string) => store.file[normalize(input)]
|
const get = (input: string) => store.file[normalize(input)]
|
||||||
|
|
||||||
const scrollTop = (input: string) => view().scrollTop(normalize(input))
|
const scrollTop = (input: string) => view.file[normalize(input)]?.scrollTop
|
||||||
const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
|
const scrollLeft = (input: string) => view.file[normalize(input)]?.scrollLeft
|
||||||
const selectedLines = (input: string) => view().selectedLines(normalize(input))
|
const selectedLines = (input: string) => view.file[normalize(input)]?.selectedLines
|
||||||
|
|
||||||
const setScrollTop = (input: string, top: number) => {
|
const setScrollTop = (input: string, top: number) => {
|
||||||
const path = normalize(input)
|
const path = normalize(input)
|
||||||
view().setScrollTop(path, top)
|
setView("file", path, (current) => {
|
||||||
|
if (current?.scrollTop === top) return current
|
||||||
|
return {
|
||||||
|
...(current ?? {}),
|
||||||
|
scrollTop: top,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pruneView(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setScrollLeft = (input: string, left: number) => {
|
const setScrollLeft = (input: string, left: number) => {
|
||||||
const path = normalize(input)
|
const path = normalize(input)
|
||||||
view().setScrollLeft(path, left)
|
setView("file", path, (current) => {
|
||||||
|
if (current?.scrollLeft === left) return current
|
||||||
|
return {
|
||||||
|
...(current ?? {}),
|
||||||
|
scrollLeft: left,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pruneView(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
|
const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
|
||||||
const path = normalize(input)
|
const path = normalize(input)
|
||||||
view().setSelectedLines(path, range)
|
const next = range ? normalizeSelectedLines(range) : null
|
||||||
|
setView("file", path, (current) => {
|
||||||
|
if (current?.selectedLines === next) return current
|
||||||
|
return {
|
||||||
|
...(current ?? {}),
|
||||||
|
selectedLines: next,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pruneView(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => stop())
|
||||||
stop()
|
|
||||||
disposeViews()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => view().ready(),
|
ready,
|
||||||
normalize,
|
normalize,
|
||||||
tab,
|
tab,
|
||||||
pathFromTab,
|
pathFromTab,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -99,146 +99,74 @@ function clonePrompt(prompt: Prompt): Prompt {
|
|||||||
return prompt.map(clonePart)
|
return prompt.map(clonePart)
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_KEY = "__workspace__"
|
|
||||||
const MAX_PROMPT_SESSIONS = 20
|
|
||||||
|
|
||||||
type PromptSession = ReturnType<typeof createPromptSession>
|
|
||||||
|
|
||||||
type PromptCacheEntry = {
|
|
||||||
value: PromptSession
|
|
||||||
dispose: VoidFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPromptSession(dir: string, id: string | undefined) {
|
|
||||||
const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2`
|
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
|
||||||
Persist.scoped(dir, id, "prompt", [legacy]),
|
|
||||||
createStore<{
|
|
||||||
prompt: Prompt
|
|
||||||
cursor?: number
|
|
||||||
context: {
|
|
||||||
activeTab: boolean
|
|
||||||
items: (ContextItem & { key: string })[]
|
|
||||||
}
|
|
||||||
}>({
|
|
||||||
prompt: clonePrompt(DEFAULT_PROMPT),
|
|
||||||
cursor: undefined,
|
|
||||||
context: {
|
|
||||||
activeTab: true,
|
|
||||||
items: [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
function keyForItem(item: ContextItem) {
|
|
||||||
if (item.type !== "file") return item.type
|
|
||||||
const start = item.selection?.startLine
|
|
||||||
const end = item.selection?.endLine
|
|
||||||
return `${item.type}:${item.path}:${start}:${end}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
ready,
|
|
||||||
current: createMemo(() => store.prompt),
|
|
||||||
cursor: createMemo(() => store.cursor),
|
|
||||||
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
|
||||||
context: {
|
|
||||||
activeTab: createMemo(() => store.context.activeTab),
|
|
||||||
items: createMemo(() => store.context.items),
|
|
||||||
addActive() {
|
|
||||||
setStore("context", "activeTab", true)
|
|
||||||
},
|
|
||||||
removeActive() {
|
|
||||||
setStore("context", "activeTab", false)
|
|
||||||
},
|
|
||||||
add(item: ContextItem) {
|
|
||||||
const key = keyForItem(item)
|
|
||||||
if (store.context.items.find((x) => x.key === key)) return
|
|
||||||
setStore("context", "items", (items) => [...items, { key, ...item }])
|
|
||||||
},
|
|
||||||
remove(key: string) {
|
|
||||||
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
set(prompt: Prompt, cursorPosition?: number) {
|
|
||||||
const next = clonePrompt(prompt)
|
|
||||||
batch(() => {
|
|
||||||
setStore("prompt", next)
|
|
||||||
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
batch(() => {
|
|
||||||
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
|
|
||||||
setStore("cursor", 0)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
|
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
|
||||||
name: "Prompt",
|
name: "Prompt",
|
||||||
gate: false,
|
|
||||||
init: () => {
|
init: () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const cache = new Map<string, PromptCacheEntry>()
|
const legacy = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v2`)
|
||||||
|
|
||||||
const disposeAll = () => {
|
const [store, setStore, _, ready] = persisted(
|
||||||
for (const entry of cache.values()) {
|
Persist.scoped(params.dir!, params.id, "prompt", [legacy()]),
|
||||||
entry.dispose()
|
createStore<{
|
||||||
}
|
prompt: Prompt
|
||||||
cache.clear()
|
cursor?: number
|
||||||
|
context: {
|
||||||
|
activeTab: boolean
|
||||||
|
items: (ContextItem & { key: string })[]
|
||||||
|
}
|
||||||
|
}>({
|
||||||
|
prompt: clonePrompt(DEFAULT_PROMPT),
|
||||||
|
cursor: undefined,
|
||||||
|
context: {
|
||||||
|
activeTab: true,
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function keyForItem(item: ContextItem) {
|
||||||
|
if (item.type !== "file") return item.type
|
||||||
|
const start = item.selection?.startLine
|
||||||
|
const end = item.selection?.endLine
|
||||||
|
return `${item.type}:${item.path}:${start}:${end}`
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(disposeAll)
|
|
||||||
|
|
||||||
const prune = () => {
|
|
||||||
while (cache.size > MAX_PROMPT_SESSIONS) {
|
|
||||||
const first = cache.keys().next().value
|
|
||||||
if (!first) return
|
|
||||||
const entry = cache.get(first)
|
|
||||||
entry?.dispose()
|
|
||||||
cache.delete(first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const load = (dir: string, id: string | undefined) => {
|
|
||||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
|
||||||
const existing = cache.get(key)
|
|
||||||
if (existing) {
|
|
||||||
cache.delete(key)
|
|
||||||
cache.set(key, existing)
|
|
||||||
return existing.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
|
||||||
value: createPromptSession(dir, id),
|
|
||||||
dispose,
|
|
||||||
}))
|
|
||||||
|
|
||||||
cache.set(key, entry)
|
|
||||||
prune()
|
|
||||||
return entry.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = createMemo(() => load(params.dir!, params.id))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => session().ready(),
|
ready,
|
||||||
current: () => session().current(),
|
current: createMemo(() => store.prompt),
|
||||||
cursor: () => session().cursor(),
|
cursor: createMemo(() => store.cursor),
|
||||||
dirty: () => session().dirty(),
|
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||||
context: {
|
context: {
|
||||||
activeTab: () => session().context.activeTab(),
|
activeTab: createMemo(() => store.context.activeTab),
|
||||||
items: () => session().context.items(),
|
items: createMemo(() => store.context.items),
|
||||||
addActive: () => session().context.addActive(),
|
addActive() {
|
||||||
removeActive: () => session().context.removeActive(),
|
setStore("context", "activeTab", true)
|
||||||
add: (item: ContextItem) => session().context.add(item),
|
},
|
||||||
remove: (key: string) => session().context.remove(key),
|
removeActive() {
|
||||||
|
setStore("context", "activeTab", false)
|
||||||
|
},
|
||||||
|
add(item: ContextItem) {
|
||||||
|
const key = keyForItem(item)
|
||||||
|
if (store.context.items.find((x) => x.key === key)) return
|
||||||
|
setStore("context", "items", (items) => [...items, { key, ...item }])
|
||||||
|
},
|
||||||
|
remove(key: string) {
|
||||||
|
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
set(prompt: Prompt, cursorPosition?: number) {
|
||||||
|
const next = clonePrompt(prompt)
|
||||||
|
batch(() => {
|
||||||
|
setStore("prompt", next)
|
||||||
|
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
batch(() => {
|
||||||
|
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
|
||||||
|
setStore("cursor", 0)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
set: (prompt: Prompt, cursorPosition?: number) => session().set(prompt, cursorPosition),
|
|
||||||
reset: () => session().reset(),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { batch, createMemo } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { produce, reconcile } from "solid-js/store"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
@@ -14,76 +14,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const [store, setStore] = globalSync.child(sdk.directory)
|
const [store, setStore] = globalSync.child(sdk.directory)
|
||||||
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
||||||
const chunk = 200
|
|
||||||
const inflight = new Map<string, Promise<void>>()
|
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
|
||||||
const inflightTodo = new Map<string, Promise<void>>()
|
|
||||||
const [meta, setMeta] = createStore({
|
|
||||||
limit: {} as Record<string, number>,
|
|
||||||
complete: {} as Record<string, boolean>,
|
|
||||||
loading: {} as Record<string, boolean>,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getSession = (sessionID: string) => {
|
|
||||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
|
||||||
if (match.found) return store.session[match.index]
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const limitFor = (count: number) => {
|
|
||||||
if (count <= chunk) return chunk
|
|
||||||
return Math.ceil(count / chunk) * chunk
|
|
||||||
}
|
|
||||||
|
|
||||||
const hydrateMessages = (sessionID: string) => {
|
|
||||||
if (meta.limit[sessionID] !== undefined) return
|
|
||||||
|
|
||||||
const messages = store.message[sessionID]
|
|
||||||
if (!messages) return
|
|
||||||
|
|
||||||
const limit = limitFor(messages.length)
|
|
||||||
setMeta("limit", sessionID, limit)
|
|
||||||
setMeta("complete", sessionID, messages.length < limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMessages = async (sessionID: string, limit: number) => {
|
|
||||||
if (meta.loading[sessionID]) return
|
|
||||||
|
|
||||||
setMeta("loading", sessionID, true)
|
|
||||||
await retry(() => sdk.client.session.messages({ sessionID, limit }))
|
|
||||||
.then((messages) => {
|
|
||||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
|
||||||
const next = items
|
|
||||||
.map((x) => x.info)
|
|
||||||
.filter((m) => !!m?.id)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
|
||||||
|
|
||||||
batch(() => {
|
|
||||||
setStore("message", sessionID, reconcile(next, { key: "id" }))
|
|
||||||
|
|
||||||
for (const message of items) {
|
|
||||||
setStore(
|
|
||||||
"part",
|
|
||||||
message.info.id,
|
|
||||||
reconcile(
|
|
||||||
message.parts
|
|
||||||
.filter((p) => !!p?.id)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setMeta("limit", sessionID, limit)
|
|
||||||
setMeta("complete", sessionID, next.length < limit)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setMeta("loading", sessionID, false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: store,
|
data: store,
|
||||||
@@ -100,7 +30,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
get: getSession,
|
get(sessionID: string) {
|
||||||
|
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||||
|
if (match.found) return store.session[match.index]
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
addOptimisticMessage(input: {
|
addOptimisticMessage(input: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
@@ -132,98 +66,58 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
async sync(sessionID: string) {
|
async sync(sessionID: string, _isRetry = false) {
|
||||||
const hasSession = getSession(sessionID) !== undefined
|
const [session, messages, todo, diff] = await Promise.all([
|
||||||
hydrateMessages(sessionID)
|
retry(() => sdk.client.session.get({ sessionID })),
|
||||||
|
retry(() => sdk.client.session.messages({ sessionID, limit: 1000 })),
|
||||||
|
retry(() => sdk.client.session.todo({ sessionID })),
|
||||||
|
retry(() => sdk.client.session.diff({ sessionID })),
|
||||||
|
])
|
||||||
|
|
||||||
const hasMessages = store.message[sessionID] !== undefined
|
batch(() => {
|
||||||
if (hasSession && hasMessages) return
|
setStore(
|
||||||
|
"session",
|
||||||
|
produce((draft) => {
|
||||||
|
const match = Binary.search(draft, sessionID, (s) => s.id)
|
||||||
|
if (match.found) {
|
||||||
|
draft[match.index] = session.data!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
draft.splice(match.index, 0, session.data!)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const pending = inflight.get(sessionID)
|
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
||||||
if (pending) return pending
|
setStore(
|
||||||
|
"message",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
(messages.data ?? [])
|
||||||
|
.map((x) => x.info)
|
||||||
|
.filter((m) => !!m?.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const limit = meta.limit[sessionID] ?? chunk
|
for (const message of messages.data ?? []) {
|
||||||
|
if (!message?.info?.id) continue
|
||||||
|
setStore(
|
||||||
|
"part",
|
||||||
|
message.info.id,
|
||||||
|
reconcile(
|
||||||
|
message.parts
|
||||||
|
.filter((p) => !!p?.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const sessionReq = hasSession
|
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
||||||
? Promise.resolve()
|
})
|
||||||
: retry(() => sdk.client.session.get({ sessionID })).then((session) => {
|
|
||||||
const data = session.data
|
|
||||||
if (!data) return
|
|
||||||
setStore(
|
|
||||||
"session",
|
|
||||||
produce((draft) => {
|
|
||||||
const match = Binary.search(draft, sessionID, (s) => s.id)
|
|
||||||
if (match.found) {
|
|
||||||
draft[match.index] = data
|
|
||||||
return
|
|
||||||
}
|
|
||||||
draft.splice(match.index, 0, data)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit)
|
|
||||||
|
|
||||||
const promise = Promise.all([sessionReq, messagesReq])
|
|
||||||
.then(() => {})
|
|
||||||
.finally(() => {
|
|
||||||
inflight.delete(sessionID)
|
|
||||||
})
|
|
||||||
|
|
||||||
inflight.set(sessionID, promise)
|
|
||||||
return promise
|
|
||||||
},
|
|
||||||
async diff(sessionID: string) {
|
|
||||||
if (store.session_diff[sessionID] !== undefined) return
|
|
||||||
|
|
||||||
const pending = inflightDiff.get(sessionID)
|
|
||||||
if (pending) return pending
|
|
||||||
|
|
||||||
const promise = retry(() => sdk.client.session.diff({ sessionID }))
|
|
||||||
.then((diff) => {
|
|
||||||
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
inflightDiff.delete(sessionID)
|
|
||||||
})
|
|
||||||
|
|
||||||
inflightDiff.set(sessionID, promise)
|
|
||||||
return promise
|
|
||||||
},
|
|
||||||
async todo(sessionID: string) {
|
|
||||||
if (store.todo[sessionID] !== undefined) return
|
|
||||||
|
|
||||||
const pending = inflightTodo.get(sessionID)
|
|
||||||
if (pending) return pending
|
|
||||||
|
|
||||||
const promise = retry(() => sdk.client.session.todo({ sessionID }))
|
|
||||||
.then((todo) => {
|
|
||||||
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
inflightTodo.delete(sessionID)
|
|
||||||
})
|
|
||||||
|
|
||||||
inflightTodo.set(sessionID, promise)
|
|
||||||
return promise
|
|
||||||
},
|
|
||||||
history: {
|
|
||||||
more(sessionID: string) {
|
|
||||||
if (store.message[sessionID] === undefined) return false
|
|
||||||
if (meta.limit[sessionID] === undefined) return false
|
|
||||||
if (meta.complete[sessionID]) return false
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
loading(sessionID: string) {
|
|
||||||
return meta.loading[sessionID] ?? false
|
|
||||||
},
|
|
||||||
async loadMore(sessionID: string, count = chunk) {
|
|
||||||
if (meta.loading[sessionID]) return
|
|
||||||
if (meta.complete[sessionID]) return
|
|
||||||
|
|
||||||
const current = meta.limit[sessionID] ?? chunk
|
|
||||||
await loadMessages(sessionID, current + count)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
fetch: async (count = 10) => {
|
fetch: async (count = 10) => {
|
||||||
setStore("limit", (x) => x + count)
|
setStore("limit", (x) => x + count)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -14,175 +14,108 @@ export type LocalPTY = {
|
|||||||
scrollY?: number
|
scrollY?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_KEY = "__workspace__"
|
|
||||||
const MAX_TERMINAL_SESSIONS = 20
|
|
||||||
|
|
||||||
type TerminalSession = ReturnType<typeof createTerminalSession>
|
|
||||||
|
|
||||||
type TerminalCacheEntry = {
|
|
||||||
value: TerminalSession
|
|
||||||
dispose: VoidFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id: string | undefined) {
|
|
||||||
const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1`
|
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
|
||||||
Persist.scoped(dir, id, "terminal", [legacy]),
|
|
||||||
createStore<{
|
|
||||||
active?: string
|
|
||||||
all: LocalPTY[]
|
|
||||||
}>({
|
|
||||||
all: [],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
ready,
|
|
||||||
all: createMemo(() => Object.values(store.all)),
|
|
||||||
active: createMemo(() => store.active),
|
|
||||||
new() {
|
|
||||||
sdk.client.pty
|
|
||||||
.create({ title: `Terminal ${store.all.length + 1}` })
|
|
||||||
.then((pty) => {
|
|
||||||
const id = pty.data?.id
|
|
||||||
if (!id) return
|
|
||||||
setStore("all", [
|
|
||||||
...store.all,
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
title: pty.data?.title ?? "Terminal",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
setStore("active", id)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Failed to create terminal", e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
|
||||||
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
|
||||||
sdk.client.pty
|
|
||||||
.update({
|
|
||||||
ptyID: pty.id,
|
|
||||||
title: pty.title,
|
|
||||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Failed to update terminal", e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async clone(id: string) {
|
|
||||||
const index = store.all.findIndex((x) => x.id === id)
|
|
||||||
const pty = store.all[index]
|
|
||||||
if (!pty) return
|
|
||||||
const clone = await sdk.client.pty
|
|
||||||
.create({
|
|
||||||
title: pty.title,
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Failed to clone terminal", e)
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
if (!clone?.data) return
|
|
||||||
setStore("all", index, {
|
|
||||||
...pty,
|
|
||||||
...clone.data,
|
|
||||||
})
|
|
||||||
if (store.active === pty.id) {
|
|
||||||
setStore("active", clone.data.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
open(id: string) {
|
|
||||||
setStore("active", id)
|
|
||||||
},
|
|
||||||
async close(id: string) {
|
|
||||||
batch(() => {
|
|
||||||
setStore(
|
|
||||||
"all",
|
|
||||||
store.all.filter((x) => x.id !== id),
|
|
||||||
)
|
|
||||||
if (store.active === id) {
|
|
||||||
const index = store.all.findIndex((f) => f.id === id)
|
|
||||||
const previous = store.all[Math.max(0, index - 1)]
|
|
||||||
setStore("active", previous?.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
|
|
||||||
console.error("Failed to close terminal", e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
move(id: string, to: number) {
|
|
||||||
const index = store.all.findIndex((f) => f.id === id)
|
|
||||||
if (index === -1) return
|
|
||||||
setStore(
|
|
||||||
"all",
|
|
||||||
produce((all) => {
|
|
||||||
all.splice(to, 0, all.splice(index, 1)[0])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
|
export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
|
||||||
name: "Terminal",
|
name: "Terminal",
|
||||||
gate: false,
|
|
||||||
init: () => {
|
init: () => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const cache = new Map<string, TerminalCacheEntry>()
|
const legacy = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
|
||||||
|
|
||||||
const disposeAll = () => {
|
const [store, setStore, _, ready] = persisted(
|
||||||
for (const entry of cache.values()) {
|
Persist.scoped(params.dir!, params.id, "terminal", [legacy()]),
|
||||||
entry.dispose()
|
createStore<{
|
||||||
}
|
active?: string
|
||||||
cache.clear()
|
all: LocalPTY[]
|
||||||
}
|
}>({
|
||||||
|
all: [],
|
||||||
onCleanup(disposeAll)
|
}),
|
||||||
|
)
|
||||||
const prune = () => {
|
|
||||||
while (cache.size > MAX_TERMINAL_SESSIONS) {
|
|
||||||
const first = cache.keys().next().value
|
|
||||||
if (!first) return
|
|
||||||
const entry = cache.get(first)
|
|
||||||
entry?.dispose()
|
|
||||||
cache.delete(first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const load = (dir: string, id: string | undefined) => {
|
|
||||||
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
|
||||||
const existing = cache.get(key)
|
|
||||||
if (existing) {
|
|
||||||
cache.delete(key)
|
|
||||||
cache.set(key, existing)
|
|
||||||
return existing.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = createRoot((dispose) => ({
|
|
||||||
value: createTerminalSession(sdk, dir, id),
|
|
||||||
dispose,
|
|
||||||
}))
|
|
||||||
|
|
||||||
cache.set(key, entry)
|
|
||||||
prune()
|
|
||||||
return entry.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = createMemo(() => load(params.dir!, params.id))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: () => session().ready(),
|
ready,
|
||||||
all: () => session().all(),
|
all: createMemo(() => Object.values(store.all)),
|
||||||
active: () => session().active(),
|
active: createMemo(() => store.active),
|
||||||
new: () => session().new(),
|
new() {
|
||||||
update: (pty: Partial<LocalPTY> & { id: string }) => session().update(pty),
|
sdk.client.pty
|
||||||
clone: (id: string) => session().clone(id),
|
.create({ title: `Terminal ${store.all.length + 1}` })
|
||||||
open: (id: string) => session().open(id),
|
.then((pty) => {
|
||||||
close: (id: string) => session().close(id),
|
const id = pty.data?.id
|
||||||
move: (id: string, to: number) => session().move(id, to),
|
if (!id) return
|
||||||
|
setStore("all", [
|
||||||
|
...store.all,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
title: pty.data?.title ?? "Terminal",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
setStore("active", id)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to create terminal", e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
|
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
||||||
|
sdk.client.pty
|
||||||
|
.update({
|
||||||
|
ptyID: pty.id,
|
||||||
|
title: pty.title,
|
||||||
|
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to update terminal", e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async clone(id: string) {
|
||||||
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
|
const pty = store.all[index]
|
||||||
|
if (!pty) return
|
||||||
|
const clone = await sdk.client.pty
|
||||||
|
.create({
|
||||||
|
title: pty.title,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to clone terminal", e)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (!clone?.data) return
|
||||||
|
setStore("all", index, {
|
||||||
|
...pty,
|
||||||
|
...clone.data,
|
||||||
|
})
|
||||||
|
if (store.active === pty.id) {
|
||||||
|
setStore("active", clone.data.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
open(id: string) {
|
||||||
|
setStore("active", id)
|
||||||
|
},
|
||||||
|
async close(id: string) {
|
||||||
|
batch(() => {
|
||||||
|
setStore(
|
||||||
|
"all",
|
||||||
|
store.all.filter((x) => x.id !== id),
|
||||||
|
)
|
||||||
|
if (store.active === id) {
|
||||||
|
const index = store.all.findIndex((f) => f.id === id)
|
||||||
|
const previous = store.all[Math.max(0, index - 1)]
|
||||||
|
setStore("active", previous?.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
|
||||||
|
console.error("Failed to close terminal", e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
move(id: string, to: number) {
|
||||||
|
const index = store.all.findIndex((f) => f.id === id)
|
||||||
|
if (index === -1) return
|
||||||
|
setStore(
|
||||||
|
"all",
|
||||||
|
produce((all) => {
|
||||||
|
all.splice(to, 0, all.splice(index, 1)[0])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
batch,
|
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createSignal,
|
createSignal,
|
||||||
@@ -32,7 +31,7 @@ import { getFilename } from "@opencode-ai/util/path"
|
|||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Session } from "@opencode-ai/sdk/v2/client"
|
import { Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import {
|
import {
|
||||||
DragDropProvider,
|
DragDropProvider,
|
||||||
DragDropSensors,
|
DragDropSensors,
|
||||||
@@ -48,7 +47,6 @@ import { useGlobalSDK } from "@/context/global-sdk"
|
|||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
|
||||||
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
@@ -57,7 +55,6 @@ import { DialogEditProject } from "@/components/dialog-edit-project"
|
|||||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||||
import { navStart } from "@/utils/perf"
|
|
||||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
|
|
||||||
@@ -287,146 +284,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const currentSessions = createMemo(() => projectSessions(currentProject()))
|
const currentSessions = createMemo(() => projectSessions(currentProject()))
|
||||||
|
|
||||||
type PrefetchQueue = {
|
|
||||||
inflight: Set<string>
|
|
||||||
pending: string[]
|
|
||||||
pendingSet: Set<string>
|
|
||||||
running: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefetchChunk = 200
|
|
||||||
const prefetchConcurrency = 1
|
|
||||||
const prefetchPendingLimit = 6
|
|
||||||
const prefetchToken = { value: 0 }
|
|
||||||
const prefetchQueues = new Map<string, PrefetchQueue>()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
params.dir
|
|
||||||
globalSDK.url
|
|
||||||
|
|
||||||
prefetchToken.value += 1
|
|
||||||
for (const q of prefetchQueues.values()) {
|
|
||||||
q.pending.length = 0
|
|
||||||
q.pendingSet.clear()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const queueFor = (directory: string) => {
|
|
||||||
const existing = prefetchQueues.get(directory)
|
|
||||||
if (existing) return existing
|
|
||||||
|
|
||||||
const created: PrefetchQueue = {
|
|
||||||
inflight: new Set(),
|
|
||||||
pending: [],
|
|
||||||
pendingSet: new Set(),
|
|
||||||
running: 0,
|
|
||||||
}
|
|
||||||
prefetchQueues.set(directory, created)
|
|
||||||
return created
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefetchMessages = (directory: string, sessionID: string, token: number) => {
|
|
||||||
const [, setStore] = globalSync.child(directory)
|
|
||||||
|
|
||||||
return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
|
||||||
.then((messages) => {
|
|
||||||
if (prefetchToken.value !== token) return
|
|
||||||
|
|
||||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
|
||||||
const next = items
|
|
||||||
.map((x) => x.info)
|
|
||||||
.filter((m) => !!m?.id)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
|
||||||
|
|
||||||
batch(() => {
|
|
||||||
setStore("message", sessionID, reconcile(next, { key: "id" }))
|
|
||||||
|
|
||||||
for (const message of items) {
|
|
||||||
setStore(
|
|
||||||
"part",
|
|
||||||
message.info.id,
|
|
||||||
reconcile(
|
|
||||||
message.parts
|
|
||||||
.filter((p) => !!p?.id)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(() => undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pumpPrefetch = (directory: string) => {
|
|
||||||
const q = queueFor(directory)
|
|
||||||
if (q.running >= prefetchConcurrency) return
|
|
||||||
|
|
||||||
const sessionID = q.pending.shift()
|
|
||||||
if (!sessionID) return
|
|
||||||
|
|
||||||
q.pendingSet.delete(sessionID)
|
|
||||||
q.inflight.add(sessionID)
|
|
||||||
q.running += 1
|
|
||||||
|
|
||||||
const token = prefetchToken.value
|
|
||||||
|
|
||||||
void prefetchMessages(directory, sessionID, token).finally(() => {
|
|
||||||
q.running -= 1
|
|
||||||
q.inflight.delete(sessionID)
|
|
||||||
pumpPrefetch(directory)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefetchSession = (session: Session, priority: "high" | "low" = "low") => {
|
|
||||||
const directory = session.directory
|
|
||||||
if (!directory) return
|
|
||||||
|
|
||||||
const [store] = globalSync.child(directory)
|
|
||||||
if (store.message[session.id] !== undefined) return
|
|
||||||
|
|
||||||
const q = queueFor(directory)
|
|
||||||
if (q.inflight.has(session.id)) return
|
|
||||||
if (q.pendingSet.has(session.id)) return
|
|
||||||
|
|
||||||
if (priority === "high") q.pending.unshift(session.id)
|
|
||||||
if (priority !== "high") q.pending.push(session.id)
|
|
||||||
q.pendingSet.add(session.id)
|
|
||||||
|
|
||||||
while (q.pending.length > prefetchPendingLimit) {
|
|
||||||
const dropped = q.pending.pop()
|
|
||||||
if (!dropped) continue
|
|
||||||
q.pendingSet.delete(dropped)
|
|
||||||
}
|
|
||||||
|
|
||||||
pumpPrefetch(directory)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const sessions = currentSessions()
|
|
||||||
const id = params.id
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
const first = sessions[0]
|
|
||||||
if (first) prefetchSession(first)
|
|
||||||
|
|
||||||
const second = sessions[1]
|
|
||||||
if (second) prefetchSession(second)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = sessions.findIndex((s) => s.id === id)
|
|
||||||
if (index === -1) return
|
|
||||||
|
|
||||||
const next = sessions[index + 1]
|
|
||||||
if (next) prefetchSession(next)
|
|
||||||
|
|
||||||
const prev = sessions[index - 1]
|
|
||||||
if (prev) prefetchSession(prev)
|
|
||||||
})
|
|
||||||
|
|
||||||
function navigateSessionByOffset(offset: number) {
|
function navigateSessionByOffset(offset: number) {
|
||||||
const projects = layout.projects.list()
|
const projects = layout.projects.list()
|
||||||
if (projects.length === 0) return
|
if (projects.length === 0) return
|
||||||
@@ -452,27 +309,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
if (targetIndex >= 0 && targetIndex < sessions.length) {
|
if (targetIndex >= 0 && targetIndex < sessions.length) {
|
||||||
const session = sessions[targetIndex]
|
const session = sessions[targetIndex]
|
||||||
const next = sessions[targetIndex + 1]
|
|
||||||
const prev = sessions[targetIndex - 1]
|
|
||||||
|
|
||||||
if (offset > 0) {
|
|
||||||
if (next) prefetchSession(next, "high")
|
|
||||||
if (prev) prefetchSession(prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset < 0) {
|
|
||||||
if (prev) prefetchSession(prev, "high")
|
|
||||||
if (next) prefetchSession(next)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
navStart({
|
|
||||||
dir: base64Encode(session.directory),
|
|
||||||
from: params.id,
|
|
||||||
to: session.id,
|
|
||||||
trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
navigateToSession(session)
|
navigateToSession(session)
|
||||||
queueMicrotask(() => scrollToSession(session.id))
|
queueMicrotask(() => scrollToSession(session.id))
|
||||||
return
|
return
|
||||||
@@ -488,27 +324,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = offset > 0 ? 0 : nextProjectSessions.length - 1
|
const targetSession = offset > 0 ? nextProjectSessions[0] : nextProjectSessions[nextProjectSessions.length - 1]
|
||||||
const targetSession = nextProjectSessions[index]
|
|
||||||
const nextSession = nextProjectSessions[index + 1]
|
|
||||||
const prevSession = nextProjectSessions[index - 1]
|
|
||||||
|
|
||||||
if (offset > 0) {
|
|
||||||
if (nextSession) prefetchSession(nextSession, "high")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset < 0) {
|
|
||||||
if (prevSession) prefetchSession(prevSession, "high")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
navStart({
|
|
||||||
dir: base64Encode(targetSession.directory),
|
|
||||||
from: params.id,
|
|
||||||
to: targetSession.id,
|
|
||||||
trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
navigateToSession(targetSession)
|
navigateToSession(targetSession)
|
||||||
queueMicrotask(() => scrollToSession(targetSession.id))
|
queueMicrotask(() => scrollToSession(targetSession.id))
|
||||||
}
|
}
|
||||||
@@ -863,8 +679,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
<A
|
<A
|
||||||
href={`${props.slug}/session/${props.session.id}`}
|
href={`${props.slug}/session/${props.session.id}`}
|
||||||
class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
|
class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
|
||||||
onMouseEnter={() => prefetchSession(props.session, "high")}
|
|
||||||
onFocus={() => prefetchSession(props.session, "high")}
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
||||||
<span
|
<span
|
||||||
@@ -1061,7 +875,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Tooltip placement="right" value={getFilename(props.project.worktree)}>
|
<Tooltip placement="right" value={props.project.worktree}>
|
||||||
<ProjectVisual project={props.project} />
|
<ProjectVisual project={props.project} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
+201
-519
@@ -1,4 +1,4 @@
|
|||||||
import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
|
import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
@@ -8,7 +8,6 @@ import { createStore } from "solid-js/store"
|
|||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
@@ -50,17 +49,10 @@ import {
|
|||||||
NewSessionView,
|
NewSessionView,
|
||||||
} from "@/components/session"
|
} from "@/components/session"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { navMark, navParams } from "@/utils/perf"
|
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
|
|
||||||
type DiffStyle = "unified" | "split"
|
type DiffStyle = "unified" | "split"
|
||||||
|
|
||||||
const handoff = {
|
|
||||||
prompt: "",
|
|
||||||
terminals: [] as string[],
|
|
||||||
files: {} as Record<string, SelectedLineRange | null>,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionReviewTabProps {
|
interface SessionReviewTabProps {
|
||||||
diffs: () => FileDiff[]
|
diffs: () => FileDiff[]
|
||||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
||||||
@@ -170,46 +162,6 @@ export default function Page() {
|
|||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey()))
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => [params.dir, params.id] as const,
|
|
||||||
([dir, id], prev) => {
|
|
||||||
if (!id) return
|
|
||||||
navParams({ dir, from: prev?.[1], to: id })
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (!prompt.ready()) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "storage:prompt-ready" })
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (!terminal.ready()) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "storage:terminal-ready" })
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (!file.ready()) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "storage:file-view-ready" })
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (sync.data.message[id] === undefined) return
|
|
||||||
navMark({ dir: params.dir, to: id, name: "session:data-ready" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
|
|
||||||
function normalizeTab(tab: string) {
|
function normalizeTab(tab: string) {
|
||||||
@@ -264,8 +216,6 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const reviewCount = createMemo(() => info()?.summary?.files ?? 0)
|
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
|
||||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
const messagesReady = createMemo(() => {
|
const messagesReady = createMemo(() => {
|
||||||
@@ -273,16 +223,6 @@ export default function Page() {
|
|||||||
if (!id) return true
|
if (!id) return true
|
||||||
return sync.data.message[id] !== undefined
|
return sync.data.message[id] !== undefined
|
||||||
})
|
})
|
||||||
const historyMore = createMemo(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return false
|
|
||||||
return sync.session.history.more(id)
|
|
||||||
})
|
|
||||||
const historyLoading = createMemo(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return false
|
|
||||||
return sync.session.history.loading(id)
|
|
||||||
})
|
|
||||||
const emptyUserMessages: UserMessage[] = []
|
const emptyUserMessages: UserMessage[] = []
|
||||||
const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages)
|
const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages)
|
||||||
const visibleUserMessages = createMemo(() => {
|
const visibleUserMessages = createMemo(() => {
|
||||||
@@ -309,20 +249,11 @@ export default function Page() {
|
|||||||
activeTerminalDraggable: undefined as string | undefined,
|
activeTerminalDraggable: undefined as string | undefined,
|
||||||
expanded: {} as Record<string, boolean>,
|
expanded: {} as Record<string, boolean>,
|
||||||
messageId: undefined as string | undefined,
|
messageId: undefined as string | undefined,
|
||||||
turnStart: 0,
|
|
||||||
mobileTab: "session" as "session" | "review",
|
mobileTab: "session" as "session" | "review",
|
||||||
newSessionWorktree: "main",
|
newSessionWorktree: "main",
|
||||||
promptHeight: 0,
|
promptHeight: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderedUserMessages = createMemo(() => {
|
|
||||||
const msgs = visibleUserMessages()
|
|
||||||
const start = store.turnStart
|
|
||||||
if (start <= 0) return msgs
|
|
||||||
if (start >= msgs.length) return emptyUserMessages
|
|
||||||
return msgs.slice(start)
|
|
||||||
}, emptyUserMessages)
|
|
||||||
|
|
||||||
const newSessionWorktree = createMemo(() => {
|
const newSessionWorktree = createMemo(() => {
|
||||||
if (store.newSessionWorktree === "create") return "create"
|
if (store.newSessionWorktree === "create") return "create"
|
||||||
const project = sync.project
|
const project = sync.project
|
||||||
@@ -359,12 +290,6 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||||
const diffsReady = createMemo(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return true
|
|
||||||
if (!hasReview()) return true
|
|
||||||
return sync.data.session_diff[id] !== undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const idle = { type: "idle" as const }
|
const idle = { type: "idle" as const }
|
||||||
let inputRef!: HTMLDivElement
|
let inputRef!: HTMLDivElement
|
||||||
@@ -377,10 +302,11 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!layout.terminal.opened()) return
|
if (layout.terminal.opened()) {
|
||||||
if (!terminal.ready()) return
|
if (terminal.all().length === 0) {
|
||||||
if (terminal.all().length !== 0) return
|
terminal.new()
|
||||||
terminal.new()
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -717,10 +643,12 @@ export default function Page() {
|
|||||||
.filter((tab) => tab !== "context"),
|
.filter((tab) => tab !== "context"),
|
||||||
)
|
)
|
||||||
|
|
||||||
const reviewTab = createMemo(() => hasReview() || tabs().active() === "review")
|
const reviewTab = createMemo(() => diffs().length > 0 || tabs().active() === "review")
|
||||||
const mobileReview = createMemo(() => !isDesktop() && hasReview() && store.mobileTab === "review")
|
const mobileReview = createMemo(() => !isDesktop() && diffs().length > 0 && store.mobileTab === "review")
|
||||||
|
|
||||||
const showTabs = createMemo(() => layout.review.opened() && (hasReview() || tabs().all().length > 0 || contextOpen()))
|
const showTabs = createMemo(
|
||||||
|
() => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0 || contextOpen()),
|
||||||
|
)
|
||||||
|
|
||||||
const activeTab = createMemo(() => {
|
const activeTab = createMemo(() => {
|
||||||
const active = tabs().active()
|
const active = tabs().active()
|
||||||
@@ -736,22 +664,10 @@ export default function Page() {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!layout.ready()) return
|
if (!layout.ready()) return
|
||||||
if (tabs().active()) return
|
if (tabs().active()) return
|
||||||
if (!hasReview() && openedTabs().length === 0 && !contextOpen()) return
|
if (diffs().length === 0 && openedTabs().length === 0 && !contextOpen()) return
|
||||||
tabs().setActive(activeTab())
|
tabs().setActive(activeTab())
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
if (!hasReview()) return
|
|
||||||
|
|
||||||
const wants = isDesktop() ? layout.review.opened() && activeTab() === "review" : store.mobileTab === "review"
|
|
||||||
if (!wants) return
|
|
||||||
if (diffsReady()) return
|
|
||||||
|
|
||||||
sync.session.diff(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isWorking = createMemo(() => status().type !== "idle")
|
const isWorking = createMemo(() => status().type !== "idle")
|
||||||
const autoScroll = createAutoScroll({
|
const autoScroll = createAutoScroll({
|
||||||
working: isWorking,
|
working: isWorking,
|
||||||
@@ -767,88 +683,6 @@ export default function Page() {
|
|||||||
autoScroll.scrollRef(el)
|
autoScroll.scrollRef(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
const turnInit = 20
|
|
||||||
const turnBatch = 20
|
|
||||||
let turnHandle: number | undefined
|
|
||||||
let turnIdle = false
|
|
||||||
|
|
||||||
function cancelTurnBackfill() {
|
|
||||||
const handle = turnHandle
|
|
||||||
if (handle === undefined) return
|
|
||||||
turnHandle = undefined
|
|
||||||
|
|
||||||
if (turnIdle && window.cancelIdleCallback) {
|
|
||||||
window.cancelIdleCallback(handle)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleTurnBackfill() {
|
|
||||||
if (turnHandle !== undefined) return
|
|
||||||
if (store.turnStart <= 0) return
|
|
||||||
|
|
||||||
if (window.requestIdleCallback) {
|
|
||||||
turnIdle = true
|
|
||||||
turnHandle = window.requestIdleCallback(() => {
|
|
||||||
turnHandle = undefined
|
|
||||||
backfillTurns()
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
turnIdle = false
|
|
||||||
turnHandle = window.setTimeout(() => {
|
|
||||||
turnHandle = undefined
|
|
||||||
backfillTurns()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function backfillTurns() {
|
|
||||||
const start = store.turnStart
|
|
||||||
if (start <= 0) return
|
|
||||||
|
|
||||||
const next = start - turnBatch
|
|
||||||
const nextStart = next > 0 ? next : 0
|
|
||||||
|
|
||||||
const el = scroller
|
|
||||||
if (!el) {
|
|
||||||
setStore("turnStart", nextStart)
|
|
||||||
scheduleTurnBackfill()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeTop = el.scrollTop
|
|
||||||
const beforeHeight = el.scrollHeight
|
|
||||||
|
|
||||||
setStore("turnStart", nextStart)
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const delta = el.scrollHeight - beforeHeight
|
|
||||||
if (delta) el.scrollTop = beforeTop + delta
|
|
||||||
})
|
|
||||||
|
|
||||||
scheduleTurnBackfill()
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => [params.id, messagesReady()] as const,
|
|
||||||
([id, ready]) => {
|
|
||||||
cancelTurnBackfill()
|
|
||||||
setStore("turnStart", 0)
|
|
||||||
if (!id || !ready) return
|
|
||||||
|
|
||||||
const len = visibleUserMessages().length
|
|
||||||
const start = len > turnInit ? len - turnInit : 0
|
|
||||||
setStore("turnStart", start)
|
|
||||||
scheduleTurnBackfill()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createResizeObserver(
|
createResizeObserver(
|
||||||
() => promptDock,
|
() => promptDock,
|
||||||
({ height }) => {
|
({ height }) => {
|
||||||
@@ -876,21 +710,6 @@ export default function Page() {
|
|||||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||||
setActiveMessage(message)
|
setActiveMessage(message)
|
||||||
|
|
||||||
const msgs = visibleUserMessages()
|
|
||||||
const index = msgs.findIndex((m) => m.id === message.id)
|
|
||||||
if (index !== -1 && index < store.turnStart) {
|
|
||||||
setStore("turnStart", index)
|
|
||||||
scheduleTurnBackfill()
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const el = document.getElementById(anchor(message.id))
|
|
||||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
|
||||||
})
|
|
||||||
|
|
||||||
updateHash(message.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const el = document.getElementById(anchor(message.id))
|
const el = document.getElementById(anchor(message.id))
|
||||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
if (el) el.scrollIntoView({ behavior, block: "start" })
|
||||||
updateHash(message.id)
|
updateHash(message.id)
|
||||||
@@ -936,27 +755,12 @@ export default function Page() {
|
|||||||
if (!sessionID || !ready) return
|
if (!sessionID || !ready) return
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const hash = window.location.hash.slice(1)
|
const id = window.location.hash.slice(1)
|
||||||
if (!hash) {
|
const hashTarget = id ? document.getElementById(id) : undefined
|
||||||
autoScroll.forceScrollToBottom()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashTarget = document.getElementById(hash)
|
|
||||||
if (hashTarget) {
|
if (hashTarget) {
|
||||||
hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
|
hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = hash.match(/^message-(.+)$/)
|
|
||||||
if (match) {
|
|
||||||
const msg = visibleUserMessages().find((m) => m.id === match[1])
|
|
||||||
if (msg) {
|
|
||||||
scrollToMessage(msg, "auto")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
autoScroll.forceScrollToBottom()
|
autoScroll.forceScrollToBottom()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -965,43 +769,7 @@ export default function Page() {
|
|||||||
document.addEventListener("keydown", handleKeyDown)
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
const previewPrompt = () =>
|
|
||||||
prompt
|
|
||||||
.current()
|
|
||||||
.map((part) => {
|
|
||||||
if (part.type === "file") return `[file:${part.path}]`
|
|
||||||
if (part.type === "agent") return `@${part.name}`
|
|
||||||
if (part.type === "image") return `[image:${part.filename}]`
|
|
||||||
return part.content
|
|
||||||
})
|
|
||||||
.join("")
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!prompt.ready()) return
|
|
||||||
handoff.prompt = previewPrompt()
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!terminal.ready()) return
|
|
||||||
handoff.terminals = terminal.all().map((t) => t.title)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!file.ready()) return
|
|
||||||
handoff.files = Object.fromEntries(
|
|
||||||
tabs()
|
|
||||||
.all()
|
|
||||||
.flatMap((tab) => {
|
|
||||||
const path = file.pathFromTab(tab)
|
|
||||||
if (!path) return []
|
|
||||||
return [[path, file.selectedLines(path) ?? null] as const]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
cancelTurnBackfill()
|
|
||||||
document.removeEventListener("keydown", handleKeyDown)
|
document.removeEventListener("keydown", handleKeyDown)
|
||||||
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
|
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
|
||||||
})
|
})
|
||||||
@@ -1011,7 +779,7 @@ export default function Page() {
|
|||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||||
{/* Mobile tab bar - only shown on mobile when there are diffs */}
|
{/* Mobile tab bar - only shown on mobile when there are diffs */}
|
||||||
<Show when={!isDesktop() && hasReview()}>
|
<Show when={!isDesktop() && diffs().length > 0}>
|
||||||
<Tabs class="h-auto">
|
<Tabs class="h-auto">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
@@ -1028,7 +796,7 @@ export default function Page() {
|
|||||||
classes={{ button: "w-full" }}
|
classes={{ button: "w-full" }}
|
||||||
onClick={() => setStore("mobileTab", "review")}
|
onClick={() => setStore("mobileTab", "review")}
|
||||||
>
|
>
|
||||||
{reviewCount()} Files Changed
|
{diffs().length} Files Changed
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -1053,26 +821,21 @@ export default function Page() {
|
|||||||
when={!mobileReview()}
|
when={!mobileReview()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="relative h-full overflow-hidden">
|
<div class="relative h-full overflow-hidden">
|
||||||
<Show
|
<SessionReviewTab
|
||||||
when={diffsReady()}
|
diffs={diffs}
|
||||||
fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>}
|
view={view}
|
||||||
>
|
diffStyle="unified"
|
||||||
<SessionReviewTab
|
onViewFile={(path) => {
|
||||||
diffs={diffs}
|
const value = file.tab(path)
|
||||||
view={view}
|
tabs().open(value)
|
||||||
diffStyle="unified"
|
file.load(path)
|
||||||
onViewFile={(path) => {
|
}}
|
||||||
const value = file.tab(path)
|
classes={{
|
||||||
tabs().open(value)
|
root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
|
||||||
file.load(path)
|
header: "px-4",
|
||||||
}}
|
container: "px-4",
|
||||||
classes={{
|
}}
|
||||||
root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
|
/>
|
||||||
header: "px-4",
|
|
||||||
container: "px-4",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -1105,82 +868,42 @@ export default function Page() {
|
|||||||
"mt-0": showTabs(),
|
"mt-0": showTabs(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={store.turnStart > 0}>
|
<For each={visibleUserMessages()}>
|
||||||
<div class="w-full flex justify-center">
|
{(message) => (
|
||||||
<Button
|
<div
|
||||||
variant="ghost"
|
id={anchor(message.id)}
|
||||||
size="large"
|
data-message-id={message.id}
|
||||||
class="text-12-medium opacity-50"
|
classList={{
|
||||||
onClick={() => setStore("turnStart", 0)}
|
"min-w-0 w-full max-w-full": true,
|
||||||
>
|
"last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
|
||||||
Render earlier messages
|
platform.platform !== "desktop",
|
||||||
</Button>
|
"last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
|
||||||
</div>
|
platform.platform === "desktop",
|
||||||
</Show>
|
|
||||||
<Show when={historyMore()}>
|
|
||||||
<div class="w-full flex justify-center">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="large"
|
|
||||||
class="text-12-medium opacity-50"
|
|
||||||
disabled={historyLoading()}
|
|
||||||
onClick={() => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
setStore("turnStart", 0)
|
|
||||||
sync.session.history.loadMore(id)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{historyLoading() ? "Loading earlier messages..." : "Load earlier messages"}
|
<SessionTurn
|
||||||
</Button>
|
sessionID={params.id!}
|
||||||
</div>
|
messageID={message.id}
|
||||||
</Show>
|
lastUserMessageID={lastUserMessage()?.id}
|
||||||
<For each={renderedUserMessages()}>
|
stepsExpanded={store.expanded[message.id] ?? false}
|
||||||
{(message) => {
|
onStepsExpandedToggle={() =>
|
||||||
if (import.meta.env.DEV) {
|
setStore("expanded", message.id, (open: boolean | undefined) => !open)
|
||||||
onMount(() => {
|
}
|
||||||
const id = params.id
|
classes={{
|
||||||
if (!id) return
|
root: "min-w-0 w-full relative",
|
||||||
navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" })
|
content:
|
||||||
})
|
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||||
}
|
container:
|
||||||
|
"px-4 md:px-6 " +
|
||||||
return (
|
(!showTabs()
|
||||||
<div
|
? "md:max-w-200 md:mx-auto"
|
||||||
id={anchor(message.id)}
|
: visibleUserMessages().length > 1
|
||||||
data-message-id={message.id}
|
? "md:pr-6 md:pl-18"
|
||||||
classList={{
|
: ""),
|
||||||
"min-w-0 w-full max-w-full": true,
|
|
||||||
"last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
|
|
||||||
platform.platform !== "desktop",
|
|
||||||
"last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
|
|
||||||
platform.platform === "desktop",
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<SessionTurn
|
</div>
|
||||||
sessionID={params.id!}
|
)}
|
||||||
messageID={message.id}
|
|
||||||
lastUserMessageID={lastUserMessage()?.id}
|
|
||||||
stepsExpanded={store.expanded[message.id] ?? false}
|
|
||||||
onStepsExpandedToggle={() =>
|
|
||||||
setStore("expanded", message.id, (open: boolean | undefined) => !open)
|
|
||||||
}
|
|
||||||
classes={{
|
|
||||||
root: "min-w-0 w-full relative",
|
|
||||||
content:
|
|
||||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
|
||||||
container:
|
|
||||||
"px-4 md:px-6 " +
|
|
||||||
(!showTabs()
|
|
||||||
? "md:max-w-200 md:mx-auto"
|
|
||||||
: visibleUserMessages().length > 1
|
|
||||||
? "md:pr-6 md:pl-18"
|
|
||||||
: ""),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1221,22 +944,13 @@ export default function Page() {
|
|||||||
"md:max-w-200": !showTabs(),
|
"md:max-w-200": !showTabs(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show
|
<PromptInput
|
||||||
when={prompt.ready()}
|
ref={(el) => {
|
||||||
fallback={
|
inputRef = el
|
||||||
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
|
}}
|
||||||
{handoff.prompt || "Loading prompt..."}
|
newSessionWorktree={newSessionWorktree()}
|
||||||
</div>
|
onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
|
||||||
}
|
/>
|
||||||
>
|
|
||||||
<PromptInput
|
|
||||||
ref={(el) => {
|
|
||||||
inputRef = el
|
|
||||||
}}
|
|
||||||
newSessionWorktree={newSessionWorktree()}
|
|
||||||
onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1320,40 +1034,31 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<Show when={reviewTab()}>
|
<Show when={reviewTab()}>
|
||||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<Show when={activeTab() === "review"}>
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
<SessionReviewTab
|
||||||
<Show
|
diffs={diffs}
|
||||||
when={diffsReady()}
|
view={view}
|
||||||
fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>}
|
diffStyle={layout.review.diffStyle()}
|
||||||
>
|
onDiffStyleChange={layout.review.setDiffStyle}
|
||||||
<SessionReviewTab
|
onViewFile={(path) => {
|
||||||
diffs={diffs}
|
const value = file.tab(path)
|
||||||
view={view}
|
tabs().open(value)
|
||||||
diffStyle={layout.review.diffStyle()}
|
file.load(path)
|
||||||
onDiffStyleChange={layout.review.setDiffStyle}
|
}}
|
||||||
onViewFile={(path) => {
|
/>
|
||||||
const value = file.tab(path)
|
</div>
|
||||||
tabs().open(value)
|
|
||||||
file.load(path)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={contextOpen()}>
|
<Show when={contextOpen()}>
|
||||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<Show when={activeTab() === "context"}>
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
<SessionContextTab
|
||||||
<SessionContextTab
|
messages={messages}
|
||||||
messages={messages}
|
visibleUserMessages={visibleUserMessages}
|
||||||
visibleUserMessages={visibleUserMessages}
|
view={view}
|
||||||
view={view}
|
info={info}
|
||||||
info={info}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Show>
|
</Show>
|
||||||
<For each={openedTabs()}>
|
<For each={openedTabs()}>
|
||||||
@@ -1402,8 +1107,7 @@ export default function Page() {
|
|||||||
const selectedLines = createMemo(() => {
|
const selectedLines = createMemo(() => {
|
||||||
const p = path()
|
const p = path()
|
||||||
if (!p) return null
|
if (!p) return null
|
||||||
if (file.ready()) return file.selectedLines(p) ?? null
|
return file.selectedLines(p) ?? null
|
||||||
return handoff.files[p] ?? null
|
|
||||||
})
|
})
|
||||||
const selection = createMemo(() => {
|
const selection = createMemo(() => {
|
||||||
const range = selectedLines()
|
const range = selectedLines()
|
||||||
@@ -1500,63 +1204,37 @@ export default function Page() {
|
|||||||
}}
|
}}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
<Show when={activeTab() === tab}>
|
<Show when={selection()}>
|
||||||
<Show when={selection()}>
|
{(sel) => (
|
||||||
{(sel) => (
|
<div class="hidden sticky top-0 z-10 px-6 py-2 _flex justify-end bg-background-base border-b border-border-weak-base">
|
||||||
<div class="hidden sticky top-0 z-10 px-6 py-2 _flex justify-end bg-background-base border-b border-border-weak-base">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-strong hover:bg-surface-raised-base-hover"
|
||||||
class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-strong hover:bg-surface-raised-base-hover"
|
onClick={() => {
|
||||||
onClick={() => {
|
const p = path()
|
||||||
const p = path()
|
if (!p) return
|
||||||
if (!p) return
|
prompt.context.add({ type: "file", path: p, selection: sel() })
|
||||||
prompt.context.add({ type: "file", path: p, selection: sel() })
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Icon name="plus-small" size="small" />
|
||||||
<Icon name="plus-small" size="small" />
|
<span>Add {selectionLabel()} to context</span>
|
||||||
<span>Add {selectionLabel()} to context</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Show>
|
||||||
</Show>
|
<Switch>
|
||||||
<Switch>
|
<Match when={state()?.loaded && isImage()}>
|
||||||
<Match when={state()?.loaded && isImage()}>
|
<div class="px-6 py-4 pb-40">
|
||||||
<div class="px-6 py-4 pb-40">
|
<img src={imageDataUrl()} alt={path()} class="max-w-full" />
|
||||||
<img src={imageDataUrl()} alt={path()} class="max-w-full" />
|
</div>
|
||||||
</div>
|
</Match>
|
||||||
</Match>
|
<Match when={state()?.loaded && isSvg()}>
|
||||||
<Match when={state()?.loaded && isSvg()}>
|
<div class="flex flex-col gap-4 px-6 py-4">
|
||||||
<div class="flex flex-col gap-4 px-6 py-4">
|
|
||||||
<Dynamic
|
|
||||||
component={codeComponent}
|
|
||||||
file={{
|
|
||||||
name: path() ?? "",
|
|
||||||
contents: svgContent() ?? "",
|
|
||||||
cacheKey: cacheKey(),
|
|
||||||
}}
|
|
||||||
enableLineSelection
|
|
||||||
selectedLines={selectedLines()}
|
|
||||||
onLineSelected={(range: SelectedLineRange | null) => {
|
|
||||||
const p = path()
|
|
||||||
if (!p) return
|
|
||||||
file.setSelectedLines(p, range)
|
|
||||||
}}
|
|
||||||
overflow="scroll"
|
|
||||||
class="select-text"
|
|
||||||
/>
|
|
||||||
<Show when={svgPreviewUrl()}>
|
|
||||||
<div class="flex justify-center pb-40">
|
|
||||||
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={state()?.loaded}>
|
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={codeComponent}
|
component={codeComponent}
|
||||||
file={{
|
file={{
|
||||||
name: path() ?? "",
|
name: path() ?? "",
|
||||||
contents: contents(),
|
contents: svgContent() ?? "",
|
||||||
cacheKey: cacheKey(),
|
cacheKey: cacheKey(),
|
||||||
}}
|
}}
|
||||||
enableLineSelection
|
enableLineSelection
|
||||||
@@ -1567,17 +1245,41 @@ export default function Page() {
|
|||||||
file.setSelectedLines(p, range)
|
file.setSelectedLines(p, range)
|
||||||
}}
|
}}
|
||||||
overflow="scroll"
|
overflow="scroll"
|
||||||
class="select-text pb-40"
|
class="select-text"
|
||||||
/>
|
/>
|
||||||
</Match>
|
<Show when={svgPreviewUrl()}>
|
||||||
<Match when={state()?.loading}>
|
<div class="flex justify-center pb-40">
|
||||||
<div class="px-6 py-4 text-text-weak">Loading...</div>
|
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
|
||||||
</Match>
|
</div>
|
||||||
<Match when={state()?.error}>
|
</Show>
|
||||||
{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
<Match when={state()?.loaded}>
|
||||||
</Show>
|
<Dynamic
|
||||||
|
component={codeComponent}
|
||||||
|
file={{
|
||||||
|
name: path() ?? "",
|
||||||
|
contents: contents(),
|
||||||
|
cacheKey: cacheKey(),
|
||||||
|
}}
|
||||||
|
enableLineSelection
|
||||||
|
selectedLines={selectedLines()}
|
||||||
|
onLineSelected={(range: SelectedLineRange | null) => {
|
||||||
|
const p = path()
|
||||||
|
if (!p) return
|
||||||
|
file.setSelectedLines(p, range)
|
||||||
|
}}
|
||||||
|
overflow="scroll"
|
||||||
|
class="select-text pb-40"
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={state()?.loading}>
|
||||||
|
<div class="px-6 py-4 text-text-weak">Loading...</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={state()?.error}>
|
||||||
|
{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -1614,74 +1316,54 @@ export default function Page() {
|
|||||||
onResize={layout.terminal.resize}
|
onResize={layout.terminal.resize}
|
||||||
onCollapse={layout.terminal.close}
|
onCollapse={layout.terminal.close}
|
||||||
/>
|
/>
|
||||||
<Show
|
<DragDropProvider
|
||||||
when={terminal.ready()}
|
onDragStart={handleTerminalDragStart}
|
||||||
fallback={
|
onDragEnd={handleTerminalDragEnd}
|
||||||
<div class="flex flex-col h-full pointer-events-none">
|
onDragOver={handleTerminalDragOver}
|
||||||
<div class="h-10 flex items-center gap-2 px-2 border-b border-border-weak-base bg-background-stronger overflow-hidden">
|
collisionDetector={closestCenter}
|
||||||
<For each={handoff.terminals}>
|
|
||||||
{(title) => (
|
|
||||||
<div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<div class="flex-1" />
|
|
||||||
<div class="text-text-weak pr-2">Loading...</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 flex items-center justify-center text-text-weak">Loading terminal...</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DragDropProvider
|
<DragDropSensors />
|
||||||
onDragStart={handleTerminalDragStart}
|
<ConstrainDragYAxis />
|
||||||
onDragEnd={handleTerminalDragEnd}
|
<Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
|
||||||
onDragOver={handleTerminalDragOver}
|
<Tabs.List class="h-10">
|
||||||
collisionDetector={closestCenter}
|
<SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
|
||||||
>
|
<For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
|
||||||
<DragDropSensors />
|
</SortableProvider>
|
||||||
<ConstrainDragYAxis />
|
<div class="h-full flex items-center justify-center">
|
||||||
<Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
|
<TooltipKeybind
|
||||||
<Tabs.List class="h-10">
|
title="New terminal"
|
||||||
<SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
|
keybind={command.keybind("terminal.new")}
|
||||||
<For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
|
class="flex items-center"
|
||||||
</SortableProvider>
|
>
|
||||||
<div class="h-full flex items-center justify-center">
|
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
|
||||||
<TooltipKeybind
|
</TooltipKeybind>
|
||||||
title="New terminal"
|
</div>
|
||||||
keybind={command.keybind("terminal.new")}
|
</Tabs.List>
|
||||||
class="flex items-center"
|
<For each={terminal.all()}>
|
||||||
>
|
{(pty) => (
|
||||||
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
|
<Tabs.Content value={pty.id}>
|
||||||
</TooltipKeybind>
|
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
|
||||||
</div>
|
</Tabs.Content>
|
||||||
</Tabs.List>
|
)}
|
||||||
<For each={terminal.all()}>
|
</For>
|
||||||
{(pty) => (
|
</Tabs>
|
||||||
<Tabs.Content value={pty.id}>
|
<DragOverlay>
|
||||||
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
|
<Show when={store.activeTerminalDraggable}>
|
||||||
</Tabs.Content>
|
{(draggedId) => {
|
||||||
)}
|
const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
|
||||||
</For>
|
return (
|
||||||
</Tabs>
|
<Show when={pty()}>
|
||||||
<DragOverlay>
|
{(t) => (
|
||||||
<Show when={store.activeTerminalDraggable}>
|
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
||||||
{(draggedId) => {
|
{t().title}
|
||||||
const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
|
</div>
|
||||||
return (
|
)}
|
||||||
<Show when={pty()}>
|
</Show>
|
||||||
{(t) => (
|
)
|
||||||
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
}}
|
||||||
{t().title}
|
</Show>
|
||||||
</div>
|
</DragOverlay>
|
||||||
)}
|
</DragDropProvider>
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Show>
|
|
||||||
</DragOverlay>
|
|
||||||
</DragDropProvider>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
type Nav = {
|
|
||||||
id: string
|
|
||||||
dir?: string
|
|
||||||
from?: string
|
|
||||||
to: string
|
|
||||||
trigger?: string
|
|
||||||
start: number
|
|
||||||
marks: Record<string, number>
|
|
||||||
logged: boolean
|
|
||||||
timer?: ReturnType<typeof setTimeout>
|
|
||||||
}
|
|
||||||
|
|
||||||
const dev = import.meta.env.DEV
|
|
||||||
|
|
||||||
const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
|
|
||||||
|
|
||||||
const now = () => performance.now()
|
|
||||||
|
|
||||||
const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)
|
|
||||||
|
|
||||||
const navs = new Map<string, Nav>()
|
|
||||||
const pending = new Map<string, string>()
|
|
||||||
const active = new Map<string, string>()
|
|
||||||
|
|
||||||
const required = [
|
|
||||||
"session:params",
|
|
||||||
"session:data-ready",
|
|
||||||
"session:first-turn-mounted",
|
|
||||||
"storage:prompt-ready",
|
|
||||||
"storage:terminal-ready",
|
|
||||||
"storage:file-view-ready",
|
|
||||||
]
|
|
||||||
|
|
||||||
function flush(id: string, reason: "complete" | "timeout") {
|
|
||||||
if (!dev) return
|
|
||||||
const nav = navs.get(id)
|
|
||||||
if (!nav) return
|
|
||||||
if (nav.logged) return
|
|
||||||
|
|
||||||
nav.logged = true
|
|
||||||
if (nav.timer) clearTimeout(nav.timer)
|
|
||||||
|
|
||||||
const baseName = nav.marks["navigate:start"] !== undefined ? "navigate:start" : "session:params"
|
|
||||||
const base = nav.marks[baseName] ?? nav.start
|
|
||||||
|
|
||||||
const ms = Object.fromEntries(
|
|
||||||
Object.entries(nav.marks)
|
|
||||||
.slice()
|
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
|
||||||
.map(([name, t]) => [name, Math.round((t - base) * 100) / 100]),
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"perf.session-nav " +
|
|
||||||
JSON.stringify({
|
|
||||||
type: "perf.session-nav.v0",
|
|
||||||
id: nav.id,
|
|
||||||
dir: nav.dir,
|
|
||||||
from: nav.from,
|
|
||||||
to: nav.to,
|
|
||||||
trigger: nav.trigger,
|
|
||||||
base: baseName,
|
|
||||||
reason,
|
|
||||||
ms,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
navs.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeFlush(id: string) {
|
|
||||||
if (!dev) return
|
|
||||||
const nav = navs.get(id)
|
|
||||||
if (!nav) return
|
|
||||||
if (nav.logged) return
|
|
||||||
if (!required.every((name) => nav.marks[name] !== undefined)) return
|
|
||||||
flush(id, "complete")
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
|
|
||||||
const existing = navs.get(id)
|
|
||||||
if (existing) return existing
|
|
||||||
|
|
||||||
const nav: Nav = {
|
|
||||||
...data,
|
|
||||||
marks: {},
|
|
||||||
logged: false,
|
|
||||||
}
|
|
||||||
nav.timer = setTimeout(() => flush(id, "timeout"), 5000)
|
|
||||||
navs.set(id, nav)
|
|
||||||
return nav
|
|
||||||
}
|
|
||||||
|
|
||||||
export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
|
|
||||||
if (!dev) return
|
|
||||||
|
|
||||||
const id = uid()
|
|
||||||
const start = now()
|
|
||||||
const nav = ensure(id, { ...input, id, start })
|
|
||||||
nav.marks["navigate:start"] = start
|
|
||||||
|
|
||||||
pending.set(key(input.dir, input.to), id)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
export function navParams(input: { dir?: string; from?: string; to: string }) {
|
|
||||||
if (!dev) return
|
|
||||||
|
|
||||||
const k = key(input.dir, input.to)
|
|
||||||
const pendingId = pending.get(k)
|
|
||||||
if (pendingId) pending.delete(k)
|
|
||||||
const id = pendingId ?? uid()
|
|
||||||
|
|
||||||
const start = now()
|
|
||||||
const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
|
|
||||||
nav.marks["session:params"] = start
|
|
||||||
|
|
||||||
active.set(k, id)
|
|
||||||
maybeFlush(id)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
export function navMark(input: { dir?: string; to: string; name: string }) {
|
|
||||||
if (!dev) return
|
|
||||||
|
|
||||||
const id = active.get(key(input.dir, input.to))
|
|
||||||
if (!id) return
|
|
||||||
|
|
||||||
const nav = navs.get(id)
|
|
||||||
if (!nav) return
|
|
||||||
if (nav.marks[input.name] !== undefined) return
|
|
||||||
|
|
||||||
nav.marks[input.name] = now()
|
|
||||||
maybeFlush(id)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use tauri::{
|
|||||||
};
|
};
|
||||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
use tokio::net::TcpSocket;
|
use tokio::net::TcpSocket;
|
||||||
|
|
||||||
use crate::window_customizer::PinchZoomDisablePlugin;
|
use crate::window_customizer::PinchZoomDisablePlugin;
|
||||||
@@ -45,6 +46,65 @@ impl ServerState {
|
|||||||
struct LogState(Arc<Mutex<VecDeque<String>>>);
|
struct LogState(Arc<Mutex<VecDeque<String>>>);
|
||||||
|
|
||||||
const MAX_LOG_ENTRIES: usize = 200;
|
const MAX_LOG_ENTRIES: usize = 200;
|
||||||
|
const GLOBAL_STORAGE: &str = "opencode.global.dat";
|
||||||
|
|
||||||
|
/// Check if a URL's origin matches any configured server in the store.
|
||||||
|
/// Returns true if the URL should be allowed for internal navigation.
|
||||||
|
fn is_allowed_server(app: &AppHandle, url: &tauri::Url) -> bool {
|
||||||
|
// Always allow localhost and 127.0.0.1
|
||||||
|
if let Some(host) = url.host_str() {
|
||||||
|
if host == "localhost" || host == "127.0.0.1" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read the server list from the store
|
||||||
|
let Ok(store) = app.store(GLOBAL_STORAGE) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(server_data) = store.get("server") else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the server list from the stored JSON
|
||||||
|
let Some(list) = server_data.get("list").and_then(|v| v.as_array()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the origin of the navigation URL (scheme + host + port)
|
||||||
|
let url_origin = format!(
|
||||||
|
"{}://{}{}",
|
||||||
|
url.scheme(),
|
||||||
|
url.host_str().unwrap_or(""),
|
||||||
|
url.port().map(|p| format!(":{}", p)).unwrap_or_default()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if any configured server matches the URL's origin
|
||||||
|
for server in list {
|
||||||
|
let Some(server_url) = server.as_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the server URL to extract its origin
|
||||||
|
let Ok(parsed) = tauri::Url::parse(server_url) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let server_origin = format!(
|
||||||
|
"{}://{}{}",
|
||||||
|
parsed.scheme(),
|
||||||
|
parsed.host_str().unwrap_or(""),
|
||||||
|
parsed.port().map(|p| format!(":{}", p)).unwrap_or_default()
|
||||||
|
);
|
||||||
|
|
||||||
|
if url_origin == server_origin {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn kill_sidecar(app: AppHandle) {
|
fn kill_sidecar(app: AppHandle) {
|
||||||
@@ -236,6 +296,7 @@ pub fn run() {
|
|||||||
.unwrap_or(LogicalSize::new(1920, 1080));
|
.unwrap_or(LogicalSize::new(1920, 1080));
|
||||||
|
|
||||||
// Create window immediately with serverReady = false
|
// Create window immediately with serverReady = false
|
||||||
|
let app_for_nav = app.clone();
|
||||||
let mut window_builder =
|
let mut window_builder =
|
||||||
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
||||||
.title("OpenCode")
|
.title("OpenCode")
|
||||||
@@ -243,6 +304,22 @@ pub fn run() {
|
|||||||
.decorations(true)
|
.decorations(true)
|
||||||
.zoom_hotkeys_enabled(true)
|
.zoom_hotkeys_enabled(true)
|
||||||
.disable_drag_drop_handler()
|
.disable_drag_drop_handler()
|
||||||
|
.on_navigation(move |url| {
|
||||||
|
// Allow internal navigation (tauri:// scheme)
|
||||||
|
if url.scheme() == "tauri" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Allow navigation to configured servers (localhost, 127.0.0.1, or remote)
|
||||||
|
if is_allowed_server(&app_for_nav, url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Open external http/https URLs in default browser
|
||||||
|
if url.scheme() == "http" || url.scheme() == "https" {
|
||||||
|
let _ = app_for_nav.shell().open(url.as_str(), None);
|
||||||
|
return false; // Cancel internal navigation
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
.initialization_script(format!(
|
.initialization_script(format!(
|
||||||
r#"
|
r#"
|
||||||
window.__OPENCODE__ ??= {{}};
|
window.__OPENCODE__ ??= {{}};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.1.8"
|
version = "1.1.7"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.70",
|
"@opentui/core": "0.1.69",
|
||||||
"@opentui/solid": "0.1.70",
|
"@opentui/solid": "0.1.69",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export type AutocompleteRef = {
|
|||||||
|
|
||||||
export type AutocompleteOption = {
|
export type AutocompleteOption = {
|
||||||
display: string
|
display: string
|
||||||
value?: string
|
|
||||||
aliases?: string[]
|
aliases?: string[]
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
description?: string
|
description?: string
|
||||||
@@ -222,7 +221,6 @@ export function Autocomplete(props: {
|
|||||||
const isDir = item.endsWith("/")
|
const isDir = item.endsWith("/")
|
||||||
return {
|
return {
|
||||||
display: Locale.truncateMiddle(filename, width),
|
display: Locale.truncateMiddle(filename, width),
|
||||||
value: filename,
|
|
||||||
isDirectory: isDir,
|
isDirectory: isDir,
|
||||||
path: item,
|
path: item,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
@@ -261,10 +259,8 @@ export function Autocomplete(props: {
|
|||||||
const width = props.anchor().width - 4
|
const width = props.anchor().width - 4
|
||||||
|
|
||||||
for (const res of Object.values(sync.data.mcp_resource)) {
|
for (const res of Object.values(sync.data.mcp_resource)) {
|
||||||
const text = `${res.name} (${res.uri})`
|
|
||||||
options.push({
|
options.push({
|
||||||
display: Locale.truncateMiddle(text, width),
|
display: Locale.truncateMiddle(`${res.name} (${res.uri})`, width),
|
||||||
value: text,
|
|
||||||
description: res.description,
|
description: res.description,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
insertPart(res.name, {
|
insertPart(res.name, {
|
||||||
@@ -489,11 +485,7 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
|
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
|
||||||
keys: [
|
keys: [(obj) => removeLineRange(obj.display.trimEnd()), "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||||
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
|
||||||
"description",
|
|
||||||
(obj) => obj.aliases?.join(" ") ?? "",
|
|
||||||
],
|
|
||||||
limit: 10,
|
limit: 10,
|
||||||
scoreFn: (objResults) => {
|
scoreFn: (objResults) => {
|
||||||
const displayResult = objResults[0]
|
const displayResult = objResults[0]
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ function init() {
|
|||||||
current.onClose?.()
|
current.onClose?.()
|
||||||
setStore("stack", store.stack.slice(0, -1))
|
setStore("stack", store.stack.slice(0, -1))
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
evt.stopPropagation()
|
|
||||||
refocus()
|
refocus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export const rustfmt: Info = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const cargofmt: Info = {
|
export const cargofmt: Info = {
|
||||||
name: "cargofmt",
|
name: "cargo fmt",
|
||||||
command: ["cargo", "fmt", "--", "$FILE"],
|
command: ["cargo", "fmt", "--", "$FILE"],
|
||||||
extensions: [".rs"],
|
extensions: [".rs"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1249
-281
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,53 +1,19 @@
|
|||||||
import { useMarked } from "../context/marked"
|
import { useMarked } from "../context/marked"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
|
||||||
import { ComponentProps, createResource, splitProps } from "solid-js"
|
import { ComponentProps, createResource, splitProps } from "solid-js"
|
||||||
|
|
||||||
type Entry = {
|
|
||||||
hash: string
|
|
||||||
html: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const max = 200
|
|
||||||
const cache = new Map<string, Entry>()
|
|
||||||
|
|
||||||
function touch(key: string, value: Entry) {
|
|
||||||
cache.delete(key)
|
|
||||||
cache.set(key, value)
|
|
||||||
|
|
||||||
if (cache.size <= max) return
|
|
||||||
|
|
||||||
const first = cache.keys().next().value
|
|
||||||
if (!first) return
|
|
||||||
cache.delete(first)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Markdown(
|
export function Markdown(
|
||||||
props: ComponentProps<"div"> & {
|
props: ComponentProps<"div"> & {
|
||||||
text: string
|
text: string
|
||||||
cacheKey?: string
|
|
||||||
class?: string
|
class?: string
|
||||||
classList?: Record<string, boolean>
|
classList?: Record<string, boolean>
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const [local, others] = splitProps(props, ["text", "cacheKey", "class", "classList"])
|
const [local, others] = splitProps(props, ["text", "class", "classList"])
|
||||||
const marked = useMarked()
|
const marked = useMarked()
|
||||||
const [html] = createResource(
|
const [html] = createResource(
|
||||||
() => local.text,
|
() => local.text,
|
||||||
async (markdown) => {
|
async (markdown) => {
|
||||||
const hash = checksum(markdown)
|
return marked.parse(markdown)
|
||||||
const key = local.cacheKey ?? hash
|
|
||||||
|
|
||||||
if (key && hash) {
|
|
||||||
const cached = cache.get(key)
|
|
||||||
if (cached && cached.hash === hash) {
|
|
||||||
touch(key, cached)
|
|
||||||
return cached.html
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = await marked.parse(markdown)
|
|
||||||
if (key && hash) touch(key, { hash, html: next })
|
|
||||||
return next
|
|
||||||
},
|
},
|
||||||
{ initialValue: "" },
|
{ initialValue: "" },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
|||||||
return (
|
return (
|
||||||
<Show when={throttledText()}>
|
<Show when={throttledText()}>
|
||||||
<div data-component="text-part">
|
<div data-component="text-part">
|
||||||
<Markdown text={throttledText()} cacheKey={part.id} />
|
<Markdown text={throttledText()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -580,7 +580,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
|||||||
return (
|
return (
|
||||||
<Show when={throttledText()}>
|
<Show when={throttledText()}>
|
||||||
<div data-component="reasoning-part">
|
<div data-component="reasoning-part">
|
||||||
<Markdown text={throttledText()} cacheKey={part.id} />
|
<Markdown text={throttledText()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -350,31 +350,15 @@ export function SessionTurn(
|
|||||||
onUserInteracted: props.onUserInteracted,
|
onUserInteracted: props.onUserInteracted,
|
||||||
})
|
})
|
||||||
|
|
||||||
const diffInit = 20
|
|
||||||
const diffBatch = 20
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
stickyTitleRef: undefined as HTMLDivElement | undefined,
|
stickyTitleRef: undefined as HTMLDivElement | undefined,
|
||||||
stickyTriggerRef: undefined as HTMLDivElement | undefined,
|
stickyTriggerRef: undefined as HTMLDivElement | undefined,
|
||||||
stickyHeaderHeight: 0,
|
stickyHeaderHeight: 0,
|
||||||
retrySeconds: 0,
|
retrySeconds: 0,
|
||||||
diffsOpen: [] as string[],
|
|
||||||
diffLimit: diffInit,
|
|
||||||
status: rawStatus(),
|
status: rawStatus(),
|
||||||
duration: duration(),
|
duration: duration(),
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => message()?.id,
|
|
||||||
() => {
|
|
||||||
setStore("diffsOpen", [])
|
|
||||||
setStore("diffLimit", diffInit)
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const r = retry()
|
const r = retry()
|
||||||
if (!r) {
|
if (!r) {
|
||||||
@@ -558,23 +542,10 @@ export function SessionTurn(
|
|||||||
<div data-slot="session-turn-summary-section">
|
<div data-slot="session-turn-summary-section">
|
||||||
<div data-slot="session-turn-summary-header">
|
<div data-slot="session-turn-summary-header">
|
||||||
<h2 data-slot="session-turn-summary-title">Response</h2>
|
<h2 data-slot="session-turn-summary-title">Response</h2>
|
||||||
<Markdown
|
<Markdown data-slot="session-turn-markdown" data-diffs={hasDiffs()} text={response() ?? ""} />
|
||||||
data-slot="session-turn-markdown"
|
|
||||||
data-diffs={hasDiffs()}
|
|
||||||
text={response() ?? ""}
|
|
||||||
cacheKey={responsePartId()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Accordion
|
<Accordion data-slot="session-turn-accordion" multiple>
|
||||||
data-slot="session-turn-accordion"
|
<For each={msg().summary?.diffs ?? []}>
|
||||||
multiple
|
|
||||||
value={store.diffsOpen}
|
|
||||||
onChange={(value) => {
|
|
||||||
if (!Array.isArray(value)) return
|
|
||||||
setStore("diffsOpen", value)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<For each={(msg().summary?.diffs ?? []).slice(0, store.diffLimit)}>
|
|
||||||
{(diff) => (
|
{(diff) => (
|
||||||
<Accordion.Item value={diff.file}>
|
<Accordion.Item value={diff.file}>
|
||||||
<StickyAccordionHeader>
|
<StickyAccordionHeader>
|
||||||
@@ -602,41 +573,22 @@ export function SessionTurn(
|
|||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
</StickyAccordionHeader>
|
</StickyAccordionHeader>
|
||||||
<Accordion.Content data-slot="session-turn-accordion-content">
|
<Accordion.Content data-slot="session-turn-accordion-content">
|
||||||
<Show when={store.diffsOpen.includes(diff.file!)}>
|
<Dynamic
|
||||||
<Dynamic
|
component={diffComponent}
|
||||||
component={diffComponent}
|
before={{
|
||||||
before={{
|
name: diff.file!,
|
||||||
name: diff.file!,
|
contents: diff.before!,
|
||||||
contents: diff.before!,
|
}}
|
||||||
}}
|
after={{
|
||||||
after={{
|
name: diff.file!,
|
||||||
name: diff.file!,
|
contents: diff.after!,
|
||||||
contents: diff.after!,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Show when={(msg().summary?.diffs?.length ?? 0) > store.diffLimit}>
|
|
||||||
<Button
|
|
||||||
data-slot="session-turn-accordion-more"
|
|
||||||
variant="ghost"
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
const total = msg().summary?.diffs?.length ?? 0
|
|
||||||
setStore("diffLimit", (limit) => {
|
|
||||||
const next = limit + diffBatch
|
|
||||||
if (next > total) return total
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Show more changes ({(msg().summary?.diffs?.length ?? 0) - store.diffLimit})
|
|
||||||
</Button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={error() && !props.stepsExpanded}>
|
<Show when={error() && !props.stepsExpanded}>
|
||||||
|
|||||||
@@ -3,19 +3,12 @@ import { createContext, createMemo, Show, useContext, type ParentProps, type Acc
|
|||||||
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
|
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
|
||||||
name: string
|
name: string
|
||||||
init: ((input: Props) => T) | (() => T)
|
init: ((input: Props) => T) | (() => T)
|
||||||
gate?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const ctx = createContext<T>()
|
const ctx = createContext<T>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: (props: ParentProps<Props>) => {
|
provider: (props: ParentProps<Props>) => {
|
||||||
const init = input.init(props)
|
const init = input.init(props)
|
||||||
const gate = input.gate ?? true
|
|
||||||
|
|
||||||
if (!gate) {
|
|
||||||
return <ctx.Provider value={init}>{props.children}</ctx.Provider>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access init.ready inside the memo to make it reactive for getter properties
|
// Access init.ready inside the memo to make it reactive for getter properties
|
||||||
const isReady = createMemo(() => {
|
const isReady = createMemo(() => {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import ayuThemeJson from "./themes/ayu.json"
|
|||||||
import oneDarkProThemeJson from "./themes/onedarkpro.json"
|
import oneDarkProThemeJson from "./themes/onedarkpro.json"
|
||||||
import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
|
import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
|
||||||
import nightowlThemeJson from "./themes/nightowl.json"
|
import nightowlThemeJson from "./themes/nightowl.json"
|
||||||
import vesperThemeJson from "./themes/vesper.json"
|
|
||||||
|
|
||||||
export const oc1Theme = oc1ThemeJson as DesktopTheme
|
export const oc1Theme = oc1ThemeJson as DesktopTheme
|
||||||
export const tokyonightTheme = tokyoThemeJson as DesktopTheme
|
export const tokyonightTheme = tokyoThemeJson as DesktopTheme
|
||||||
@@ -23,7 +22,6 @@ export const ayuTheme = ayuThemeJson as DesktopTheme
|
|||||||
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
|
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
|
||||||
export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
|
export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
|
||||||
export const nightowlTheme = nightowlThemeJson as DesktopTheme
|
export const nightowlTheme = nightowlThemeJson as DesktopTheme
|
||||||
export const vesperTheme = vesperThemeJson as DesktopTheme
|
|
||||||
|
|
||||||
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
||||||
"oc-1": oc1Theme,
|
"oc-1": oc1Theme,
|
||||||
@@ -37,5 +35,4 @@ export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
|||||||
onedarkpro: oneDarkProTheme,
|
onedarkpro: oneDarkProTheme,
|
||||||
shadesofpurple: shadesOfPurpleTheme,
|
shadesofpurple: shadesOfPurpleTheme,
|
||||||
nightowl: nightowlTheme,
|
nightowl: nightowlTheme,
|
||||||
vesper: vesperTheme,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,4 @@ export {
|
|||||||
oneDarkProTheme,
|
oneDarkProTheme,
|
||||||
shadesOfPurpleTheme,
|
shadesOfPurpleTheme,
|
||||||
nightowlTheme,
|
nightowlTheme,
|
||||||
vesperTheme,
|
|
||||||
} from "./default-themes"
|
} from "./default-themes"
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
|
||||||
"name": "Vesper",
|
|
||||||
"id": "vesper",
|
|
||||||
"light": {
|
|
||||||
"seeds": {
|
|
||||||
"neutral": "#F0F0F0",
|
|
||||||
"primary": "#FFC799",
|
|
||||||
"success": "#99FFE4",
|
|
||||||
"warning": "#FFC799",
|
|
||||||
"error": "#FF8080",
|
|
||||||
"info": "#FFC799",
|
|
||||||
"interactive": "#FFC799",
|
|
||||||
"diffAdd": "#99FFE4",
|
|
||||||
"diffDelete": "#FF8080"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"background-base": "#FFF",
|
|
||||||
"background-weak": "#F8F8F8",
|
|
||||||
"background-strong": "#F0F0F0",
|
|
||||||
"background-stronger": "#E8E8E8",
|
|
||||||
"border-weak-base": "#E8E8E8",
|
|
||||||
"border-weak-hover": "#E0E0E0",
|
|
||||||
"border-weak-active": "#D8D8D8",
|
|
||||||
"border-weak-selected": "#D0D0D0",
|
|
||||||
"border-weak-disabled": "#F0F0F0",
|
|
||||||
"border-weak-focus": "#D8D8D8",
|
|
||||||
"border-base": "#D0D0D0",
|
|
||||||
"border-hover": "#C8C8C8",
|
|
||||||
"border-active": "#C0C0C0",
|
|
||||||
"border-selected": "#B8B8B8",
|
|
||||||
"border-disabled": "#E8E8E8",
|
|
||||||
"border-focus": "#C0C0C0",
|
|
||||||
"border-strong-base": "#A0A0A0",
|
|
||||||
"border-strong-hover": "#989898",
|
|
||||||
"border-strong-active": "#909090",
|
|
||||||
"border-strong-selected": "#888888",
|
|
||||||
"border-strong-disabled": "#D0D0D0",
|
|
||||||
"border-strong-focus": "#909090",
|
|
||||||
"surface-diff-add-base": "#e8f5e8",
|
|
||||||
"surface-diff-delete-base": "#f5e8e8",
|
|
||||||
"surface-diff-hidden-base": "#F0F0F0",
|
|
||||||
"text-base": "#101010",
|
|
||||||
"text-weak": "#A0A0A0",
|
|
||||||
"text-strong": "#000000",
|
|
||||||
"syntax-string": "#99FFE4",
|
|
||||||
"syntax-primitive": "#FF8080",
|
|
||||||
"syntax-property": "#FFC799",
|
|
||||||
"syntax-type": "#FFC799",
|
|
||||||
"syntax-constant": "#A0A0A0",
|
|
||||||
"syntax-info": "#A0A0A0",
|
|
||||||
"markdown-heading": "#FFC799",
|
|
||||||
"markdown-text": "#101010",
|
|
||||||
"markdown-link": "#FFC799",
|
|
||||||
"markdown-link-text": "#A0A0A0",
|
|
||||||
"markdown-code": "#A0A0A0",
|
|
||||||
"markdown-block-quote": "#101010",
|
|
||||||
"markdown-emph": "#101010",
|
|
||||||
"markdown-strong": "#101010",
|
|
||||||
"markdown-horizontal-rule": "#65737E",
|
|
||||||
"markdown-list-item": "#101010",
|
|
||||||
"markdown-list-enumeration": "#101010",
|
|
||||||
"markdown-image": "#FFC799",
|
|
||||||
"markdown-image-text": "#A0A0A0",
|
|
||||||
"markdown-code-block": "#FFC799"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dark": {
|
|
||||||
"seeds": {
|
|
||||||
"neutral": "#101010",
|
|
||||||
"primary": "#FFC799",
|
|
||||||
"success": "#99FFE4",
|
|
||||||
"warning": "#FFC799",
|
|
||||||
"error": "#FF8080",
|
|
||||||
"info": "#FFC799",
|
|
||||||
"interactive": "#FFC799",
|
|
||||||
"diffAdd": "#99FFE4",
|
|
||||||
"diffDelete": "#FF8080"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"background-base": "#101010",
|
|
||||||
"background-weak": "#141414",
|
|
||||||
"background-strong": "#0C0C0C",
|
|
||||||
"background-stronger": "#080808",
|
|
||||||
"border-weak-base": "#1C1C1C",
|
|
||||||
"border-weak-hover": "#202020",
|
|
||||||
"border-weak-active": "#242424",
|
|
||||||
"border-weak-selected": "#282828",
|
|
||||||
"border-weak-disabled": "#141414",
|
|
||||||
"border-weak-focus": "#242424",
|
|
||||||
"border-base": "#282828",
|
|
||||||
"border-hover": "#303030",
|
|
||||||
"border-active": "#383838",
|
|
||||||
"border-selected": "#404040",
|
|
||||||
"border-disabled": "#181818",
|
|
||||||
"border-focus": "#383838",
|
|
||||||
"border-strong-base": "#505050",
|
|
||||||
"border-strong-hover": "#585858",
|
|
||||||
"border-strong-active": "#606060",
|
|
||||||
"border-strong-selected": "#686868",
|
|
||||||
"border-strong-disabled": "#202020",
|
|
||||||
"border-strong-focus": "#606060",
|
|
||||||
"surface-diff-add-base": "#0d2818",
|
|
||||||
"surface-diff-delete-base": "#281a1a",
|
|
||||||
"surface-diff-hidden-base": "#141414",
|
|
||||||
"text-base": "#FFF",
|
|
||||||
"text-weak": "#A0A0A0",
|
|
||||||
"text-strong": "#FFFFFF",
|
|
||||||
"syntax-string": "#99FFE4",
|
|
||||||
"syntax-primitive": "#FF8080",
|
|
||||||
"syntax-property": "#FFC799",
|
|
||||||
"syntax-type": "#FFC799",
|
|
||||||
"syntax-constant": "#A0A0A0",
|
|
||||||
"syntax-info": "#8b8b8b",
|
|
||||||
"markdown-heading": "#FFC799",
|
|
||||||
"markdown-text": "#FFF",
|
|
||||||
"markdown-link": "#FFC799",
|
|
||||||
"markdown-link-text": "#A0A0A0",
|
|
||||||
"markdown-code": "#A0A0A0",
|
|
||||||
"markdown-block-quote": "#FFF",
|
|
||||||
"markdown-emph": "#FFF",
|
|
||||||
"markdown-strong": "#FFF",
|
|
||||||
"markdown-horizontal-rule": "#65737E",
|
|
||||||
"markdown-list-item": "#FFF",
|
|
||||||
"markdown-list-enumeration": "#FFF",
|
|
||||||
"markdown-image": "#FFC799",
|
|
||||||
"markdown-image-text": "#A0A0A0",
|
|
||||||
"markdown-code-block": "#FFF"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export function getFilename(path: string | undefined) {
|
export function getFilename(path: string | undefined) {
|
||||||
if (!path) return ""
|
if (!path) return ""
|
||||||
const trimmed = path.replace(/[\/\\]+$/, "")
|
const trimmed = path.replace(/[\/]+$/, "")
|
||||||
const parts = trimmed.split(/[\/\\]/)
|
const parts = trimmed.split("/")
|
||||||
return parts[parts.length - 1] ?? ""
|
return parts[parts.length - 1] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ OpenCode comes with several built-in formatters for popular languages and framew
|
|||||||
| ktlint | .kt, .kts | `ktlint` command available |
|
| ktlint | .kt, .kts | `ktlint` command available |
|
||||||
| ruff | .py, .pyi | `ruff` command available with config |
|
| ruff | .py, .pyi | `ruff` command available with config |
|
||||||
| rustfmt | .rs | `rustfmt` command available |
|
| rustfmt | .rs | `rustfmt` command available |
|
||||||
| cargofmt | .rs | `cargo fmt` command available |
|
|
||||||
| uv | .py, .pyi | `uv` command available |
|
| uv | .py, .pyi | `uv` command available |
|
||||||
| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` command available |
|
| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` command available |
|
||||||
| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available |
|
| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "1.1.8",
|
"version": "1.1.7",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Reference in New Issue
Block a user