chore: merge dev into core database branch
This commit is contained in:
@@ -735,6 +735,7 @@
|
||||
"patchedDependencies": {
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-szOnLhAI4d3O4NaQvCRBufklmtRU8AR8p5LyFxjlTXo=",
|
||||
"aarch64-linux": "sha256-mfyL4Pswl6HeObPg+TMK1r7RjvLCMgDWYrFnfHkXGj4=",
|
||||
"aarch64-darwin": "sha256-XMK5IFCIxlSwH21Nqya8ZkAQ4Skp6GghukIl7NMp+so=",
|
||||
"x86_64-darwin": "sha256-W7nLMO6qhR32442yMebEJJgaZRNpqTbZ8BJhdqMK3S4="
|
||||
"x86_64-linux": "sha256-Nol27sqG6za6oR32MqZrmRxch0/XFeQ61C9Yqtxo7Hk=",
|
||||
"aarch64-linux": "sha256-+jfkGr4tuv/s/v890XaFTRqUxwA8H8KVMiys6sEXi/4=",
|
||||
"aarch64-darwin": "sha256-FYtKluNazzuQcrr2iHm7B2pJfYrAHkbcj4kWs2FtBgc=",
|
||||
"x86_64-darwin": "sha256-PpyWDkzb0p50US7Bs3m3A4PZ0JM3dccGgTKYN9JFYR0="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
import { expect, test, type Locator, type Page, type Route } from "@playwright/test"
|
||||
|
||||
const directory = "C:/OpenCode/TimelineStateRegression"
|
||||
const projectID = "proj_timeline_state_regression"
|
||||
const sessionID = "ses_timeline_state_regression"
|
||||
const userMessageID = "msg_user_regression"
|
||||
const assistantMessageID = "msg_assistant_regression"
|
||||
const editPartID = "prt_0001_edit"
|
||||
const textPartID = "prt_9999_text"
|
||||
const title = "Timeline collapse state regression"
|
||||
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
||||
|
||||
type EventPayload = {
|
||||
directory: string
|
||||
payload: Record<string, unknown>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__timelineDiffProbe: {
|
||||
reset: () => void
|
||||
shadowRoots: () => number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
info: {
|
||||
id: userMessageID,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: 1700000000000 },
|
||||
summary: { diffs: [] },
|
||||
agent: "build",
|
||||
model,
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: "prt_user_text",
|
||||
sessionID,
|
||||
messageID: userMessageID,
|
||||
type: "text",
|
||||
text: "Please edit the file.",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const editPart = {
|
||||
id: editPartID,
|
||||
sessionID,
|
||||
messageID: assistantMessageID,
|
||||
type: "tool",
|
||||
callID: "call_edit_regression",
|
||||
tool: "edit",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { filePath: "src/regression.ts" },
|
||||
output: "Edited src/regression.ts",
|
||||
title: "src/regression.ts",
|
||||
metadata: {
|
||||
filediff: {
|
||||
file: "src/regression.ts",
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
before: "export const value = 'before'\n",
|
||||
after: "export const value = 'after'\n",
|
||||
},
|
||||
diff: "diff --git a/src/regression.ts b/src/regression.ts\n-export const value = 'before'\n+export const value = 'after'\n",
|
||||
},
|
||||
time: { start: 1700000001000, end: 1700000002000 },
|
||||
},
|
||||
}
|
||||
|
||||
const streamedTextPart = {
|
||||
id: textPartID,
|
||||
sessionID,
|
||||
messageID: assistantMessageID,
|
||||
type: "text",
|
||||
text: "Streaming added a later assistant text part.",
|
||||
}
|
||||
|
||||
const assistantMessage = {
|
||||
info: {
|
||||
id: assistantMessageID,
|
||||
sessionID,
|
||||
role: "assistant",
|
||||
time: { created: 1700000001000 },
|
||||
parentID: userMessageID,
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
mode: "build",
|
||||
agent: "build",
|
||||
path: { cwd: directory, root: directory },
|
||||
cost: 0.01,
|
||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
variant: "max",
|
||||
},
|
||||
parts: [editPart],
|
||||
}
|
||||
|
||||
test.describe("regression: session timeline local row state", () => {
|
||||
test("keeps a manually collapsed tool collapsed when later assistant content streams", async ({ page }) => {
|
||||
const events: EventPayload[] = []
|
||||
await mockServer(page, events)
|
||||
await configurePage(page)
|
||||
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
||||
|
||||
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
|
||||
await expect(wrapper).toBeVisible()
|
||||
await expectExpanded(wrapper, true)
|
||||
|
||||
await wrapper.evaluate((element) => {
|
||||
;(element as HTMLElement).dataset.regressionMarker = "before-stream"
|
||||
})
|
||||
await wrapper.locator('[data-slot="collapsible-trigger"]').first().click()
|
||||
await expectExpanded(wrapper, false)
|
||||
|
||||
events.push({
|
||||
directory,
|
||||
payload: {
|
||||
type: "message.part.updated",
|
||||
properties: { part: streamedTextPart },
|
||||
},
|
||||
})
|
||||
|
||||
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
expect(await readToolState(page)).toEqual({
|
||||
expanded: false,
|
||||
row: "AssistantPart",
|
||||
streamedTextVisible: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("does not remount an edit diff when sibling parts or diff counts update", async ({ page }) => {
|
||||
const events: EventPayload[] = []
|
||||
await installDiffProbe(page)
|
||||
await mockServer(page, events)
|
||||
await configurePage(page)
|
||||
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
||||
|
||||
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
|
||||
await expect(wrapper).toBeVisible()
|
||||
await expect(wrapper.locator('[data-component="file"][data-mode="diff"]').first()).toBeVisible()
|
||||
await markDiffProbe(page)
|
||||
|
||||
events.push({
|
||||
directory,
|
||||
payload: {
|
||||
type: "message.part.updated",
|
||||
properties: { part: streamedTextPart },
|
||||
},
|
||||
})
|
||||
|
||||
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 })
|
||||
expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" })
|
||||
|
||||
await markDiffProbe(page)
|
||||
events.push({
|
||||
directory,
|
||||
payload: {
|
||||
type: "message.part.updated",
|
||||
properties: { part: editPartWithAdditions(2) },
|
||||
},
|
||||
})
|
||||
|
||||
await expect(wrapper.locator('[data-slot="diff-changes-additions"]').filter({ hasText: "+2" }).first()).toBeVisible(
|
||||
{ timeout: 10_000 },
|
||||
)
|
||||
expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" })
|
||||
})
|
||||
})
|
||||
|
||||
async function configurePage(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
"settings.v3",
|
||||
JSON.stringify({
|
||||
general: {
|
||||
editToolPartsExpanded: true,
|
||||
shellToolPartsExpanded: true,
|
||||
showReasoningSummaries: true,
|
||||
showSessionProgressBar: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function expectExpanded(locator: Locator, expected: boolean) {
|
||||
await expect.poll(() => locator.evaluate(readExpanded)).toBe(expected)
|
||||
}
|
||||
|
||||
async function readToolState(page: Page) {
|
||||
return page
|
||||
.locator(`[data-timeline-part-id="${editPartID}"]`)
|
||||
.first()
|
||||
.evaluate(
|
||||
(element, textPartID) => ({
|
||||
expanded: (() => {
|
||||
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
|
||||
const aria = trigger?.getAttribute("aria-expanded")
|
||||
if (aria === "true") return true
|
||||
if (aria === "false") return false
|
||||
|
||||
const root = element.querySelector('[data-component="collapsible"]')
|
||||
if (root?.hasAttribute("data-expanded")) return true
|
||||
if (root?.hasAttribute("data-closed")) return false
|
||||
|
||||
const content = element.querySelector<HTMLElement>('[data-slot="collapsible-content"]')
|
||||
return !!content && content.getBoundingClientRect().height > 0
|
||||
})(),
|
||||
row: element.closest("[data-timeline-row]")?.getAttribute("data-timeline-row"),
|
||||
streamedTextVisible: !!document.querySelector(`[data-timeline-part-id="${textPartID}"]`),
|
||||
}),
|
||||
textPartID,
|
||||
)
|
||||
}
|
||||
|
||||
async function installDiffProbe(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
let shadowRootCount = 0
|
||||
const attachShadow = Element.prototype.attachShadow
|
||||
Element.prototype.attachShadow = function (init) {
|
||||
shadowRootCount += 1
|
||||
return attachShadow.call(this, init)
|
||||
}
|
||||
window.__timelineDiffProbe = {
|
||||
reset: () => {
|
||||
shadowRootCount = 0
|
||||
},
|
||||
shadowRoots: () => shadowRootCount,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function markDiffProbe(page: Page) {
|
||||
await page
|
||||
.locator(`[data-timeline-part-id="${editPartID}"]`)
|
||||
.first()
|
||||
.evaluate((element) => {
|
||||
const tool = element as HTMLElement
|
||||
const file = tool.querySelector<HTMLElement>('[data-component="file"][data-mode="diff"]')
|
||||
if (!file) throw new Error("missing edit diff file")
|
||||
|
||||
tool.dataset.timelineProbe = "before"
|
||||
file.dataset.timelineProbe = "before"
|
||||
window.__timelineDiffProbe.reset()
|
||||
})
|
||||
}
|
||||
|
||||
async function readDiffProbe(page: Page) {
|
||||
return page
|
||||
.locator(`[data-timeline-part-id="${editPartID}"]`)
|
||||
.first()
|
||||
.evaluate((element) => {
|
||||
const tool = element as HTMLElement
|
||||
const file = tool.querySelector<HTMLElement>('[data-component="file"][data-mode="diff"]')
|
||||
return {
|
||||
fileMarker: file?.dataset.timelineProbe,
|
||||
shadowRoots: window.__timelineDiffProbe.shadowRoots(),
|
||||
toolMarker: tool.dataset.timelineProbe,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function editPartWithAdditions(additions: number) {
|
||||
return {
|
||||
...editPart,
|
||||
state: {
|
||||
...editPart.state,
|
||||
metadata: {
|
||||
...editPart.state.metadata,
|
||||
filediff: {
|
||||
...editPart.state.metadata.filediff,
|
||||
additions,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function readExpanded(element: Element) {
|
||||
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
|
||||
const aria = trigger?.getAttribute("aria-expanded")
|
||||
if (aria === "true") return true
|
||||
if (aria === "false") return false
|
||||
|
||||
const root = element.querySelector('[data-component="collapsible"]')
|
||||
if (root?.hasAttribute("data-expanded")) return true
|
||||
if (root?.hasAttribute("data-closed")) return false
|
||||
|
||||
const content = element.querySelector<HTMLElement>('[data-slot="collapsible-content"]')
|
||||
return !!content && content.getBoundingClientRect().height > 0
|
||||
}
|
||||
|
||||
async function mockServer(page: Page, events: EventPayload[]) {
|
||||
await page.route("**/*", async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
if (url.port !== targetPort) return route.fallback()
|
||||
|
||||
const path = url.pathname
|
||||
if (path === "/global/event") return sse(route, events.splice(0))
|
||||
if (
|
||||
path === "/global/config" ||
|
||||
path === "/config" ||
|
||||
path === "/provider/auth" ||
|
||||
path === "/mcp" ||
|
||||
path === "/session/status"
|
||||
)
|
||||
return json(route, {})
|
||||
if (
|
||||
["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes(
|
||||
path,
|
||||
)
|
||||
)
|
||||
return json(route, [])
|
||||
if (path === "/provider") return json(route, provider())
|
||||
if (path === "/path")
|
||||
return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" })
|
||||
if (path === "/project") return json(route, [project()])
|
||||
if (path === "/project/current") return json(route, project())
|
||||
if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }])
|
||||
if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" })
|
||||
if (path === "/session") return json(route, [session()])
|
||||
if (path === `/session/${sessionID}`) return json(route, session())
|
||||
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, [])
|
||||
if (path === `/session/${sessionID}/message`) return json(route, [userMessage, assistantMessage])
|
||||
return json(route, {})
|
||||
})
|
||||
}
|
||||
|
||||
function project() {
|
||||
return {
|
||||
id: projectID,
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "timeline-state-regression",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
}
|
||||
}
|
||||
|
||||
function session() {
|
||||
return {
|
||||
id: sessionID,
|
||||
slug: "timeline-state-regression",
|
||||
projectID,
|
||||
directory,
|
||||
title,
|
||||
version: "dev",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
}
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return {
|
||||
all: [
|
||||
{
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
|
||||
},
|
||||
],
|
||||
connected: ["opencode"],
|
||||
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
||||
}
|
||||
}
|
||||
|
||||
function json(route: Route, body: unknown, headers?: Record<string, string>) {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers },
|
||||
body: JSON.stringify(body ?? null),
|
||||
})
|
||||
}
|
||||
|
||||
function sse(route: Route, events: EventPayload[]) {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/event-stream",
|
||||
headers: { "access-control-allow-origin": "*" },
|
||||
body: events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""),
|
||||
})
|
||||
}
|
||||
|
||||
function base64Encode(value: string) {
|
||||
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import { expect, test, type Page, type Route } from "@playwright/test"
|
||||
|
||||
const directory = "C:/OpenCode/ContextResizeRegression"
|
||||
const projectID = "proj_context_resize_regression"
|
||||
const sessionID = "ses_context_resize_regression"
|
||||
const title = "Context resize regression"
|
||||
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
||||
const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"]
|
||||
const followingTextID = "prt_0104_text"
|
||||
|
||||
type Message = {
|
||||
info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
|
||||
parts: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
const messages = [...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...turn(10, true)]
|
||||
|
||||
test.describe("regression: session timeline context group resize", () => {
|
||||
test("remeasures a recent explored context group before the next paint", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 900 })
|
||||
await mockServer(page)
|
||||
await configurePage(page)
|
||||
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await expect(page.getByRole("heading", { name: title })).toBeVisible()
|
||||
await expect(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()).toBeVisible()
|
||||
await expect(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first()).toBeVisible()
|
||||
await settle(page)
|
||||
|
||||
const samples = await sampleExpansion(page)
|
||||
const visibleOverlap = samples.filter((sample) => sample.frame >= 1 && sample.overlap > 0.5)
|
||||
|
||||
console.log("context resize samples", JSON.stringify(samples, null, 2))
|
||||
|
||||
expect(samples[0]?.overlap).toBe(0)
|
||||
expect(visibleOverlap).toEqual([])
|
||||
expect(samples.at(-1)?.expanded).toBe("true")
|
||||
})
|
||||
})
|
||||
|
||||
async function configurePage(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
"settings.v3",
|
||||
JSON.stringify({
|
||||
general: {
|
||||
editToolPartsExpanded: true,
|
||||
shellToolPartsExpanded: true,
|
||||
showReasoningSummaries: true,
|
||||
showSessionProgressBar: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function sampleExpansion(page: Page) {
|
||||
return page.evaluate(
|
||||
({ contextIDs, followingTextID }) =>
|
||||
new Promise<
|
||||
{
|
||||
frame: number
|
||||
label: string
|
||||
scrollTop: number
|
||||
scrollHeight: number
|
||||
contextBottom: number
|
||||
textTop: number
|
||||
overlap: number
|
||||
gap: number
|
||||
expanded: string | null
|
||||
}[]
|
||||
>((resolve) => {
|
||||
const context = document.querySelector<HTMLElement>(`[data-timeline-part-ids="${contextIDs.join(",")}"]`)
|
||||
const text = document.querySelector<HTMLElement>(`[data-timeline-part-id="${followingTextID}"]`)
|
||||
const scroller = context?.closest<HTMLElement>(".scroll-view__viewport")
|
||||
const trigger = context?.querySelector<HTMLElement>('[data-slot="collapsible-trigger"]')
|
||||
const contextRow = context?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
|
||||
const textRow = text?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
|
||||
if (!context || !text || !scroller || !trigger || !contextRow || !textRow)
|
||||
throw new Error("missing regression nodes")
|
||||
|
||||
scroller.scrollTop = scroller.scrollHeight
|
||||
const samples: {
|
||||
frame: number
|
||||
label: string
|
||||
scrollTop: number
|
||||
scrollHeight: number
|
||||
contextBottom: number
|
||||
textTop: number
|
||||
overlap: number
|
||||
gap: number
|
||||
expanded: string | null
|
||||
}[] = []
|
||||
const capture = (frame: number, label: string) => {
|
||||
const contextRect = contextRow.getBoundingClientRect()
|
||||
const textRect = textRow.getBoundingClientRect()
|
||||
samples.push({
|
||||
frame,
|
||||
label,
|
||||
scrollTop: Math.round(scroller.scrollTop * 10) / 10,
|
||||
scrollHeight: Math.round(scroller.scrollHeight * 10) / 10,
|
||||
contextBottom: Math.round(contextRect.bottom * 10) / 10,
|
||||
textTop: Math.round(textRect.top * 10) / 10,
|
||||
overlap: Math.max(0, Math.round((contextRect.bottom - textRect.top) * 10) / 10),
|
||||
gap: Math.max(0, Math.round((textRect.top - contextRect.bottom) * 10) / 10),
|
||||
expanded: trigger.getAttribute("aria-expanded"),
|
||||
})
|
||||
}
|
||||
|
||||
capture(-1, "before")
|
||||
trigger.click()
|
||||
capture(0, "sync-after-click")
|
||||
|
||||
let frame = 1
|
||||
const tick = () => {
|
||||
capture(frame, "raf")
|
||||
frame += 1
|
||||
if (frame > 8) {
|
||||
resolve(samples)
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
}),
|
||||
{ contextIDs, followingTextID },
|
||||
)
|
||||
}
|
||||
|
||||
function turn(index: number, target: boolean): Message[] {
|
||||
const userID = id("msg_user", index)
|
||||
const assistantID = id("msg_assistant", index)
|
||||
return [
|
||||
{
|
||||
info: {
|
||||
id: userID,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: 1700000000000 + index * 10_000 },
|
||||
summary: { diffs: [] },
|
||||
agent: "build",
|
||||
model,
|
||||
},
|
||||
parts: [{ id: id("prt_user", index), sessionID, messageID: userID, type: "text", text: `User message ${index}` }],
|
||||
},
|
||||
{
|
||||
info: {
|
||||
id: assistantID,
|
||||
sessionID,
|
||||
role: "assistant",
|
||||
time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 2_000 },
|
||||
parentID: userID,
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
mode: "build",
|
||||
agent: "build",
|
||||
path: { cwd: directory, root: directory },
|
||||
cost: 0.01,
|
||||
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
variant: "max",
|
||||
finish: "stop",
|
||||
},
|
||||
parts: target
|
||||
? [
|
||||
contextTool(contextIDs[0]!, assistantID, "read", { filePath: "src/recent-a.ts", offset: 0, limit: 120 }),
|
||||
contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }),
|
||||
contextTool(contextIDs[2]!, assistantID, "grep", { path: directory, pattern: "Explored", include: "*.ts" }),
|
||||
contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }),
|
||||
{
|
||||
id: followingTextID,
|
||||
sessionID,
|
||||
messageID: assistantID,
|
||||
type: "text",
|
||||
text: "This assistant text is immediately after the explored context group.",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: id("prt_text", index),
|
||||
sessionID,
|
||||
messageID: assistantID,
|
||||
type: "text",
|
||||
text: `Assistant filler ${index}. ${"filler ".repeat(60)}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function contextTool(partID: string, messageID: string, tool: string, input: Record<string, unknown>) {
|
||||
return {
|
||||
id: partID,
|
||||
sessionID,
|
||||
messageID,
|
||||
type: "tool",
|
||||
callID: `call_${partID}`,
|
||||
tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
input,
|
||||
output: `Completed ${tool}.\n${"detail line\n".repeat(8)}`,
|
||||
title: input.filePath || input.path || input.pattern || "completed",
|
||||
metadata: {},
|
||||
time: { start: 1700000000000, end: 1700000000100 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function mockServer(page: Page) {
|
||||
await page.route("**/*", async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||
if (url.port !== targetPort) return route.fallback()
|
||||
|
||||
const path = url.pathname
|
||||
if (path === "/global/event" || path === "/event") return sse(route)
|
||||
if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(path))
|
||||
return json(route, {})
|
||||
if (
|
||||
["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes(
|
||||
path,
|
||||
)
|
||||
)
|
||||
return json(route, [])
|
||||
if (path === "/provider") return json(route, provider())
|
||||
if (path === "/path")
|
||||
return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" })
|
||||
if (path === "/project") return json(route, [project()])
|
||||
if (path === "/project/current") return json(route, project())
|
||||
if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }])
|
||||
if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" })
|
||||
if (path === "/session") return json(route, [session()])
|
||||
if (path === `/session/${sessionID}`) return json(route, session())
|
||||
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, [])
|
||||
if (path === `/session/${sessionID}/message`) return json(route, messages)
|
||||
return json(route, {})
|
||||
})
|
||||
}
|
||||
|
||||
async function settle(page: Page) {
|
||||
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
|
||||
}
|
||||
|
||||
function id(prefix: string, index: number) {
|
||||
return `${prefix}_${String(index).padStart(4, "0")}`
|
||||
}
|
||||
|
||||
function project() {
|
||||
return {
|
||||
id: projectID,
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "context-resize-regression",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
}
|
||||
}
|
||||
|
||||
function session() {
|
||||
return {
|
||||
id: sessionID,
|
||||
slug: "context-resize-regression",
|
||||
projectID,
|
||||
directory,
|
||||
title,
|
||||
version: "dev",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
}
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return {
|
||||
all: [
|
||||
{
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
|
||||
},
|
||||
],
|
||||
connected: ["opencode"],
|
||||
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
||||
}
|
||||
}
|
||||
|
||||
function json(route: Route, body: unknown, headers?: Record<string, string>) {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers },
|
||||
body: JSON.stringify(body ?? null),
|
||||
})
|
||||
}
|
||||
|
||||
function sse(route: Route) {
|
||||
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
|
||||
}
|
||||
|
||||
function base64Encode(value: string) {
|
||||
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
||||
}
|
||||
@@ -30,8 +30,8 @@ import { Dynamic } from "solid-js/web"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import { CommentsProvider } from "@/context/comments"
|
||||
import { FileProvider } from "@/context/file"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
||||
import { ServerSyncProvider } from "@/context/server-sync"
|
||||
import { HighlightsProvider } from "@/context/highlights"
|
||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
@@ -304,8 +304,8 @@ export function AppInterface(props: {
|
||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||
<ServerKey>
|
||||
<QueryProvider>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<ServerSDKProvider>
|
||||
<ServerSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
@@ -316,8 +316,8 @@ export function AppInterface(props: {
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</QueryProvider>
|
||||
</ServerKey>
|
||||
</ConnectionGate>
|
||||
|
||||
@@ -12,15 +12,15 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Link } from "@/components/link"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
|
||||
export function DialogConnectProvider(props: { provider: string }) {
|
||||
const dialog = useDialog()
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const serverSync = useServerSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const language = useLanguage()
|
||||
const providers = useProviders()
|
||||
|
||||
@@ -41,7 +41,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
})
|
||||
|
||||
const provider = createMemo(
|
||||
() => providers.all().get(props.provider) ?? globalSync.data.provider.all.get(props.provider)!,
|
||||
() => providers.all().get(props.provider) ?? serverSync.data.provider.all.get(props.provider)!,
|
||||
)
|
||||
const fallback = createMemo<ProviderAuthMethod[]>(() => [
|
||||
{
|
||||
@@ -52,16 +52,16 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
const [auth] = createResource(
|
||||
() => props.provider,
|
||||
async () => {
|
||||
const cached = globalSync.data.provider_auth[props.provider]
|
||||
const cached = serverSync.data.provider_auth[props.provider]
|
||||
if (cached) return cached
|
||||
const res = await globalSDK.client.provider.auth()
|
||||
const res = await serverSDK.client.provider.auth()
|
||||
if (!alive.value) return fallback()
|
||||
globalSync.set("provider_auth", res.data ?? {})
|
||||
serverSync.set("provider_auth", res.data ?? {})
|
||||
return res.data?.[props.provider] ?? fallback()
|
||||
},
|
||||
)
|
||||
const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider])
|
||||
const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback())
|
||||
const loading = createMemo(() => auth.loading && !serverSync.data.provider_auth[props.provider])
|
||||
const methods = createMemo(() => auth.latest ?? serverSync.data.provider_auth[props.provider] ?? fallback())
|
||||
const [store, setStore] = createStore({
|
||||
methodIndex: undefined as undefined | number,
|
||||
authorization: undefined as undefined | ProviderAuthAuthorization,
|
||||
@@ -158,7 +158,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}
|
||||
dispatch({ type: "auth.pending" })
|
||||
const start = Date.now()
|
||||
await globalSDK.client.provider.oauth
|
||||
await serverSDK.client.provider.oauth
|
||||
.authorize(
|
||||
{
|
||||
providerID: props.provider,
|
||||
@@ -330,7 +330,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
})
|
||||
|
||||
async function complete() {
|
||||
await globalSDK.client.global.dispose()
|
||||
await serverSDK.client.global.dispose()
|
||||
dialog.close()
|
||||
showToast({
|
||||
variant: "success",
|
||||
@@ -407,7 +407,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await globalSDK.client.auth.set({
|
||||
await serverSDK.client.auth.set({
|
||||
providerID: props.provider,
|
||||
auth: {
|
||||
type: "api",
|
||||
@@ -478,7 +478,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
const result = await serverSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
@@ -531,7 +531,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
const result = await serverSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { batch, For } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Link } from "@/components/link"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
@@ -21,8 +21,8 @@ type Props = {
|
||||
|
||||
export function DialogCustomProvider(props: Props) {
|
||||
const dialog = useDialog()
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const serverSync = useServerSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const language = useLanguage()
|
||||
|
||||
const [form, setForm] = createStore<FormState>({
|
||||
@@ -105,8 +105,8 @@ export function DialogCustomProvider(props: Props) {
|
||||
const output = validateCustomProvider({
|
||||
form,
|
||||
t: language.t,
|
||||
disabledProviders: globalSync.data.config.disabled_providers ?? [],
|
||||
existingProviderIDs: new Set(globalSync.data.provider.all.keys()),
|
||||
disabledProviders: serverSync.data.config.disabled_providers ?? [],
|
||||
existingProviderIDs: new Set(serverSync.data.provider.all.keys()),
|
||||
})
|
||||
batch(() => {
|
||||
setForm("err", output.err)
|
||||
@@ -118,11 +118,11 @@ export function DialogCustomProvider(props: Props) {
|
||||
|
||||
const saveMutation = useMutation(() => ({
|
||||
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
|
||||
const disabledProviders = globalSync.data.config.disabled_providers ?? []
|
||||
const disabledProviders = serverSync.data.config.disabled_providers ?? []
|
||||
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
|
||||
|
||||
if (result.key) {
|
||||
await globalSDK.client.auth.set({
|
||||
await serverSDK.client.auth.set({
|
||||
providerID: result.providerID,
|
||||
auth: {
|
||||
type: "api",
|
||||
@@ -131,7 +131,7 @@ export function DialogCustomProvider(props: Props) {
|
||||
})
|
||||
}
|
||||
|
||||
await globalSync.updateConfig({
|
||||
await serverSync.updateConfig({
|
||||
provider: { [result.providerID]: result.config },
|
||||
disabled_providers: nextDisabled,
|
||||
})
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useMutation } from "@tanstack/solid-query"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
@@ -18,8 +18,8 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
|
||||
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const dialog = useDialog()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const language = useLanguage()
|
||||
|
||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||
@@ -78,19 +78,19 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const start = store.startup.trim()
|
||||
|
||||
if (props.project.id && props.project.id !== "global") {
|
||||
await globalSDK.client.project.update({
|
||||
await serverSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
||||
commands: { start },
|
||||
})
|
||||
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||
serverSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.project.meta(props.project.worktree, {
|
||||
serverSync.project.meta(props.project.worktree, {
|
||||
name,
|
||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
||||
commands: { start: start || undefined },
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { ListRef } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { createMemo, createResource, createSignal } from "solid-js"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
@@ -128,7 +128,7 @@ function uniqueRows(rows: Row[]) {
|
||||
}
|
||||
|
||||
function useDirectorySearch(args: {
|
||||
sdk: ReturnType<typeof useGlobalSDK>
|
||||
sdk: ReturnType<typeof useServerSDK>
|
||||
start: () => string | undefined
|
||||
home: () => string
|
||||
}) {
|
||||
@@ -246,8 +246,8 @@ function useDirectorySearch(args: {
|
||||
}
|
||||
|
||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useGlobalSync()
|
||||
const sdk = useGlobalSDK()
|
||||
const sync = useServerSync()
|
||||
const sdk = useServerSDK()
|
||||
const layout = useLayout()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -175,7 +175,7 @@ function createFileEntries(props: {
|
||||
function createSessionEntries(props: {
|
||||
workspaces: () => string[]
|
||||
label: (directory: string) => string
|
||||
globalSDK: ReturnType<typeof useGlobalSDK>
|
||||
serverSDK: ReturnType<typeof useServerSDK>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const state: {
|
||||
@@ -207,7 +207,7 @@ function createSessionEntries(props: {
|
||||
state.inflight = Promise.all(
|
||||
dirs.map((directory) => {
|
||||
const description = props.label(directory)
|
||||
return props.globalSDK.client.session
|
||||
return props.serverSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
.then((x) =>
|
||||
(x.data ?? [])
|
||||
@@ -268,8 +268,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
const navigate = useNavigate()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const { params, tabs, view } = useSessionLayout()
|
||||
const filesOnly = () => props.mode === "files"
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
@@ -292,21 +292,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||
return dirs
|
||||
})
|
||||
const homedir = createMemo(() => globalSync.data.path.home)
|
||||
const homedir = createMemo(() => serverSync.data.path.home)
|
||||
const label = (directory: string) => {
|
||||
const current = project()
|
||||
const kind =
|
||||
current && directory === current.worktree
|
||||
? language.t("workspace.type.local")
|
||||
: language.t("workspace.type.sandbox")
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
||||
const home = homedir()
|
||||
const path = home ? directory.replace(home, "~") : directory
|
||||
const name = store.vcs?.branch ?? getFilename(directory)
|
||||
return `${kind} : ${name || path}`
|
||||
}
|
||||
|
||||
const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language })
|
||||
const { sessions } = createSessionEntries({ workspaces, label, serverSDK, language })
|
||||
|
||||
const items = async (text: string) => {
|
||||
const query = text.trim()
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useQueryOptions } from "@/context/global-sync"
|
||||
import { useQueryOptions } from "@/context/server-sync"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
const statusLabels = {
|
||||
|
||||
@@ -66,7 +66,7 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { useQueryOptions } from "@/context/global-sync"
|
||||
import { useQueryOptions } from "@/context/server-sync"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
|
||||
|
||||
@@ -162,8 +162,8 @@ beforeAll(async () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("@/context/global-sync", () => ({
|
||||
useGlobalSync: () => ({
|
||||
mock.module("@/context/server-sync", () => ({
|
||||
useServerSync: () => ({
|
||||
child: (directory: string) => {
|
||||
syncedDirectories.push(directory)
|
||||
storedSessions[directory] ??= []
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { batch, type Accessor } from "solid-js"
|
||||
import type { FileSelection } from "@/context/file"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -38,7 +38,7 @@ export type FollowupDraft = {
|
||||
|
||||
type FollowupSendInput = {
|
||||
client: ReturnType<typeof useSDK>["client"]
|
||||
globalSync: ReturnType<typeof useGlobalSync>
|
||||
serverSync: ReturnType<typeof useServerSync>
|
||||
sync: ReturnType<typeof useSync>
|
||||
draft: FollowupDraft
|
||||
messageID?: string
|
||||
@@ -53,7 +53,7 @@ const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttac
|
||||
export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
const text = draftText(input.draft.prompt)
|
||||
const images = draftImages(input.draft.prompt)
|
||||
const [, setStore] = input.globalSync.child(input.draft.sessionDirectory)
|
||||
const [, setStore] = input.serverSync.child(input.draft.sessionDirectory)
|
||||
|
||||
const setBusy = () => {
|
||||
if (!input.optimisticBusy) return
|
||||
@@ -205,7 +205,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const local = useLocal()
|
||||
const permission = usePermission()
|
||||
const prompt = usePrompt()
|
||||
@@ -226,8 +226,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return Promise.resolve()
|
||||
|
||||
globalSync.todo.set(sessionID, [])
|
||||
const [, setStore] = globalSync.child(sdk.directory)
|
||||
serverSync.todo.set(sessionID, [])
|
||||
const [, setStore] = serverSync.child(sdk.directory)
|
||||
setStore("todo", sessionID, [])
|
||||
|
||||
input.onAbort?.()
|
||||
@@ -273,7 +273,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
}
|
||||
|
||||
const seed = (dir: string, info: Session) => {
|
||||
const [, setStore] = globalSync.child(dir)
|
||||
const [, setStore] = serverSync.child(dir)
|
||||
setStore("session", (list: Session[]) => {
|
||||
const result = Binary.search(list, info.id, (item) => item.id)
|
||||
const next = [...list]
|
||||
@@ -354,7 +354,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
directory: sessionDirectory,
|
||||
throwOnError: true,
|
||||
})
|
||||
globalSync.child(sessionDirectory)
|
||||
serverSync.child(sessionDirectory)
|
||||
}
|
||||
|
||||
input.onNewSessionWorktreeReset?.()
|
||||
@@ -557,7 +557,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
void sendFollowupDraft({
|
||||
client,
|
||||
sync,
|
||||
globalSync,
|
||||
serverSync,
|
||||
draft,
|
||||
messageID,
|
||||
optimisticBusy: sessionDirectory === projectDirectory,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useServer } from "@/context/server"
|
||||
@@ -15,7 +15,7 @@ import { WordmarkV2 } from "@opencode-ai/ui/v2/components/wordmark-v2.jsx"
|
||||
const MAIN_WORKTREE = "main"
|
||||
|
||||
export function NewSessionDesignView(props: { worktree: string; children: JSX.Element }) {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const layout = useLayout()
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
@@ -24,7 +24,7 @@ export function NewSessionDesignView(props: { worktree: string; children: JSX.El
|
||||
|
||||
const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
|
||||
const projects = createMemo(() => {
|
||||
const roots = globalSync.data.project.map((project) => project.worktree)
|
||||
const roots = serverSync.data.project.map((project) => project.worktree)
|
||||
if (roots.includes(projectRoot())) return roots
|
||||
return [projectRoot(), ...roots]
|
||||
})
|
||||
|
||||
@@ -12,8 +12,8 @@ import { useParams } from "@solidjs/router"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import {
|
||||
monoDefault,
|
||||
monoFontFamily,
|
||||
@@ -175,8 +175,8 @@ export const SettingsGeneral: Component = () => {
|
||||
|
||||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSdk = useGlobalSDK()
|
||||
const serverSync = useServerSync()
|
||||
const globalSdk = useServerSDK()
|
||||
|
||||
const [shells] = createResource(
|
||||
() =>
|
||||
@@ -204,11 +204,11 @@ export const SettingsGeneral: Component = () => {
|
||||
})
|
||||
|
||||
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
||||
const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
|
||||
const currentShell = createMemo(() => serverSync.data.config.shell ?? "")
|
||||
|
||||
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
||||
const list = shells.latest
|
||||
const current = globalSync.data.config.shell
|
||||
const current = serverSync.data.config.shell
|
||||
|
||||
const nameCounts = new Map<string, number>()
|
||||
for (const s of list) {
|
||||
@@ -343,7 +343,7 @@ export const SettingsGeneral: Component = () => {
|
||||
onSelect={(option) => {
|
||||
if (!option) return
|
||||
if (option.value === currentShell()) return
|
||||
globalSync.updateConfig({ shell: option.value })
|
||||
serverSync.updateConfig({ shell: option.value })
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
|
||||
@@ -6,8 +6,8 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { createMemo, type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||
@@ -30,8 +30,8 @@ const PROVIDER_NOTES = [
|
||||
export const SettingsProviders: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const providers = useProviders()
|
||||
|
||||
const connected = createMemo(() => {
|
||||
@@ -74,7 +74,7 @@ export const SettingsProviders: Component = () => {
|
||||
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
|
||||
|
||||
const isConfigCustom = (providerID: string) => {
|
||||
const provider = globalSync.data.config.provider?.[providerID]
|
||||
const provider = serverSync.data.config.provider?.[providerID]
|
||||
if (!provider) return false
|
||||
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
||||
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
||||
@@ -82,11 +82,11 @@ export const SettingsProviders: Component = () => {
|
||||
}
|
||||
|
||||
const disableProvider = async (providerID: string, name: string) => {
|
||||
const before = globalSync.data.config.disabled_providers ?? []
|
||||
const before = serverSync.data.config.disabled_providers ?? []
|
||||
const next = before.includes(providerID) ? before : [...before, providerID]
|
||||
globalSync.set("config", "disabled_providers", next)
|
||||
serverSync.set("config", "disabled_providers", next)
|
||||
|
||||
await globalSync
|
||||
await serverSync
|
||||
.updateConfig({ disabled_providers: next })
|
||||
.then(() => {
|
||||
showToast({
|
||||
@@ -97,7 +97,7 @@ export const SettingsProviders: Component = () => {
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
globalSync.set("config", "disabled_providers", before)
|
||||
serverSync.set("config", "disabled_providers", before)
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
@@ -105,14 +105,14 @@ export const SettingsProviders: Component = () => {
|
||||
|
||||
const disconnect = async (providerID: string, name: string) => {
|
||||
if (isConfigCustom(providerID)) {
|
||||
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
||||
await serverSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
||||
await disableProvider(providerID, name)
|
||||
return
|
||||
}
|
||||
await globalSDK.client.auth
|
||||
await serverSDK.client.auth
|
||||
.remove({ providerID })
|
||||
.then(async () => {
|
||||
await globalSDK.client.global.dispose()
|
||||
await serverSDK.client.global.dispose()
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useSDK } from "@/context/sdk"
|
||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||
import { useQueryOptions } from "@/context/global-sync"
|
||||
import { useQueryOptions } from "@/context/server-sync"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
const pollMs = 10_000
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { WindowsAppMenu } from "./windows-app-menu"
|
||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { decodeDirectory } from "@/pages/directory-layout"
|
||||
import { iife } from "@opencode-ai/core/util/iife"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
@@ -221,7 +221,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
<Switch>
|
||||
<Match when={USE_V2_TITLEBAR}>
|
||||
{(_) => {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const navigate = useNavigate()
|
||||
const homeMatch = useMatch(() => "/")
|
||||
|
||||
@@ -391,7 +391,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
const base = mapArray(
|
||||
() => tabsStore,
|
||||
(tab) => {
|
||||
const sync = globalSync.createDirSyncContext(tab.dir)
|
||||
const sync = serverSync.createDirSyncContext(tab.dir)
|
||||
const session = sync.session.get(tab.sessionId)
|
||||
return session ? { ...tab, info: session } : null
|
||||
},
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
getSessionPrefetchPromise,
|
||||
setSessionPrefetch,
|
||||
} from "./global-sync/session-prefetch"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { createServerSyncContext, useServerSync } from "./server-sync"
|
||||
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
@@ -164,16 +165,17 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
|
||||
})
|
||||
}
|
||||
|
||||
export const createDirSyncContext = (client: OpencodeClient, directory: string) => {
|
||||
const globalSync = useGlobalSync()
|
||||
export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
|
||||
const serverSDK = useServerSDK()
|
||||
const client = serverSDK.createClient({ directory, throwOnError: true })
|
||||
|
||||
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||
type Child = ReturnType<(typeof serverSync)["child"]>
|
||||
type Setter = Child[1]
|
||||
|
||||
const current = createMemo(() => globalSync.child(directory))
|
||||
const current = createMemo(() => serverSync.child(directory))
|
||||
const target = (directory?: string) => {
|
||||
if (!directory || directory === directory) return current()
|
||||
return globalSync.child(directory)
|
||||
return serverSync.child(directory)
|
||||
}
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const initialMessagePageSize = 80
|
||||
@@ -239,7 +241,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
if (!first) break
|
||||
const stale = [...(seen.get(first) ?? [])]
|
||||
seen.delete(first)
|
||||
const [, setStore] = globalSync.child(first, { bootstrap: false })
|
||||
const [, setStore] = serverSync.child(first, { bootstrap: false })
|
||||
evict(first, setStore, stale)
|
||||
}
|
||||
return created
|
||||
@@ -267,7 +269,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
if (sessionIDs.length === 0) return
|
||||
clearSessionPrefetch(directory, sessionIDs)
|
||||
for (const sessionID of sessionIDs) {
|
||||
globalSync.todo.set(sessionID, undefined)
|
||||
serverSync.todo.set(sessionID, undefined)
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
@@ -324,7 +326,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
for (const messageID of next.confirmed) {
|
||||
clearOptimistic(input.directory, input.sessionID, messageID)
|
||||
}
|
||||
const [store] = globalSync.child(input.directory, { bootstrap: false })
|
||||
const [store] = serverSync.child(input.directory, { bootstrap: false })
|
||||
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
||||
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
||||
batch(() => {
|
||||
@@ -373,8 +375,8 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
},
|
||||
get project() {
|
||||
const store = current()[0]
|
||||
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
||||
if (match.found) return globalSync.data.project[match.index]
|
||||
const match = Binary.search(serverSync.data.project, store.project, (p) => p.id)
|
||||
if (match.found) return serverSync.data.project[match.index]
|
||||
return undefined
|
||||
},
|
||||
session: {
|
||||
@@ -418,7 +420,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
})
|
||||
},
|
||||
async sync(sessionID: string, opts?: { force?: boolean }) {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const [store, setStore] = serverSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
|
||||
touch(directory, setStore, sessionID)
|
||||
@@ -488,7 +490,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
})
|
||||
},
|
||||
async diff(sessionID: string, opts?: { force?: boolean }) {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const [store, setStore] = serverSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
if (store.session_diff[sessionID] !== undefined && !opts?.force) return
|
||||
|
||||
@@ -501,13 +503,13 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
)
|
||||
},
|
||||
async todo(sessionID: string, opts?: { force?: boolean }) {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const [store, setStore] = serverSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const existing = store.todo[sessionID]
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
const cached = serverSync.data.session_todo[sessionID]
|
||||
if (existing !== undefined) {
|
||||
if (cached === undefined) {
|
||||
globalSync.todo.set(sessionID, existing)
|
||||
serverSync.todo.set(sessionID, existing)
|
||||
}
|
||||
if (!opts?.force) return
|
||||
}
|
||||
@@ -522,7 +524,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
if (!tracked(directory, sessionID)) return
|
||||
const list = todo.data ?? []
|
||||
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
||||
globalSync.todo.set(sessionID, list)
|
||||
serverSync.todo.set(sessionID, list)
|
||||
}),
|
||||
)
|
||||
},
|
||||
@@ -540,7 +542,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
return meta.loading[key] ?? false
|
||||
},
|
||||
async loadMore(sessionID: string, count?: number) {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
const [, setStore] = serverSync.child(directory)
|
||||
touch(directory, setStore, sessionID)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const step = count ?? historyMessagePageSize
|
||||
@@ -561,12 +563,12 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
},
|
||||
},
|
||||
evict(sessionID: string, _directory = directory) {
|
||||
const [, setStore] = globalSync.child(_directory)
|
||||
const [, setStore] = serverSync.child(_directory)
|
||||
seenFor(_directory).delete(sessionID)
|
||||
evict(_directory, setStore, [sessionID])
|
||||
},
|
||||
fetch: async (count = 10) => {
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const [store, setStore] = serverSync.child(directory)
|
||||
setStore("limit", (x) => x + count)
|
||||
await client.session.list().then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
@@ -578,7 +580,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
|
||||
},
|
||||
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
||||
archive: async (sessionID: string) => {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
const [, setStore] = serverSync.child(directory)
|
||||
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
import type { Event } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
import { createSdkForServer } from "@/utils/server"
|
||||
import { useLanguage } from "./language"
|
||||
import { usePlatform } from "./platform"
|
||||
import { useServer } from "./server"
|
||||
|
||||
const isAbortError = (error: unknown) =>
|
||||
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
||||
|
||||
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
|
||||
name: "GlobalSDK",
|
||||
init: () => {
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const abort = new AbortController()
|
||||
|
||||
const eventFetch = (() => {
|
||||
if (!platform.fetch || !server.current) return
|
||||
try {
|
||||
const url = new URL(server.current.http.url)
|
||||
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
||||
if (url.protocol === "http:" && !loopback) return platform.fetch
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})()
|
||||
|
||||
const currentServer = server.current
|
||||
if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable"))
|
||||
|
||||
const eventSdk = createSdkForServer({
|
||||
signal: abort.signal,
|
||||
fetch: eventFetch,
|
||||
server: currentServer.http,
|
||||
})
|
||||
const emitter = createGlobalEmitter<{
|
||||
[key: string]: Event
|
||||
}>()
|
||||
|
||||
type Queued = { directory: string; payload: Event }
|
||||
const FLUSH_FRAME_MS = 16
|
||||
const STREAM_YIELD_MS = 8
|
||||
const RECONNECT_DELAY_MS = 250
|
||||
|
||||
let queue: Queued[] = []
|
||||
let buffer: Queued[] = []
|
||||
const coalesced = new Map<string, number>()
|
||||
const staleDeltas = new Set<string>()
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let last = 0
|
||||
|
||||
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
||||
|
||||
const key = (directory: string, payload: Event) => {
|
||||
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
||||
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
||||
if (payload.type === "message.part.updated") {
|
||||
const part = payload.properties.part
|
||||
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
||||
}
|
||||
}
|
||||
|
||||
const flush = () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = undefined
|
||||
|
||||
if (queue.length === 0) return
|
||||
|
||||
const events = queue
|
||||
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
||||
queue = buffer
|
||||
buffer = events
|
||||
queue.length = 0
|
||||
coalesced.clear()
|
||||
staleDeltas.clear()
|
||||
|
||||
last = Date.now()
|
||||
batch(() => {
|
||||
for (const event of events) {
|
||||
if (skip && event.payload.type === "message.part.delta") {
|
||||
const props = event.payload.properties
|
||||
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
|
||||
}
|
||||
emitter.emit(event.directory, event.payload)
|
||||
}
|
||||
})
|
||||
|
||||
buffer.length = 0
|
||||
}
|
||||
|
||||
const schedule = () => {
|
||||
if (timer) return
|
||||
const elapsed = Date.now() - last
|
||||
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
|
||||
}
|
||||
|
||||
let streamErrorLogged = false
|
||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
const aborted = isAbortError
|
||||
|
||||
let attempt: AbortController | undefined
|
||||
let run: Promise<void> | undefined
|
||||
let started = false
|
||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||
let lastEventAt = Date.now()
|
||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||
const resetHeartbeat = () => {
|
||||
lastEventAt = Date.now()
|
||||
if (heartbeat) clearTimeout(heartbeat)
|
||||
heartbeat = setTimeout(() => {
|
||||
attempt?.abort()
|
||||
}, HEARTBEAT_TIMEOUT_MS)
|
||||
}
|
||||
const clearHeartbeat = () => {
|
||||
if (!heartbeat) return
|
||||
clearTimeout(heartbeat)
|
||||
heartbeat = undefined
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
if (started) return run
|
||||
started = true
|
||||
run = (async () => {
|
||||
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
||||
while (!abort.signal.aborted && started) {
|
||||
attempt = new AbortController()
|
||||
lastEventAt = Date.now()
|
||||
const onAbort = () => {
|
||||
attempt?.abort()
|
||||
}
|
||||
abort.signal.addEventListener("abort", onAbort)
|
||||
try {
|
||||
const events = await eventSdk.global.event({
|
||||
signal: attempt.signal,
|
||||
onSseError: (error) => {
|
||||
if (aborted(error)) return
|
||||
if (streamErrorLogged) return
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream error", {
|
||||
url: currentServer.http.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
})
|
||||
},
|
||||
})
|
||||
let yielded = Date.now()
|
||||
resetHeartbeat()
|
||||
for await (const event of events.stream) {
|
||||
resetHeartbeat()
|
||||
streamErrorLogged = false
|
||||
const directory = event.directory ?? "global"
|
||||
if (event.payload.type === "sync") {
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = event.payload as Event
|
||||
|
||||
const k = key(directory, payload)
|
||||
if (k) {
|
||||
const i = coalesced.get(k)
|
||||
if (i !== undefined) {
|
||||
queue[i] = { directory, payload }
|
||||
if (payload.type === "message.part.updated") {
|
||||
const part = payload.properties.part
|
||||
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
||||
}
|
||||
continue
|
||||
}
|
||||
coalesced.set(k, queue.length)
|
||||
}
|
||||
queue.push({ directory, payload })
|
||||
schedule()
|
||||
|
||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||
yielded = Date.now()
|
||||
await wait(0)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!aborted(error) && !streamErrorLogged) {
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream failed", {
|
||||
url: currentServer.http.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
abort.signal.removeEventListener("abort", onAbort)
|
||||
attempt = undefined
|
||||
clearHeartbeat()
|
||||
}
|
||||
|
||||
if (abort.signal.aborted || !started) return
|
||||
await wait(RECONNECT_DELAY_MS)
|
||||
}
|
||||
})().finally(() => {
|
||||
run = undefined
|
||||
flush()
|
||||
})
|
||||
return run
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
started = false
|
||||
attempt?.abort()
|
||||
clearHeartbeat()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
makeEventListener(document, "visibilitychange", () => {
|
||||
if (document.visibilityState !== "visible") return
|
||||
if (!started) return
|
||||
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
||||
attempt?.abort()
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
stop()
|
||||
abort.abort()
|
||||
flush()
|
||||
})
|
||||
|
||||
const sdk = createSdkForServer({
|
||||
server: server.current.http,
|
||||
fetch: platform.fetch,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSdkContext>>()
|
||||
const dirSdkContextRefCounts = new Map<string, number>()
|
||||
|
||||
return {
|
||||
url: currentServer.http.url,
|
||||
client: sdk,
|
||||
event: {
|
||||
on: emitter.on.bind(emitter),
|
||||
listen: emitter.listen.bind(emitter),
|
||||
start,
|
||||
},
|
||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||
const s = server.current
|
||||
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
|
||||
return createSdkForServer({
|
||||
server: s.http,
|
||||
fetch: platform.fetch,
|
||||
...opts,
|
||||
})
|
||||
},
|
||||
createDirSyncContext: (directory: string) => {
|
||||
onCleanup(() => {
|
||||
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) - 1)
|
||||
if (dirSdkContextRefCounts.get(directory) === 0) {
|
||||
dirSyncContexts.delete(directory)
|
||||
dirSdkContextRefCounts.delete(directory)
|
||||
}
|
||||
})
|
||||
|
||||
const cached = dirSyncContexts.get(directory)
|
||||
if (cached) {
|
||||
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) + 1)
|
||||
return cached
|
||||
}
|
||||
const ctx = createDirSdkContext(directory)
|
||||
dirSyncContexts.set(directory, ctx)
|
||||
dirSdkContextRefCounts.set(directory, 1)
|
||||
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
type SDKEventMap = {
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}
|
||||
|
||||
function createDirSdkContext(directory: string) {
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
const client = globalSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
const emitter = createGlobalEmitter<SDKEventMap>()
|
||||
|
||||
const unsub = globalSDK.event.on(directory, (event) => {
|
||||
emitter.emit(event.type, event)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
return {
|
||||
directory,
|
||||
client,
|
||||
event: emitter,
|
||||
get url() {
|
||||
return globalSDK.url
|
||||
},
|
||||
createClient(opts: Parameters<typeof globalSDK.createClient>[0]) {
|
||||
return globalSDK.createClient(opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||
import { loadMcpQuery } from "../global-sync"
|
||||
import { loadMcpQuery } from "../server-sync"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
|
||||
type GlobalStore = {
|
||||
@@ -105,7 +105,7 @@ export const loadProjectsQuery = (sdk: OpencodeClient) =>
|
||||
})
|
||||
|
||||
export async function bootstrapGlobal(input: {
|
||||
globalSDK: OpencodeClient
|
||||
serverSDK: OpencodeClient
|
||||
requestFailedTitle: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
@@ -113,12 +113,12 @@ export async function bootstrapGlobal(input: {
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
const slow = [
|
||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
|
||||
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
|
||||
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
|
||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.serverSDK)),
|
||||
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.serverSDK)),
|
||||
() => input.queryClient.fetchQuery(loadPathQuery(null, input.serverSDK)),
|
||||
() =>
|
||||
input.queryClient
|
||||
.fetchQuery(loadProjectsQuery(input.globalSDK))
|
||||
.fetchQuery(loadProjectsQuery(input.serverSDK))
|
||||
.then((data) => input.setGlobalStore("project", data)),
|
||||
]
|
||||
await runAll(slow)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRoot, getOwner, type Owner } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import type { State } from "./types"
|
||||
import type { QueryOptionsApi } from "../global-sync"
|
||||
import type { QueryOptionsApi } from "../server-sync"
|
||||
|
||||
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "./types"
|
||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { QueryOptionsApi } from "../global-sync"
|
||||
import { QueryOptionsApi } from "../server-sync"
|
||||
import { directoryKey, type DirectoryKey } from "./utils"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createStore, produce } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { useServer } from "./server"
|
||||
import { usePlatform } from "./platform"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
@@ -136,8 +136,8 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||
name: "Layout",
|
||||
init: () => {
|
||||
const globalSdk = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSdk = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
|
||||
@@ -386,11 +386,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||
const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
|
||||
const [childStore] = serverSync.child(project.worktree, { bootstrap: false })
|
||||
const projectID = childStore.project
|
||||
const metadata = projectID
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
? serverSync.data.project.find((x) => x.id === projectID)
|
||||
: serverSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
|
||||
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
|
||||
// Without this, different subdirectories of the same git repo would share the same
|
||||
@@ -404,7 +404,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
|
||||
const roots = createMemo(() => {
|
||||
const map = new Map<string, string>()
|
||||
for (const project of globalSync.data.project) {
|
||||
for (const project of serverSync.data.project) {
|
||||
const sandboxes = project.sandboxes ?? []
|
||||
for (const sandbox of sandboxes) {
|
||||
map.set(sandbox, project.worktree)
|
||||
@@ -470,12 +470,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
createEffect(() => {
|
||||
const projects = enriched()
|
||||
if (projects.length === 0) return
|
||||
if (!globalSync.ready) return
|
||||
if (!serverSync.ready) return
|
||||
|
||||
for (const project of projects) {
|
||||
if (!project.id) continue
|
||||
if (project.id === "global") continue
|
||||
globalSync.project.icon(project.worktree, project.icon?.override)
|
||||
serverSync.project.icon(project.worktree, project.icon?.override)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -509,7 +509,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
colorRequested.set(worktree, color)
|
||||
|
||||
if (project.id === "global") {
|
||||
globalSync.project.meta(worktree, { icon: { color } })
|
||||
serverSync.project.meta(worktree, { icon: { color } })
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
sessionTimer = undefined
|
||||
void Promise.all(
|
||||
server.projects.list().map((project) => {
|
||||
return globalSync.project.loadSessions(project.worktree)
|
||||
return serverSync.project.loadSessions(project.worktree)
|
||||
}),
|
||||
)
|
||||
}, 0)
|
||||
@@ -560,7 +560,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
open(directory: string) {
|
||||
const root = rootFor(directory)
|
||||
if (server.projects.list().find((x) => x.worktree === root)) return
|
||||
void globalSync.project.loadSessions(root)
|
||||
void serverSync.project.loadSessions(root)
|
||||
server.projects.open(root)
|
||||
},
|
||||
close(directory: string) {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createStore, reconcile } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
@@ -109,8 +109,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
name: "Notification",
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const platform = usePlatform()
|
||||
const settings = useSettings()
|
||||
const language = useLanguage()
|
||||
@@ -207,10 +207,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
|
||||
const lookup = async (directory: string, sessionID?: string) => {
|
||||
if (!sessionID) return undefined
|
||||
const [syncStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const [syncStore] = serverSync.child(directory, { bootstrap: false })
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
if (match.found) return syncStore.session[match.index]
|
||||
return globalSDK.client.session
|
||||
return serverSDK.client.session
|
||||
.get({ directory, sessionID })
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
@@ -285,7 +285,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
})
|
||||
}
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
const unsub = serverSDK.event.listen((e) => {
|
||||
const event = e.details
|
||||
if (event.type !== "session.idle" && event.type !== "session.error") return
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import {
|
||||
@@ -48,13 +48,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
name: "Permission",
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
|
||||
const permissionsEnabled = createMemo(() => {
|
||||
const directory = decode64(params.dir)
|
||||
if (!directory) return false
|
||||
const [store] = globalSync.child(directory)
|
||||
const [store] = serverSync.child(directory)
|
||||
return hasPermissionPromptRules(store.config.permission)
|
||||
})
|
||||
|
||||
@@ -86,7 +86,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
if (!ready()) return
|
||||
const directory = decode64(params.dir)
|
||||
if (!directory) return
|
||||
const [childStore] = globalSync.child(directory)
|
||||
const [childStore] = serverSync.child(directory)
|
||||
const perm = childStore.config.permission
|
||||
if (typeof perm === "string" && perm === "allow") {
|
||||
const key = directoryAcceptKey(directory)
|
||||
@@ -118,7 +118,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}
|
||||
|
||||
const respond: PermissionRespondFn = (input) => {
|
||||
globalSDK.client.permission.respond(input).catch(() => {
|
||||
serverSDK.client.permission.respond(input).catch(() => {
|
||||
responded.delete(input.permissionID)
|
||||
})
|
||||
}
|
||||
@@ -139,7 +139,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}
|
||||
|
||||
function isAutoAccepting(sessionID: string, directory?: string) {
|
||||
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
|
||||
const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : []
|
||||
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}
|
||||
|
||||
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
|
||||
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
|
||||
const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : []
|
||||
return autoRespondsPermission(store.autoAccept, session, permission, directory)
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
return next
|
||||
}
|
||||
|
||||
const unsubscribe = globalSDK.event.listen((e) => {
|
||||
const unsubscribe = serverSDK.event.listen((e) => {
|
||||
const event = e.details
|
||||
if (event?.type !== "permission.asked") return
|
||||
|
||||
@@ -178,7 +178,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}),
|
||||
)
|
||||
|
||||
globalSDK.client.permission
|
||||
serverSDK.client.permission
|
||||
.list({ directory })
|
||||
.then((x) => {
|
||||
if (!isAutoAcceptingDirectory(directory)) return
|
||||
@@ -210,7 +210,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
}),
|
||||
)
|
||||
|
||||
globalSDK.client.permission
|
||||
serverSDK.client.permission
|
||||
.list({ directory })
|
||||
.then((x) => {
|
||||
if (enableVersion.get(key) !== version) return
|
||||
@@ -268,7 +268,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
|
||||
},
|
||||
permissionsEnabled,
|
||||
isPermissionAllowAll(directory: string) {
|
||||
const [childStore] = globalSync.child(directory)
|
||||
const [childStore] = serverSync.child(directory)
|
||||
const perm = childStore.config.permission
|
||||
return typeof perm === "string" && perm === "allow"
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: { directory: string }) => {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const serverSDK = useServerSDK()
|
||||
|
||||
return globalSDK.createDirSyncContext(props.directory)
|
||||
return serverSDK.createDirSdkContext(props.directory)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import type { Event } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
import { createSdkForServer } from "@/utils/server"
|
||||
import { useLanguage } from "./language"
|
||||
import { usePlatform } from "./platform"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { createRefCountMap } from "@/utils/refcount"
|
||||
|
||||
const isAbortError = (error: unknown) =>
|
||||
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
||||
|
||||
function createServerSdkContext(server: ServerConnection.Any) {
|
||||
const platform = usePlatform()
|
||||
const abort = new AbortController()
|
||||
|
||||
const eventFetch = (() => {
|
||||
if (!platform.fetch || !server) return
|
||||
try {
|
||||
const url = new URL(server.http.url)
|
||||
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
||||
if (url.protocol === "http:" && !loopback) return platform.fetch
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})()
|
||||
|
||||
const eventSdk = createSdkForServer({
|
||||
signal: abort.signal,
|
||||
fetch: eventFetch,
|
||||
server: server.http,
|
||||
})
|
||||
const emitter = createGlobalEmitter<{
|
||||
[key: string]: Event
|
||||
}>()
|
||||
|
||||
type Queued = { directory: string; payload: Event }
|
||||
const FLUSH_FRAME_MS = 16
|
||||
const STREAM_YIELD_MS = 8
|
||||
const RECONNECT_DELAY_MS = 250
|
||||
|
||||
let queue: Queued[] = []
|
||||
let buffer: Queued[] = []
|
||||
const coalesced = new Map<string, number>()
|
||||
const staleDeltas = new Set<string>()
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let last = 0
|
||||
|
||||
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
||||
|
||||
const key = (directory: string, payload: Event) => {
|
||||
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
||||
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
||||
if (payload.type === "message.part.updated") {
|
||||
const part = payload.properties.part
|
||||
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
||||
}
|
||||
}
|
||||
|
||||
const flush = () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = undefined
|
||||
|
||||
if (queue.length === 0) return
|
||||
|
||||
const events = queue
|
||||
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
||||
queue = buffer
|
||||
buffer = events
|
||||
queue.length = 0
|
||||
coalesced.clear()
|
||||
staleDeltas.clear()
|
||||
|
||||
last = Date.now()
|
||||
batch(() => {
|
||||
for (const event of events) {
|
||||
if (skip && event.payload.type === "message.part.delta") {
|
||||
const props = event.payload.properties
|
||||
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
|
||||
}
|
||||
emitter.emit(event.directory, event.payload)
|
||||
}
|
||||
})
|
||||
|
||||
buffer.length = 0
|
||||
}
|
||||
|
||||
const schedule = () => {
|
||||
if (timer) return
|
||||
const elapsed = Date.now() - last
|
||||
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
|
||||
}
|
||||
|
||||
let streamErrorLogged = false
|
||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
const aborted = isAbortError
|
||||
|
||||
let attempt: AbortController | undefined
|
||||
let run: Promise<void> | undefined
|
||||
let started = false
|
||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||
let lastEventAt = Date.now()
|
||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||
const resetHeartbeat = () => {
|
||||
lastEventAt = Date.now()
|
||||
if (heartbeat) clearTimeout(heartbeat)
|
||||
heartbeat = setTimeout(() => {
|
||||
attempt?.abort()
|
||||
}, HEARTBEAT_TIMEOUT_MS)
|
||||
}
|
||||
const clearHeartbeat = () => {
|
||||
if (!heartbeat) return
|
||||
clearTimeout(heartbeat)
|
||||
heartbeat = undefined
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
if (started) return run
|
||||
started = true
|
||||
run = (async () => {
|
||||
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
||||
while (!abort.signal.aborted && started) {
|
||||
attempt = new AbortController()
|
||||
lastEventAt = Date.now()
|
||||
const onAbort = () => {
|
||||
attempt?.abort()
|
||||
}
|
||||
abort.signal.addEventListener("abort", onAbort)
|
||||
try {
|
||||
const events = await eventSdk.global.event({
|
||||
signal: attempt.signal,
|
||||
onSseError: (error) => {
|
||||
if (aborted(error)) return
|
||||
if (streamErrorLogged) return
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream error", {
|
||||
url: server.http.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
})
|
||||
},
|
||||
})
|
||||
let yielded = Date.now()
|
||||
resetHeartbeat()
|
||||
for await (const event of events.stream) {
|
||||
resetHeartbeat()
|
||||
streamErrorLogged = false
|
||||
const directory = event.directory ?? "global"
|
||||
if (event.payload.type === "sync") {
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = event.payload as Event
|
||||
|
||||
const k = key(directory, payload)
|
||||
if (k) {
|
||||
const i = coalesced.get(k)
|
||||
if (i !== undefined) {
|
||||
queue[i] = { directory, payload }
|
||||
if (payload.type === "message.part.updated") {
|
||||
const part = payload.properties.part
|
||||
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
||||
}
|
||||
continue
|
||||
}
|
||||
coalesced.set(k, queue.length)
|
||||
}
|
||||
queue.push({ directory, payload })
|
||||
schedule()
|
||||
|
||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||
yielded = Date.now()
|
||||
await wait(0)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!aborted(error) && !streamErrorLogged) {
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream failed", {
|
||||
url: server.http.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
abort.signal.removeEventListener("abort", onAbort)
|
||||
attempt = undefined
|
||||
clearHeartbeat()
|
||||
}
|
||||
|
||||
if (abort.signal.aborted || !started) return
|
||||
await wait(RECONNECT_DELAY_MS)
|
||||
}
|
||||
})().finally(() => {
|
||||
run = undefined
|
||||
flush()
|
||||
})
|
||||
return run
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
started = false
|
||||
attempt?.abort()
|
||||
clearHeartbeat()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
makeEventListener(document, "visibilitychange", () => {
|
||||
if (document.visibilityState !== "visible") return
|
||||
if (!started) return
|
||||
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
||||
attempt?.abort()
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
stop()
|
||||
abort.abort()
|
||||
flush()
|
||||
})
|
||||
|
||||
const sdk = createSdkForServer({
|
||||
server: server.http,
|
||||
fetch: platform.fetch,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
return {
|
||||
url: server.http.url,
|
||||
client: sdk,
|
||||
event: {
|
||||
on: emitter.on.bind(emitter),
|
||||
listen: emitter.listen.bind(emitter),
|
||||
start,
|
||||
},
|
||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||
return createSdkForServer({
|
||||
server: server.http,
|
||||
fetch: platform.fetch,
|
||||
...opts,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({
|
||||
name: "ServerSDK",
|
||||
init: () => {
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
|
||||
if (!server.current) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
||||
const sdk = createServerSdkContext(server.current)
|
||||
return {
|
||||
...sdk,
|
||||
createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, sdk)),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
type SDKEventMap = {
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}
|
||||
|
||||
function createDirSdkContext(directory: string, serverSDK: ReturnType<typeof createServerSdkContext>) {
|
||||
const client = serverSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
const emitter = createGlobalEmitter<SDKEventMap>()
|
||||
|
||||
const unsub = serverSDK.event.on(directory, (event) => {
|
||||
emitter.emit(event.type, event)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
return {
|
||||
directory,
|
||||
client,
|
||||
event: emitter,
|
||||
get url() {
|
||||
return serverSDK.url
|
||||
},
|
||||
createClient(opts: Parameters<typeof serverSDK.createClient>[0]) {
|
||||
return serverSDK.createClient(opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
+30
-53
@@ -5,7 +5,7 @@ import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, u
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { InitError } from "../pages/error"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useServerSDK } from "./server-sdk"
|
||||
import {
|
||||
bootstrapDirectory,
|
||||
bootstrapGlobal,
|
||||
@@ -29,7 +29,8 @@ import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { directoryKey } from "./global-sync/utils"
|
||||
import { PathKey } from "@/utils/path-key"
|
||||
import { createDirSyncContext } from "./directory-sync"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||
import { createRefCountMap } from "@/utils/refcount"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -57,13 +58,13 @@ export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
|
||||
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
|
||||
})
|
||||
|
||||
function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
|
||||
function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
|
||||
return {
|
||||
globalConfig: () => loadGlobalConfigQuery(globalSDK()),
|
||||
projects: () => loadProjectsQuery(globalSDK()),
|
||||
globalConfig: () => loadGlobalConfigQuery(serverSDK()),
|
||||
projects: () => loadProjectsQuery(serverSDK()),
|
||||
providers: (directory: PathKey | null) =>
|
||||
loadProvidersQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
|
||||
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
|
||||
loadProvidersQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
|
||||
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
|
||||
agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
|
||||
mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
|
||||
lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
|
||||
@@ -72,11 +73,11 @@ function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: Path
|
||||
}
|
||||
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
export function createServerSyncContext() {
|
||||
const serverSDK = useServerSDK()
|
||||
const language = useLanguage()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
if (!owner) throw new Error("ServerSync must be created within owner")
|
||||
|
||||
const sdkCache = new Map<string, OpencodeClient>()
|
||||
const booting = new Map<string, Promise<void>>()
|
||||
@@ -87,7 +88,7 @@ function createGlobalSync() {
|
||||
const key = directoryKey(directory)
|
||||
const cached = sdkCache.get(key)
|
||||
if (cached) return cached
|
||||
const sdk = globalSDK.createClient({
|
||||
const sdk = serverSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
@@ -95,7 +96,7 @@ function createGlobalSync() {
|
||||
return sdk
|
||||
}
|
||||
|
||||
const queryOptionsApi = makeQueryOptionsApi(() => globalSDK.client, sdkFor)
|
||||
const queryOptionsApi = makeQueryOptionsApi(() => serverSDK.client, sdkFor)
|
||||
|
||||
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
||||
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
|
||||
@@ -154,7 +155,7 @@ function createGlobalSync() {
|
||||
queryKey: ["bootstrap"],
|
||||
queryFn: async () => {
|
||||
await bootstrapGlobal({
|
||||
globalSDK: globalSDK.client,
|
||||
serverSDK: serverSDK.client,
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
@@ -248,7 +249,7 @@ function createGlobalSync() {
|
||||
loadRootSessionsWithFallback({
|
||||
directory,
|
||||
limit,
|
||||
list: (query) => globalSDK.client.session.list(query),
|
||||
list: (query) => serverSDK.client.session.list(query),
|
||||
})
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
@@ -334,7 +335,7 @@ function createGlobalSync() {
|
||||
return promise
|
||||
}
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
const unsub = serverSDK.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const key = directoryKey(directory)
|
||||
const event = e.details
|
||||
@@ -393,13 +394,13 @@ function createGlobalSync() {
|
||||
eventFrame = undefined
|
||||
eventTimer = setTimeout(() => {
|
||||
eventTimer = undefined
|
||||
void globalSDK.event.start()
|
||||
void serverSDK.event.start()
|
||||
}, 0)
|
||||
})
|
||||
} else {
|
||||
eventTimer = setTimeout(() => {
|
||||
eventTimer = undefined
|
||||
void globalSDK.event.start()
|
||||
void serverSDK.event.start()
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
@@ -415,7 +416,7 @@ function createGlobalSync() {
|
||||
}
|
||||
|
||||
const updateConfigMutation = useMutation(() => ({
|
||||
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
|
||||
mutationFn: (config: Config) => serverSDK.client.global.config.update({ config }),
|
||||
onSuccess: () => {
|
||||
bootstrap.refetch()
|
||||
// Invalidate all provider queries so newly configured custom providers
|
||||
@@ -425,9 +426,6 @@ function createGlobalSync() {
|
||||
},
|
||||
}))
|
||||
|
||||
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSyncContext>>()
|
||||
const dirSyncContextRefCounts = new Map<string, number>()
|
||||
|
||||
return {
|
||||
data: globalStore,
|
||||
set,
|
||||
@@ -446,42 +444,21 @@ function createGlobalSync() {
|
||||
todo: {
|
||||
set: setSessionTodo,
|
||||
},
|
||||
createDirSyncContext: (directory: string) => {
|
||||
onCleanup(() => {
|
||||
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) - 1)
|
||||
if (dirSyncContextRefCounts.get(directory) === 0) {
|
||||
dirSyncContexts.delete(directory)
|
||||
dirSyncContextRefCounts.delete(directory)
|
||||
}
|
||||
})
|
||||
|
||||
const cached = dirSyncContexts.get(directory)
|
||||
if (cached) {
|
||||
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) + 1)
|
||||
return cached
|
||||
}
|
||||
const ctx = createDirSyncContext(globalSDK.createClient({ directory, throwOnError: true }), directory)
|
||||
dirSyncContexts.set(directory, ctx)
|
||||
dirSyncContextRefCounts.set(directory, 1)
|
||||
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
|
||||
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
|
||||
name: "ServerSync",
|
||||
init: () => {
|
||||
const sync = createServerSyncContext()
|
||||
|
||||
export function GlobalSyncProvider(props: ParentProps) {
|
||||
const value = createGlobalSync()
|
||||
return <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
|
||||
}
|
||||
|
||||
export function useGlobalSync() {
|
||||
const context = useContext(GlobalSyncContext)
|
||||
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
|
||||
return context
|
||||
}
|
||||
return {
|
||||
...sync,
|
||||
createDirSyncContext: createRefCountMap((dir) => createDirSyncContext(dir, sync)),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export function useQueryOptions() {
|
||||
return useGlobalSync().queryOptions
|
||||
return useServerSync().queryOptions
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
@@ -109,8 +109,8 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR
|
||||
}
|
||||
|
||||
export const useSync = () => {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const sdk = useSDK()
|
||||
|
||||
return globalSync.createDirSyncContext(sdk.directory)
|
||||
return serverSync.createDirSyncContext(sdk.directory)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { Iterable, pipe } from "effect"
|
||||
@@ -17,15 +17,15 @@ export const popularProviders = [
|
||||
const popularProviderSet = new Set(popularProviders)
|
||||
|
||||
export function useProviders() {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const params = useParams()
|
||||
const dir = createMemo(() => decode64(params.dir) ?? "")
|
||||
const providers = () => {
|
||||
if (dir()) {
|
||||
const [projectStore] = globalSync.child(dir())
|
||||
const [projectStore] = serverSync.child(dir())
|
||||
if (projectStore.provider_ready) return projectStore.provider
|
||||
}
|
||||
return globalSync.data.provider
|
||||
return serverSync.data.provider
|
||||
}
|
||||
return {
|
||||
all: () => providers().all,
|
||||
|
||||
@@ -413,7 +413,7 @@ export const dict = {
|
||||
"error.page.version": "الإصدار: {{version}}",
|
||||
"error.dev.rootNotFound":
|
||||
"لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟",
|
||||
"error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
|
||||
"error.serverSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
|
||||
"directory.error.invalidUrl": "دليل غير صالح في عنوان URL.",
|
||||
"error.chain.unknown": "خطأ غير معروف",
|
||||
"error.chain.causedBy": "بسبب:",
|
||||
@@ -837,8 +837,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "غير معروف",
|
||||
"error.page.circular": "[دائري]",
|
||||
"error.globalSDK.noServerAvailable": "لا يوجد خادم متاح",
|
||||
"error.globalSDK.serverNotAvailable": "الخادم غير متاح",
|
||||
"error.serverSDK.noServerAvailable": "لا يوجد خادم متاح",
|
||||
"error.serverSDK.serverNotAvailable": "الخادم غير متاح",
|
||||
"error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة",
|
||||
|
||||
@@ -414,7 +414,7 @@ export const dict = {
|
||||
"error.page.version": "Versão: {{version}}",
|
||||
"error.dev.rootNotFound":
|
||||
"Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?",
|
||||
"error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Diretório inválido na URL.",
|
||||
"error.chain.unknown": "Erro desconhecido",
|
||||
"error.chain.causedBy": "Causado por:",
|
||||
@@ -850,8 +850,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "desconhecido",
|
||||
"error.page.circular": "[Circular]",
|
||||
"error.globalSDK.noServerAvailable": "Nenhum servidor disponível",
|
||||
"error.globalSDK.serverNotAvailable": "Servidor indisponível",
|
||||
"error.serverSDK.noServerAvailable": "Nenhum servidor disponível",
|
||||
"error.serverSDK.serverNotAvailable": "Servidor indisponível",
|
||||
"error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente",
|
||||
|
||||
@@ -462,7 +462,7 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?",
|
||||
|
||||
"error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Nevažeći direktorij u URL-u.",
|
||||
|
||||
"error.chain.unknown": "Nepoznata greška",
|
||||
@@ -926,8 +926,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "nepoznato",
|
||||
"error.page.circular": "[Kružno]",
|
||||
"error.globalSDK.noServerAvailable": "Nema dostupnog servera",
|
||||
"error.globalSDK.serverNotAvailable": "Server nije dostupan",
|
||||
"error.serverSDK.noServerAvailable": "Nema dostupnog servera",
|
||||
"error.serverSDK.serverNotAvailable": "Server nije dostupan",
|
||||
"error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta",
|
||||
|
||||
@@ -459,7 +459,7 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?",
|
||||
|
||||
"error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Ugyldig mappe i URL.",
|
||||
|
||||
"error.chain.unknown": "Ukendt fejl",
|
||||
@@ -920,8 +920,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "ukendt",
|
||||
"error.page.circular": "[Cirkulær]",
|
||||
"error.globalSDK.noServerAvailable": "Ingen server tilgængelig",
|
||||
"error.globalSDK.serverNotAvailable": "Server ikke tilgængelig",
|
||||
"error.serverSDK.noServerAvailable": "Ingen server tilgængelig",
|
||||
"error.serverSDK.serverNotAvailable": "Server ikke tilgængelig",
|
||||
"error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon",
|
||||
|
||||
@@ -421,7 +421,7 @@ export const dict = {
|
||||
"error.page.version": "Version: {{version}}",
|
||||
"error.dev.rootNotFound":
|
||||
"Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?",
|
||||
"error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.",
|
||||
"error.chain.unknown": "Unbekannter Fehler",
|
||||
"error.chain.causedBy": "Verursacht durch:",
|
||||
@@ -862,8 +862,8 @@ export const dict = {
|
||||
"common.key.insert": "Einfg",
|
||||
"common.unknown": "unbekannt",
|
||||
"error.page.circular": "[Zirkulär]",
|
||||
"error.globalSDK.noServerAvailable": "Kein Server verfügbar",
|
||||
"error.globalSDK.serverNotAvailable": "Server nicht verfügbar",
|
||||
"error.serverSDK.noServerAvailable": "Kein Server verfügbar",
|
||||
"error.serverSDK.serverNotAvailable": "Server nicht verfügbar",
|
||||
"error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden",
|
||||
|
||||
@@ -482,9 +482,9 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||
|
||||
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
||||
"error.globalSDK.noServerAvailable": "No server available",
|
||||
"error.globalSDK.serverNotAvailable": "Server not available",
|
||||
"error.serverSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
||||
"error.serverSDK.noServerAvailable": "No server available",
|
||||
"error.serverSDK.serverNotAvailable": "Server not available",
|
||||
"error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon",
|
||||
|
||||
@@ -462,7 +462,7 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?",
|
||||
|
||||
"error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
|
||||
"directory.error.invalidUrl": "URL de directorio inválida.",
|
||||
|
||||
"error.chain.unknown": "Error desconocido",
|
||||
@@ -933,8 +933,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "desconocido",
|
||||
"error.page.circular": "[Circular]",
|
||||
"error.globalSDK.noServerAvailable": "Ningún servidor disponible",
|
||||
"error.globalSDK.serverNotAvailable": "Servidor no disponible",
|
||||
"error.serverSDK.noServerAvailable": "Ningún servidor disponible",
|
||||
"error.serverSDK.serverNotAvailable": "Servidor no disponible",
|
||||
"error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente",
|
||||
|
||||
@@ -417,7 +417,7 @@ export const dict = {
|
||||
"error.page.version": "Version : {{version}}",
|
||||
"error.dev.rootNotFound":
|
||||
"Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?",
|
||||
"error.globalSync.connectFailed":
|
||||
"error.serverSync.connectFailed":
|
||||
"Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?",
|
||||
"directory.error.invalidUrl": "Répertoire invalide dans l'URL.",
|
||||
"error.chain.unknown": "Erreur inconnue",
|
||||
@@ -860,8 +860,8 @@ export const dict = {
|
||||
"common.key.insert": "Inser",
|
||||
"common.unknown": "inconnu",
|
||||
"error.page.circular": "[Circulaire]",
|
||||
"error.globalSDK.noServerAvailable": "Aucun serveur disponible",
|
||||
"error.globalSDK.serverNotAvailable": "Serveur non disponible",
|
||||
"error.serverSDK.noServerAvailable": "Aucun serveur disponible",
|
||||
"error.serverSDK.serverNotAvailable": "Serveur non disponible",
|
||||
"error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed":
|
||||
"Échec de la création des métadonnées de projet persistantes",
|
||||
|
||||
@@ -413,7 +413,7 @@ export const dict = {
|
||||
"error.page.version": "バージョン: {{version}}",
|
||||
"error.dev.rootNotFound":
|
||||
"ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?",
|
||||
"error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?",
|
||||
"error.serverSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?",
|
||||
"directory.error.invalidUrl": "URL内のディレクトリが無効です。",
|
||||
"error.chain.unknown": "不明なエラー",
|
||||
"error.chain.causedBy": "原因:",
|
||||
@@ -844,8 +844,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "不明",
|
||||
"error.page.circular": "[循環]",
|
||||
"error.globalSDK.noServerAvailable": "利用可能なサーバーがありません",
|
||||
"error.globalSDK.serverNotAvailable": "サーバーが利用できません",
|
||||
"error.serverSDK.noServerAvailable": "利用可能なサーバーがありません",
|
||||
"error.serverSDK.serverNotAvailable": "サーバーが利用できません",
|
||||
"error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました",
|
||||
|
||||
@@ -412,7 +412,7 @@ export const dict = {
|
||||
"error.page.version": "버전: {{version}}",
|
||||
"error.dev.rootNotFound":
|
||||
"루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.",
|
||||
"error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?",
|
||||
"error.serverSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?",
|
||||
"directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.",
|
||||
"error.chain.unknown": "알 수 없는 오류",
|
||||
"error.chain.causedBy": "원인:",
|
||||
@@ -839,8 +839,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "알 수 없음",
|
||||
"error.page.circular": "[순환]",
|
||||
"error.globalSDK.noServerAvailable": "사용 가능한 서버 없음",
|
||||
"error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음",
|
||||
"error.serverSDK.noServerAvailable": "사용 가능한 서버 없음",
|
||||
"error.serverSDK.serverNotAvailable": "서버를 사용할 수 없음",
|
||||
"error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패",
|
||||
|
||||
@@ -463,7 +463,7 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?",
|
||||
|
||||
"error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Invalid directory in URL.",
|
||||
|
||||
"error.chain.unknown": "Ukjent feil",
|
||||
@@ -927,8 +927,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "ukjent",
|
||||
"error.page.circular": "[Sirkulær]",
|
||||
"error.globalSDK.noServerAvailable": "Ingen server tilgjengelig",
|
||||
"error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig",
|
||||
"error.serverSDK.noServerAvailable": "Ingen server tilgjengelig",
|
||||
"error.serverSDK.serverNotAvailable": "Server ikke tilgjengelig",
|
||||
"error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon",
|
||||
|
||||
@@ -414,7 +414,7 @@ export const dict = {
|
||||
"error.page.version": "Wersja: {{version}}",
|
||||
"error.dev.rootNotFound":
|
||||
"Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?",
|
||||
"error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Nieprawidłowy katalog w URL.",
|
||||
"error.chain.unknown": "Nieznany błąd",
|
||||
"error.chain.causedBy": "Spowodowany przez:",
|
||||
@@ -848,8 +848,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "nieznany",
|
||||
"error.page.circular": "[Cykliczne]",
|
||||
"error.globalSDK.noServerAvailable": "Brak dostępnego serwera",
|
||||
"error.globalSDK.serverNotAvailable": "Serwer niedostępny",
|
||||
"error.serverSDK.noServerAvailable": "Brak dostępnego serwera",
|
||||
"error.serverSDK.serverNotAvailable": "Serwer niedostępny",
|
||||
"error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu",
|
||||
|
||||
@@ -461,7 +461,7 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?",
|
||||
|
||||
"error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
|
||||
"error.serverSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
|
||||
"directory.error.invalidUrl": "Недопустимая директория в URL.",
|
||||
|
||||
"error.chain.unknown": "Неизвестная ошибка",
|
||||
@@ -929,8 +929,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "неизвестно",
|
||||
"error.page.circular": "[Циклично]",
|
||||
"error.globalSDK.noServerAvailable": "Нет доступного сервера",
|
||||
"error.globalSDK.serverNotAvailable": "Сервер недоступен",
|
||||
"error.serverSDK.noServerAvailable": "Нет доступного сервера",
|
||||
"error.serverSDK.serverNotAvailable": "Сервер недоступен",
|
||||
"error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта",
|
||||
|
||||
@@ -459,7 +459,7 @@ export const dict = {
|
||||
|
||||
"error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?",
|
||||
|
||||
"error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
|
||||
"error.serverSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
|
||||
"directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง",
|
||||
|
||||
"error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก",
|
||||
@@ -916,8 +916,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "ไม่ทราบ",
|
||||
"error.page.circular": "[วงกลม]",
|
||||
"error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์",
|
||||
"error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน",
|
||||
"error.serverSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์",
|
||||
"error.serverSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน",
|
||||
"error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร",
|
||||
|
||||
@@ -465,7 +465,7 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?",
|
||||
|
||||
"error.globalSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?",
|
||||
"error.serverSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?",
|
||||
"directory.error.invalidUrl": "URL'de geçersiz dizin.",
|
||||
|
||||
"error.chain.unknown": "Bilinmeyen hata",
|
||||
@@ -935,8 +935,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "bilinmiyor",
|
||||
"error.page.circular": "[Döngüsel]",
|
||||
"error.globalSDK.noServerAvailable": "Sunucu yok",
|
||||
"error.globalSDK.serverNotAvailable": "Sunucu mevcut değil",
|
||||
"error.serverSDK.noServerAvailable": "Sunucu yok",
|
||||
"error.serverSDK.serverNotAvailable": "Sunucu mevcut değil",
|
||||
"error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı",
|
||||
|
||||
@@ -483,9 +483,9 @@ export const dict = {
|
||||
"error.dev.rootNotFound":
|
||||
"Кореневий елемент не знайдено. Ви забули додати його до index.html? Або, можливо, атрибут id було написано з помилкою?",
|
||||
|
||||
"error.globalSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?",
|
||||
"error.globalSDK.noServerAvailable": "Сервер недоступний",
|
||||
"error.globalSDK.serverNotAvailable": "Сервер недоступний",
|
||||
"error.serverSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?",
|
||||
"error.serverSDK.noServerAvailable": "Сервер недоступний",
|
||||
"error.serverSDK.serverNotAvailable": "Сервер недоступний",
|
||||
"error.childStore.persistedCacheCreateFailed": "Не вдалося створити постійний кеш",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "Не вдалося створити постійні метадані проєкту",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "Не вдалося створити постійну іконку проєкту",
|
||||
|
||||
@@ -462,7 +462,7 @@ export const dict = {
|
||||
"error.page.report.discord": "在 Discord 上",
|
||||
"error.page.version": "版本:{{version}}",
|
||||
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?",
|
||||
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
||||
"error.serverSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
||||
|
||||
"directory.error.invalidUrl": "URL 中的目录无效。",
|
||||
|
||||
@@ -919,8 +919,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "未知",
|
||||
"error.page.circular": "[循环]",
|
||||
"error.globalSDK.noServerAvailable": "无可用服务器",
|
||||
"error.globalSDK.serverNotAvailable": "服务器不可用",
|
||||
"error.serverSDK.noServerAvailable": "无可用服务器",
|
||||
"error.serverSDK.serverNotAvailable": "服务器不可用",
|
||||
"error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
|
||||
|
||||
@@ -457,7 +457,7 @@ export const dict = {
|
||||
|
||||
"error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?",
|
||||
|
||||
"error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
|
||||
"error.serverSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
|
||||
"directory.error.invalidUrl": "URL 中的目錄無效。",
|
||||
|
||||
"error.chain.unknown": "未知錯誤",
|
||||
@@ -907,8 +907,8 @@ export const dict = {
|
||||
"common.key.insert": "Insert",
|
||||
"common.unknown": "未知",
|
||||
"error.page.circular": "[循環]",
|
||||
"error.globalSDK.noServerAvailable": "無可用的伺服器",
|
||||
"error.globalSDK.serverNotAvailable": "伺服器無法使用",
|
||||
"error.serverSDK.noServerAvailable": "無可用的伺服器",
|
||||
"error.serverSDK.serverNotAvailable": "伺服器無法使用",
|
||||
"error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗",
|
||||
"error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗",
|
||||
"error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗",
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
@@ -55,7 +55,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
function HomeDesign() {
|
||||
const sync = useGlobalSync()
|
||||
const sync = useServerSync()
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
@@ -358,10 +358,10 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
|
||||
}
|
||||
|
||||
function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const [sessionStore] = globalSync.child(props.record.session.directory, { bootstrap: false })
|
||||
const [sessionStore] = serverSync.child(props.record.session.directory, { bootstrap: false })
|
||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id))
|
||||
const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id))
|
||||
@@ -459,7 +459,7 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof
|
||||
}
|
||||
|
||||
function LegacyHome() {
|
||||
const sync = useGlobalSync()
|
||||
const sync = useServerSync()
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -16,7 +16,7 @@ import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { useLayout, LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
@@ -35,7 +35,7 @@ import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, close
|
||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
||||
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
||||
import {
|
||||
@@ -113,8 +113,8 @@ export default function Layout(props: ParentProps) {
|
||||
let dialogDead = false
|
||||
|
||||
const params = useParams()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const layout = useLayout()
|
||||
const layoutReady = createMemo(() => layout.ready())
|
||||
const platform = usePlatform()
|
||||
@@ -136,7 +136,7 @@ export default function Layout(props: ParentProps) {
|
||||
if (!slug) return { slug, dir: "" }
|
||||
const dir = decode64(slug)
|
||||
if (!dir) return { slug, dir: "" }
|
||||
const store = globalSync.peek(dir, { bootstrap: false })
|
||||
const store = serverSync.peek(dir, { bootstrap: false })
|
||||
return {
|
||||
slug,
|
||||
store,
|
||||
@@ -226,7 +226,7 @@ export default function Layout(props: ParentProps) {
|
||||
active: () => state.hoverProject,
|
||||
el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav,
|
||||
onActivate: (directory) => {
|
||||
globalSync.child(directory)
|
||||
serverSync.child(directory)
|
||||
setState("hoverProject", directory)
|
||||
},
|
||||
})
|
||||
@@ -410,7 +410,7 @@ export default function Layout(props: ParentProps) {
|
||||
alertedAtBySession.delete(sessionKey)
|
||||
}
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
const unsub = serverSDK.event.listen((e) => {
|
||||
if (e.details?.type === "worktree.ready") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.ready(e.name)
|
||||
@@ -444,7 +444,7 @@ export default function Layout(props: ParentProps) {
|
||||
const props = e.details.properties
|
||||
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
|
||||
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
||||
const session = store.session.find((s) => s.id === props.sessionID)
|
||||
const sessionKey = `${directory}:${props.sessionID}`
|
||||
|
||||
@@ -507,7 +507,7 @@ export default function Layout(props: ParentProps) {
|
||||
if (!currentDir() || !currentSession) return
|
||||
const sessionKey = `${currentDir()}:${currentSession}`
|
||||
dismissSessionAlert(sessionKey)
|
||||
const [store] = globalSync.child(currentDir(), { bootstrap: false })
|
||||
const [store] = serverSync.child(currentDir(), { bootstrap: false })
|
||||
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
||||
for (const child of childSessions) {
|
||||
dismissSessionAlert(`${currentDir()}:${child.id}`)
|
||||
@@ -545,11 +545,11 @@ export default function Layout(props: ParentProps) {
|
||||
const direct = projects.find((p) => pathKey(p.worktree) === key)
|
||||
if (direct) return direct
|
||||
|
||||
const [child] = globalSync.child(directory, { bootstrap: false })
|
||||
const [child] = serverSync.child(directory, { bootstrap: false })
|
||||
const id = child.project
|
||||
if (!id) return
|
||||
|
||||
const meta = globalSync.data.project.find((p) => p.id === id)
|
||||
const meta = serverSync.data.project.find((p) => p.id === id)
|
||||
const root = meta?.worktree
|
||||
if (!root) return
|
||||
|
||||
@@ -640,7 +640,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const result: Session[] = []
|
||||
for (const dir of dirs) {
|
||||
const [dirStore] = globalSync.child(dir, { bootstrap: true })
|
||||
const [dirStore] = serverSync.child(dir, { bootstrap: true })
|
||||
const dirSessions = sortedRootSessions(dirStore, now)
|
||||
result.push(...dirSessions)
|
||||
}
|
||||
@@ -692,7 +692,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
createEffect(() => {
|
||||
route()
|
||||
globalSDK.url
|
||||
serverSDK.url
|
||||
|
||||
prefetchToken.value += 1
|
||||
clearSessionPrefetchInflight()
|
||||
@@ -739,13 +739,13 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
||||
const [store, setStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const [store, setStore] = serverSync.child(directory, { bootstrap: false })
|
||||
|
||||
return runSessionPrefetch({
|
||||
directory,
|
||||
sessionID,
|
||||
task: (rev) =>
|
||||
retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
||||
retry(() => serverSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
|
||||
.then((messages) => {
|
||||
if (prefetchToken.value !== token) return
|
||||
if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return
|
||||
@@ -765,7 +765,7 @@ export default function Layout(props: ParentProps) {
|
||||
if (stale.length > 0) {
|
||||
clearSessionPrefetch(directory, stale)
|
||||
for (const id of stale) {
|
||||
globalSync.todo.set(id, undefined)
|
||||
serverSync.todo.set(id, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,7 +830,7 @@ export default function Layout(props: ParentProps) {
|
||||
const directory = session.directory
|
||||
if (!directory) return
|
||||
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
||||
const cached = untrack(() => {
|
||||
const info = getSessionPrefetch(directory, session.id)
|
||||
return shouldSkipSessionPrefetch({
|
||||
@@ -935,7 +935,7 @@ export default function Layout(props: ParentProps) {
|
||||
if (!target) return
|
||||
|
||||
// warm up child store to prevent flicker
|
||||
globalSync.child(target.worktree)
|
||||
serverSync.child(target.worktree)
|
||||
void openProject(target.worktree)
|
||||
}
|
||||
|
||||
@@ -944,7 +944,7 @@ export default function Layout(props: ParentProps) {
|
||||
const target = projects[index]
|
||||
if (!target) return
|
||||
|
||||
globalSync.child(target.worktree)
|
||||
serverSync.child(target.worktree)
|
||||
void openProject(target.worktree)
|
||||
}
|
||||
|
||||
@@ -973,12 +973,12 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
async function archiveSession(session: Session) {
|
||||
const [store, setStore] = globalSync.child(session.directory)
|
||||
const [store, setStore] = serverSync.child(session.directory)
|
||||
const sessions = store.session ?? []
|
||||
const index = sessions.findIndex((s) => s.id === session.id)
|
||||
const nextSession = sessions[index + 1] ?? sessions[index - 1]
|
||||
|
||||
await globalSDK.client.session.update({
|
||||
await serverSDK.client.session.update({
|
||||
directory: session.directory,
|
||||
sessionID: session.id,
|
||||
time: { archived: Date.now() },
|
||||
@@ -1246,11 +1246,11 @@ export default function Layout(props: ParentProps) {
|
||||
)
|
||||
if (known) return known[0]
|
||||
|
||||
const [child] = globalSync.child(directory, { bootstrap: false })
|
||||
const [child] = serverSync.child(directory, { bootstrap: false })
|
||||
const id = child.project
|
||||
if (!id) return directory
|
||||
|
||||
const meta = globalSync.data.project.find((item) => item.id === id)
|
||||
const meta = serverSync.data.project.find((item) => item.id === id)
|
||||
return meta?.worktree ?? directory
|
||||
}
|
||||
|
||||
@@ -1298,7 +1298,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
const refreshDirs = async (target?: string) => {
|
||||
if (!target || target === root || canOpen(target)) return canOpen(target)
|
||||
const listed = await globalSDK.client.worktree
|
||||
const listed = await serverSDK.client.worktree
|
||||
.list({ directory: root })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [] as string[])
|
||||
@@ -1307,13 +1307,13 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
const openSession = async (target: { directory: string; id: string }) => {
|
||||
if (!canOpen(target.directory)) return false
|
||||
const [data] = globalSync.child(target.directory, { bootstrap: false })
|
||||
const [data] = serverSync.child(target.directory, { bootstrap: false })
|
||||
if (data.session.some((item) => item.id === target.id)) {
|
||||
setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
|
||||
navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
|
||||
return true
|
||||
}
|
||||
const resolved = await globalSDK.client.session
|
||||
const resolved = await serverSDK.client.session
|
||||
.get({ sessionID: target.id })
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
@@ -1333,7 +1333,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
const latest = latestRootSession(
|
||||
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
|
||||
dirs.map((item) => serverSync.child(item, { bootstrap: false })[0]),
|
||||
Date.now(),
|
||||
)
|
||||
if (latest && (await openSession(latest))) {
|
||||
@@ -1344,7 +1344,7 @@ export default function Layout(props: ParentProps) {
|
||||
await Promise.all(
|
||||
dirs.map(async (item) => ({
|
||||
path: { directory: item },
|
||||
session: await globalSDK.client.session
|
||||
session: await serverSDK.client.session
|
||||
.list({ directory: item })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => []),
|
||||
@@ -1405,11 +1405,11 @@ export default function Layout(props: ParentProps) {
|
||||
const name = next === getFilename(project.worktree) ? "" : next
|
||||
|
||||
if (project.id && project.id !== "global") {
|
||||
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
|
||||
await serverSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.project.meta(project.worktree, { name })
|
||||
serverSync.project.meta(project.worktree, { name })
|
||||
}
|
||||
|
||||
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
|
||||
@@ -1506,7 +1506,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
setBusy(directory, true)
|
||||
|
||||
const result = await globalSDK.client.worktree
|
||||
const result = await serverSDK.client.worktree
|
||||
.remove({ directory: root, worktreeRemoveInput: { directory } })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
@@ -1525,7 +1525,7 @@ export default function Layout(props: ParentProps) {
|
||||
clearLastProjectSession(root)
|
||||
}
|
||||
|
||||
globalSync.set(
|
||||
serverSync.set(
|
||||
"project",
|
||||
produce((draft) => {
|
||||
const project = draft.find((item) => item.worktree === root)
|
||||
@@ -1564,7 +1564,7 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
const dismiss = () => toaster.dismiss(progress)
|
||||
|
||||
const sessions: Session[] = await globalSDK.client.session
|
||||
const sessions: Session[] = await serverSDK.client.session
|
||||
.list({ directory })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
@@ -1575,9 +1575,9 @@ export default function Layout(props: ParentProps) {
|
||||
platform,
|
||||
getTerminalServerScope(server.current, server.key),
|
||||
)
|
||||
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||
await serverSDK.client.instance.dispose({ directory }).catch(() => undefined)
|
||||
|
||||
const result = await globalSDK.client.worktree
|
||||
const result = await serverSDK.client.worktree
|
||||
.reset({ directory: root, worktreeResetInput: { directory } })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
@@ -1599,7 +1599,7 @@ export default function Layout(props: ParentProps) {
|
||||
sessions
|
||||
.filter((session) => session.time.archived === undefined)
|
||||
.map((session) =>
|
||||
globalSDK.client.session
|
||||
serverSDK.client.session
|
||||
.update({
|
||||
sessionID: session.id,
|
||||
directory: session.directory,
|
||||
@@ -1640,7 +1640,7 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
globalSDK.client.file
|
||||
serverSDK.client.file
|
||||
.status({ directory: props.directory })
|
||||
.then((x) => {
|
||||
const files = x.data ?? []
|
||||
@@ -1699,7 +1699,7 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
const sessions = await globalSDK.client.session
|
||||
const sessions = await serverSDK.client.session
|
||||
.list({ directory: props.directory })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
@@ -1708,7 +1708,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
globalSDK.client.file
|
||||
serverSDK.client.file
|
||||
.status({ directory: props.directory })
|
||||
.then((x) => {
|
||||
const files = x.data ?? []
|
||||
@@ -1842,7 +1842,7 @@ export default function Layout(props: ParentProps) {
|
||||
const next = new Set(dirs)
|
||||
for (const directory of next) {
|
||||
if (loadedSessionDirs.has(directory)) continue
|
||||
void globalSync.project.loadSessions(directory)
|
||||
void serverSync.project.loadSessions(directory)
|
||||
}
|
||||
|
||||
loadedSessionDirs.clear()
|
||||
@@ -1939,7 +1939,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const createWorkspace = async (project: LocalProject) => {
|
||||
clearSidebarHoverState()
|
||||
const created = await globalSDK.client.worktree
|
||||
const created = await serverSDK.client.worktree
|
||||
.create({ directory: project.worktree })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
@@ -1973,7 +1973,7 @@ export default function Layout(props: ParentProps) {
|
||||
return [created.directory, ...next]
|
||||
})
|
||||
|
||||
globalSync.child(created.directory)
|
||||
serverSync.child(created.directory)
|
||||
navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`)
|
||||
}
|
||||
|
||||
@@ -2078,7 +2078,7 @@ export default function Layout(props: ParentProps) {
|
||||
if (!item) return false
|
||||
return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
|
||||
})
|
||||
const homedir = createMemo(() => globalSync.data.path.home)
|
||||
const homedir = createMemo(() => serverSync.data.path.home)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { A, useParams } from "@solidjs/router"
|
||||
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||
import { useNotification } from "@/context/notification"
|
||||
@@ -23,7 +23,7 @@ export const ProjectIcon = (props: {
|
||||
notify?: boolean
|
||||
working?: boolean
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
|
||||
@@ -33,7 +33,7 @@ export const ProjectIcon = (props: {
|
||||
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
||||
const hasPermissions = createMemo(() =>
|
||||
dirs().some((directory) => {
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
||||
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
|
||||
}),
|
||||
)
|
||||
@@ -146,10 +146,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
const language = useLanguage()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
|
||||
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
||||
const [sessionStore] = globalSync.child(props.session.directory)
|
||||
const [sessionStore] = serverSync.child(props.session.directory)
|
||||
const hasPermissions = createMemo(() => {
|
||||
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
|
||||
return !permission.autoResponds(item, props.session.directory)
|
||||
|
||||
@@ -7,7 +7,7 @@ import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||
import { useLayout, type LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
|
||||
@@ -274,7 +274,7 @@ export const SortableProject = (props: {
|
||||
ctx: ProjectSidebarContext
|
||||
sortNow: Accessor<number>
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const language = useLanguage()
|
||||
const sortable = createSortable(props.project.worktree)
|
||||
const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
|
||||
@@ -294,23 +294,23 @@ export const SortableProject = (props: {
|
||||
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
|
||||
|
||||
const label = (directory: string) => {
|
||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||
const [data] = serverSync.child(directory, { bootstrap: false })
|
||||
const kind =
|
||||
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
||||
const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id)
|
||||
return `${kind} : ${name}`
|
||||
}
|
||||
|
||||
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
|
||||
const projectStore = createMemo(() => serverSync.child(props.project.worktree, { bootstrap: false })[0])
|
||||
const isWorking = createMemo(() =>
|
||||
dirs().some((directory) => {
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const [store] = serverSync.child(directory, { bootstrap: false })
|
||||
return Object.keys(store.session_status).some((id) => store.session_working(id))
|
||||
}),
|
||||
)
|
||||
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
|
||||
const workspaceSessions = (directory: string) => {
|
||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||
const [data] = serverSync.child(directory, { bootstrap: false })
|
||||
return sortedRootSessions(data, props.sortNow())
|
||||
}
|
||||
const tile = () => (
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { type LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync, useQueryOptions } from "@/context/global-sync"
|
||||
import { useServerSync, useQueryOptions } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||
@@ -60,7 +60,7 @@ export const WorkspaceDragOverlay = (props: {
|
||||
activeWorkspace: Accessor<string | undefined>
|
||||
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const language = useLanguage()
|
||||
const label = createMemo(() => {
|
||||
const project = props.sidebarProject()
|
||||
@@ -68,7 +68,7 @@ export const WorkspaceDragOverlay = (props: {
|
||||
const directory = props.activeWorkspace()
|
||||
if (!directory) return
|
||||
|
||||
const [workspaceStore] = globalSync.child(directory, { bootstrap: false })
|
||||
const [workspaceStore] = serverSync.child(directory, { bootstrap: false })
|
||||
const kind =
|
||||
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
|
||||
const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
|
||||
@@ -299,11 +299,11 @@ export const SortableWorkspace = (props: {
|
||||
}): JSX.Element => {
|
||||
const navigate = useNavigate()
|
||||
const params = useParams()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const queryOptions = useQueryOptions()
|
||||
const language = useLanguage()
|
||||
const sortable = createSortable(props.directory)
|
||||
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
|
||||
const [workspaceStore, setWorkspaceStore] = serverSync.child(props.directory, { bootstrap: false })
|
||||
const [menu, setMenu] = createStore({
|
||||
open: false,
|
||||
pendingRename: false,
|
||||
@@ -328,7 +328,7 @@ export const SortableWorkspace = (props: {
|
||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(props.directory)
|
||||
await serverSync.project.loadSessions(props.directory)
|
||||
}
|
||||
|
||||
const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`))
|
||||
@@ -357,7 +357,7 @@ export const SortableWorkspace = (props: {
|
||||
|
||||
createEffect(() => {
|
||||
if (!boot()) return
|
||||
globalSync.child(props.directory, { bootstrap: true })
|
||||
serverSync.child(props.directory, { bootstrap: true })
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -446,11 +446,11 @@ export const LocalWorkspace = (props: {
|
||||
sortNow: Accessor<number>
|
||||
mobile?: boolean
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const queryOptions = useQueryOptions()
|
||||
const language = useLanguage()
|
||||
const workspace = createMemo(() => {
|
||||
const [store, setStore] = globalSync.child(props.project.worktree)
|
||||
const [store, setStore] = serverSync.child(props.project.worktree)
|
||||
return { store, setStore }
|
||||
})
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
@@ -461,7 +461,7 @@ export const LocalWorkspace = (props: {
|
||||
const loading = () => fetching() > 0 && count() === 0
|
||||
const loadMore = async () => {
|
||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(props.project.worktree)
|
||||
await serverSync.project.loadSessions(props.project.worktree)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,7 +34,7 @@ import { useLocation, useSearchParams } from "@solidjs/router"
|
||||
import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
@@ -182,7 +182,7 @@ function createSessionHistoryLoader(input: SessionHistoryWindowInput) {
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
const file = useFile()
|
||||
@@ -560,11 +560,11 @@ export default function Page() {
|
||||
}
|
||||
|
||||
function upsert(next: Project) {
|
||||
const list = globalSync.data.project
|
||||
const list = serverSync.data.project
|
||||
sync.set("project", next.id)
|
||||
const idx = list.findIndex((item) => item.id === next.id)
|
||||
if (idx >= 0) {
|
||||
globalSync.set(
|
||||
serverSync.set(
|
||||
"project",
|
||||
list.map((item, i) => (i === idx ? { ...item, ...next } : item)),
|
||||
)
|
||||
@@ -572,10 +572,10 @@ export default function Page() {
|
||||
}
|
||||
const at = list.findIndex((item) => item.id > next.id)
|
||||
if (at >= 0) {
|
||||
globalSync.set("project", [...list.slice(0, at), next, ...list.slice(at)])
|
||||
serverSync.set("project", [...list.slice(0, at), next, ...list.slice(at)])
|
||||
return
|
||||
}
|
||||
globalSync.set("project", [...list, next])
|
||||
serverSync.set("project", [...list, next])
|
||||
}
|
||||
|
||||
const gitMutation = useMutation(() => ({
|
||||
@@ -673,7 +673,7 @@ export default function Page() {
|
||||
todoTimer = undefined
|
||||
if (!id) return
|
||||
if (status === "idle" && !blocked) return
|
||||
const cached = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined)
|
||||
const cached = untrack(() => sync.data.todo[id] !== undefined || serverSync.data.session_todo[id] !== undefined)
|
||||
|
||||
todoFrame = requestAnimationFrame(() => {
|
||||
todoFrame = undefined
|
||||
@@ -1386,7 +1386,7 @@ export default function Page() {
|
||||
const ok = await sendFollowupDraft({
|
||||
client: sdk.client,
|
||||
sync,
|
||||
globalSync,
|
||||
serverSync,
|
||||
draft: item,
|
||||
optimisticBusy: item.sessionDirectory === sdk.directory,
|
||||
}).catch((err) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createStore } from "solid-js/store"
|
||||
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -27,7 +27,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
||||
const params = useParams()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const globalSync = useGlobalSync()
|
||||
const serverSync = useServerSync()
|
||||
const language = useLanguage()
|
||||
const permission = usePermission()
|
||||
|
||||
@@ -50,7 +50,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
||||
const todos = createMemo((): Todo[] => {
|
||||
const id = params.id
|
||||
if (!id) return []
|
||||
return globalSync.data.session_todo[id] ?? []
|
||||
return serverSync.data.session_todo[id] ?? []
|
||||
})
|
||||
|
||||
const done = createMemo(
|
||||
@@ -111,7 +111,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
||||
const clear = () => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
globalSync.todo.set(id, [])
|
||||
serverSync.todo.set(id, [])
|
||||
sync.set("todo", id, [])
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ export type TimelineRowMap = {
|
||||
userMessageID: string
|
||||
group: PartGroup
|
||||
previousAssistantPart: boolean
|
||||
lastAssistantPart: boolean
|
||||
}
|
||||
Thinking: { userMessageID: string; reasoningHeading?: string }
|
||||
Retry: { userMessageID: string }
|
||||
@@ -50,7 +49,6 @@ export namespace TimelineRow {
|
||||
userMessageID: string
|
||||
group: PartGroup
|
||||
previousAssistantPart: boolean
|
||||
lastAssistantPart: boolean
|
||||
}> {}
|
||||
export class Thinking extends Data.TaggedClass("Thinking")<{
|
||||
userMessageID: string
|
||||
@@ -151,8 +149,6 @@ export namespace Timeline {
|
||||
),
|
||||
]
|
||||
: groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group }))
|
||||
const assistantGroupCount = assistantItems.filter((item) => item.type === "part").length
|
||||
|
||||
if (comments.length > 0)
|
||||
rows.push(
|
||||
new TimelineRow.CommentStrip({
|
||||
@@ -195,7 +191,6 @@ export namespace Timeline {
|
||||
userMessageID: userMessage.id,
|
||||
group: item.group,
|
||||
previousAssistantPart: assistantGroupIndex > 0,
|
||||
lastAssistantPart: assistantGroupIndex === assistantGroupCount - 1,
|
||||
}),
|
||||
)
|
||||
assistantGroupIndex += 1
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { createEffect, createMemo, createSignal, For, Index, on, onCleanup, Show, mapArray, type JSX } from "solid-js"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Index,
|
||||
on,
|
||||
onCleanup,
|
||||
Show,
|
||||
mapArray,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
@@ -48,7 +60,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -245,7 +257,7 @@ function TimelineDiffView(props: { diff: SummaryDiff }) {
|
||||
|
||||
return (
|
||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} />
|
||||
<Dynamic component={fileComponent} mode="diff" virtualize={false} fileDiff={view.fileDiff} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -273,7 +285,7 @@ export function MessageTimeline(props: {
|
||||
let touchGesture: number | undefined
|
||||
|
||||
const navigate = useNavigate()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const serverSDK = useServerSDK()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const settings = useSettings()
|
||||
@@ -415,8 +427,7 @@ export function MessageTimeline(props: {
|
||||
if (rows.length === 0) return rows
|
||||
return reuseTimelineRows(previous, [...rows, new TimelineRow.BottomSpacer()])
|
||||
})
|
||||
const timelineRowByKey = createMemo(() => new Map(timelineRows().map((row) => [TimelineRow.key(row), row] as const)))
|
||||
const timelineRowKeys = createMemo(() => [...timelineRowByKey().keys()], [] as string[], { equals: sameKeys })
|
||||
const timelineRowKeys = createMemo(() => timelineRows().map(TimelineRow.key), [] as string[], { equals: sameKeys })
|
||||
const virtualCache = createMemo(() => readTimelineCache(sessionKey(), timelineRowKeys()))
|
||||
const messageRowIndex = createMemo(() => {
|
||||
const result = new Map<string, number>()
|
||||
@@ -427,6 +438,14 @@ export function MessageTimeline(props: {
|
||||
})
|
||||
return result
|
||||
})
|
||||
const lastAssistantGroupKey = createMemo(() => {
|
||||
const result = new Map<string, string>()
|
||||
timelineRows().forEach((row) => {
|
||||
if (row._tag !== "AssistantPart") return
|
||||
result.set(row.userMessageID, row.group.key)
|
||||
})
|
||||
return result
|
||||
})
|
||||
const keepMounted = createMemo(() => {
|
||||
const id = activeMessageID()
|
||||
if (!id) return
|
||||
@@ -540,6 +559,7 @@ export function MessageTimeline(props: {
|
||||
const [bar, setBar] = createStore({
|
||||
ms: pace(640),
|
||||
})
|
||||
const [toolOpen, setToolOpen] = createStore<Record<string, boolean | undefined>>({})
|
||||
|
||||
let more: HTMLButtonElement | undefined
|
||||
let head: HTMLDivElement | undefined
|
||||
@@ -560,6 +580,11 @@ export function MessageTimeline(props: {
|
||||
|
||||
const isMeasuredBottom = (root: HTMLDivElement) => root.scrollHeight - root.clientHeight - root.scrollTop <= 4
|
||||
|
||||
const measureTimeline = () => {
|
||||
virtualizer?.measure()
|
||||
anchorMeasuredBottom()
|
||||
}
|
||||
|
||||
function anchorMeasuredBottom() {
|
||||
if (!listRoot) return false
|
||||
if (!measuredBottomAnchored) return false
|
||||
@@ -704,14 +729,14 @@ export function MessageTimeline(props: {
|
||||
}
|
||||
|
||||
const shareMutation = useMutation(() => ({
|
||||
mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }),
|
||||
mutationFn: (id: string) => serverSDK.client.session.share({ sessionID: id, directory: sdk.directory }),
|
||||
onError: (err) => {
|
||||
console.error("Failed to share session", err)
|
||||
},
|
||||
}))
|
||||
|
||||
const unshareMutation = useMutation(() => ({
|
||||
mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }),
|
||||
mutationFn: (id: string) => serverSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }),
|
||||
onError: (err) => {
|
||||
console.error("Failed to unshare session", err)
|
||||
},
|
||||
@@ -980,29 +1005,42 @@ export function MessageTimeline(props: {
|
||||
|
||||
const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
|
||||
|
||||
const renderAssistantPartGroup = (row: TimelineRowMap["AssistantPart"]) => {
|
||||
if (row.group.type === "context") {
|
||||
const renderAssistantPartGroup = (row: Accessor<TimelineRowMap["AssistantPart"]>) => {
|
||||
if (row().group.type === "context") {
|
||||
const parts = createMemo(() => {
|
||||
const group = row.group
|
||||
const group = row().group
|
||||
if (group.type !== "context") return emptyTools
|
||||
return group.refs
|
||||
.map((ref) => getMsgPart(ref.messageID, ref.partID))
|
||||
.filter((part): part is ToolPart => part?.type === "tool")
|
||||
})
|
||||
|
||||
return <ContextToolGroup parts={parts()} busy={workingTurn(row.userMessageID) && row.lastAssistantPart} />
|
||||
return (
|
||||
<ContextToolGroup
|
||||
parts={parts()}
|
||||
busy={
|
||||
workingTurn(row().userMessageID) && lastAssistantGroupKey().get(row().userMessageID) === row().group.key
|
||||
}
|
||||
onSizeChange={measureTimeline}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const message = createMemo(() => {
|
||||
const group = row.group
|
||||
const group = row().group
|
||||
if (group.type !== "part") return
|
||||
return messageByID().get(group.ref.messageID)
|
||||
})
|
||||
const part = createMemo(() => {
|
||||
const group = row.group
|
||||
const group = row().group
|
||||
if (group.type !== "part") return
|
||||
return getMsgPart(group.ref.messageID, group.ref.partID)
|
||||
})
|
||||
const defaultOpen = createMemo(() => {
|
||||
const item = part()
|
||||
if (!item) return
|
||||
return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded())
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={message()}>
|
||||
@@ -1012,14 +1050,13 @@ export function MessageTimeline(props: {
|
||||
<MessagePart
|
||||
part={part()}
|
||||
message={message()}
|
||||
showAssistantCopyPartID={assistantCopyPartID(row.userMessageID)}
|
||||
turnDurationMs={turnDurationMs(row.userMessageID)}
|
||||
defaultOpen={partDefaultOpen(
|
||||
part(),
|
||||
settings.general.shellToolPartsExpanded(),
|
||||
settings.general.editToolPartsExpanded(),
|
||||
)}
|
||||
showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
|
||||
turnDurationMs={turnDurationMs(row().userMessageID)}
|
||||
defaultOpen={defaultOpen()}
|
||||
toolOpen={toolOpen[part().id] ?? defaultOpen()}
|
||||
onToolOpenChange={(open) => setToolOpen(part().id, open)}
|
||||
deferToolContent={false}
|
||||
virtualizeDiff={false}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
@@ -1028,25 +1065,25 @@ export function MessageTimeline(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) {
|
||||
function TimelineRowFrame(input: { row: Accessor<FramedTimelineRow>; children: JSX.Element }) {
|
||||
const anchor = () => {
|
||||
const row = input.row
|
||||
const row = input.row()
|
||||
return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor)
|
||||
}
|
||||
const previousUserMessage = () => {
|
||||
const row = input.row
|
||||
const row = input.row()
|
||||
return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage
|
||||
}
|
||||
const previousAssistantPart = () => {
|
||||
const row = input.row
|
||||
const row = input.row()
|
||||
return row._tag === "AssistantPart" && row.previousAssistantPart
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={anchor() ? props.anchor(input.row.userMessageID) : undefined}
|
||||
data-message-id={input.row.userMessageID}
|
||||
data-timeline-row={input.row._tag}
|
||||
id={anchor() ? props.anchor(input.row().userMessageID) : undefined}
|
||||
data-message-id={input.row().userMessageID}
|
||||
data-timeline-row={input.row()._tag}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
@@ -1062,14 +1099,15 @@ export function MessageTimeline(props: {
|
||||
)
|
||||
}
|
||||
|
||||
const renderTimelineRow = (row: TimelineRow.TimelineRow) => {
|
||||
switch (row._tag) {
|
||||
const renderTimelineRow = (row: Accessor<TimelineRow.TimelineRow>) => {
|
||||
switch (row()._tag) {
|
||||
case "CommentStrip": {
|
||||
const commentStripRow = row as Accessor<TimelineRowByTag<"CommentStrip">>
|
||||
const comments = createMemo(() =>
|
||||
getMsgParts(row.userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
|
||||
getMsgParts(commentStripRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
|
||||
)
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={commentStripRow}>
|
||||
<div class="w-full px-4 md:px-5 pb-2">
|
||||
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
||||
<div class="flex w-max min-w-full justify-end gap-2">
|
||||
@@ -1102,17 +1140,22 @@ export function MessageTimeline(props: {
|
||||
)
|
||||
}
|
||||
case "UserMessage": {
|
||||
const userMessageRow = row as Accessor<TimelineRowByTag<"UserMessage">>
|
||||
const message = createMemo(() => {
|
||||
const m = messageByID().get(row.userMessageID)
|
||||
const m = messageByID().get(userMessageRow().userMessageID)
|
||||
if (m?.role === "user") return m
|
||||
})
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={userMessageRow}>
|
||||
<Show when={message()}>
|
||||
{(message) => (
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<div data-slot="session-turn-message-content" aria-live="off">
|
||||
<Message message={message()} parts={getMsgParts(row.userMessageID)} actions={props.actions} />
|
||||
<Message
|
||||
message={message()}
|
||||
parts={getMsgParts(userMessageRow().userMessageID)}
|
||||
actions={props.actions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1121,13 +1164,14 @@ export function MessageTimeline(props: {
|
||||
)
|
||||
}
|
||||
case "TurnDivider": {
|
||||
const turnDividerRow = row as Accessor<TimelineRowByTag<"TurnDivider">>
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={turnDividerRow}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<div data-slot="session-turn-compaction">
|
||||
<MessageDivider
|
||||
label={language.t(
|
||||
row.label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted",
|
||||
turnDividerRow().label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1136,22 +1180,27 @@ export function MessageTimeline(props: {
|
||||
)
|
||||
}
|
||||
case "AssistantPart": {
|
||||
const assistantPartRow = row as Accessor<TimelineRowByTag<"AssistantPart">>
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={assistantPartRow}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<div data-slot="session-turn-assistant-content" aria-hidden={workingTurn(row.userMessageID)}>
|
||||
{renderAssistantPartGroup(row)}
|
||||
<div
|
||||
data-slot="session-turn-assistant-content"
|
||||
aria-hidden={workingTurn(assistantPartRow().userMessageID)}
|
||||
>
|
||||
{renderAssistantPartGroup(assistantPartRow)}
|
||||
</div>
|
||||
</div>
|
||||
</TimelineRowFrame>
|
||||
)
|
||||
}
|
||||
case "Thinking": {
|
||||
const thinkingRow = row as Accessor<TimelineRowByTag<"Thinking">>
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={thinkingRow}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<TimelineThinkingRow
|
||||
reasoningHeading={row.reasoningHeading}
|
||||
reasoningHeading={thinkingRow().reasoningHeading}
|
||||
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||
/>
|
||||
</div>
|
||||
@@ -1159,29 +1208,32 @@ export function MessageTimeline(props: {
|
||||
)
|
||||
}
|
||||
case "Retry": {
|
||||
const retryRow = row as Accessor<TimelineRowByTag<"Retry">>
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={retryRow}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<SessionRetry status={sessionStatus()} show={activeMessageID() === row.userMessageID} />
|
||||
<SessionRetry status={sessionStatus()} show={activeMessageID() === retryRow().userMessageID} />
|
||||
</div>
|
||||
</TimelineRowFrame>
|
||||
)
|
||||
}
|
||||
case "DiffSummary": {
|
||||
const diffSummaryRow = row as Accessor<TimelineRowByTag<"DiffSummary">>
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={diffSummaryRow}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<TimelineDiffSummaryRow diffs={row.diffs} />
|
||||
<TimelineDiffSummaryRow diffs={diffSummaryRow().diffs} />
|
||||
</div>
|
||||
</TimelineRowFrame>
|
||||
)
|
||||
}
|
||||
case "Error": {
|
||||
const errorRow = row as Accessor<TimelineRowByTag<"Error">>
|
||||
return (
|
||||
<TimelineRowFrame row={row}>
|
||||
<TimelineRowFrame row={errorRow}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<Card variant="error" class="error-card">
|
||||
{row.text}
|
||||
{errorRow().text}
|
||||
</Card>
|
||||
</div>
|
||||
</TimelineRowFrame>
|
||||
@@ -1192,12 +1244,8 @@ export function MessageTimeline(props: {
|
||||
}
|
||||
}
|
||||
|
||||
function TimelineRowView(props: { rowKey: string }) {
|
||||
return (
|
||||
<Show when={timelineRowByKey().get(props.rowKey)} keyed>
|
||||
{(item) => renderTimelineRow(item)}
|
||||
</Show>
|
||||
)
|
||||
function TimelineRowView(props: { row: TimelineRow.TimelineRow }) {
|
||||
return renderTimelineRow(() => props.row)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1529,7 +1577,7 @@ export function MessageTimeline(props: {
|
||||
<Show when={scrollRoot()}>
|
||||
{(root) => (
|
||||
<Virtualizer
|
||||
data={timelineRowKeys()}
|
||||
data={timelineRows()}
|
||||
cache={virtualCache()}
|
||||
itemSize={virtualCache() ? undefined : timelineFallbackItemSize}
|
||||
scrollRef={root()}
|
||||
@@ -1549,7 +1597,7 @@ export function MessageTimeline(props: {
|
||||
scheduleContentRoot(root())
|
||||
}}
|
||||
>
|
||||
{(key) => <TimelineRowView rowKey={key} />}
|
||||
{(row) => <TimelineRowView row={row} />}
|
||||
</Virtualizer>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { onCleanup } from "solid-js"
|
||||
|
||||
export function createRefCountMap<T>(create: (key: string) => T) {
|
||||
const items = new Map<string, T>()
|
||||
const refCounts = new Map<string, number>()
|
||||
|
||||
return (key: string) => {
|
||||
onCleanup(() => {
|
||||
refCounts.set(key, (refCounts.get(key) ?? 0) - 1)
|
||||
if (refCounts.get(key) === 0) {
|
||||
items.delete(key)
|
||||
refCounts.delete(key)
|
||||
}
|
||||
})
|
||||
|
||||
const cached = items.get(key)
|
||||
if (cached) {
|
||||
refCounts.set(key, (refCounts.get(key) ?? 0) + 1)
|
||||
return cached
|
||||
}
|
||||
const item = create(key)
|
||||
items.set(key, item)
|
||||
refCounts.set(key, 1)
|
||||
return item
|
||||
}
|
||||
}
|
||||
@@ -509,7 +509,11 @@ export function Prompt(props: PromptProps) {
|
||||
const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
|
||||
|
||||
const value = text
|
||||
const content = await Editor.open({ value, renderer })
|
||||
const content = await Editor.open({
|
||||
value,
|
||||
renderer,
|
||||
cwd: project.instance.path().worktree || project.instance.directory() || process.cwd(),
|
||||
})
|
||||
if (!content) return
|
||||
|
||||
input.setText(content)
|
||||
|
||||
@@ -6,13 +6,14 @@ export type ThinkingMode = "show" | "hide"
|
||||
const MODES: readonly ThinkingMode[] = ["show", "hide"] as const
|
||||
|
||||
// OpenAI's Responses API surfaces reasoning summaries that start with a bolded
|
||||
// title line: "**Inspecting PR workflow**\n\n<body>". GitHub Copilot routes
|
||||
// through the same shape, and the opencode provider relays it too. Pull the
|
||||
// title out for a nicer label; return null for providers that don't follow
|
||||
// this convention so the caller can fall back to a generic "Thinking" string.
|
||||
export function reasoningTitle(text: string): string | null {
|
||||
const match = text.trimStart().match(/^\*\*([^*\n]+)\*\*/)
|
||||
return match ? match[1].trim() : null
|
||||
// title block: "**Inspecting PR workflow**\n\n<body>". Treat that first block,
|
||||
// or a complete title still awaiting its body while streaming, as disclosure
|
||||
// metadata so the TUI can style its header independently from the markdown body.
|
||||
export function reasoningSummary(text: string) {
|
||||
const content = text.trim()
|
||||
const match = content.match(/^\*\*([^*\n]+)\*\*(?:\r?\n\r?\n|$)/)
|
||||
if (!match) return { title: null, body: content }
|
||||
return { title: match[1].trim(), body: content.slice(match[0].length).trimEnd() }
|
||||
}
|
||||
|
||||
export function isThinkingMode(value: unknown): value is ThinkingMode {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { SplitBorder } from "@tui/component/border"
|
||||
import { Spinner } from "@tui/component/spinner"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { reasoningTitle, useThinkingMode } from "@tui/context/thinking"
|
||||
import { reasoningSummary, useThinkingMode } from "@tui/context/thinking"
|
||||
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import { TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core"
|
||||
import { RGBA, TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core"
|
||||
import { useBindings } from "../../keymap"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
@@ -398,7 +398,7 @@ function AssistantReasoning(props: {
|
||||
// v2 reasoning parts have no per-part `time.end` (see SessionMessageAssistantReasoning
|
||||
// in the v2 SDK); we settle on parent-message completion instead.
|
||||
const isDone = createMemo(() => props.completedAt() !== undefined)
|
||||
const title = createMemo(() => reasoningTitle(content()))
|
||||
const summary = createMemo(() => reasoningSummary(content()))
|
||||
|
||||
const toggle = () => {
|
||||
if (!inMinimal()) return
|
||||
@@ -407,41 +407,50 @@ function AssistantReasoning(props: {
|
||||
|
||||
return (
|
||||
<Show when={content()}>
|
||||
<Switch>
|
||||
<Match when={!inMinimal() || expanded()}>
|
||||
<box paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0} onMouseUp={toggle}>
|
||||
<box paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
|
||||
<box onMouseUp={toggle}>
|
||||
<ReasoningHeader
|
||||
toggleable={inMinimal()}
|
||||
open={!inMinimal() || expanded()}
|
||||
done={isDone()}
|
||||
title={summary().title}
|
||||
/>
|
||||
</box>
|
||||
<Show when={(!inMinimal() || expanded()) && summary().body}>
|
||||
<box paddingLeft={inMinimal() ? 2 : 0} marginTop={1}>
|
||||
<code
|
||||
filetype="markdown"
|
||||
drawUnstyledText={false}
|
||||
streaming={true}
|
||||
syntaxStyle={props.subtleSyntax}
|
||||
content={(inMinimal() ? "- " : "") + (isDone() ? "_Thought:_ " : "_Thinking:_ ") + content()}
|
||||
content={summary().body}
|
||||
conceal={true}
|
||||
fg={theme.textMuted}
|
||||
/>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={isDone()}>
|
||||
<box paddingLeft={3} marginTop={1} flexShrink={0} onMouseUp={toggle}>
|
||||
<CollapsedReasoningText title={title()} />
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<box paddingLeft={3} marginTop={1} flexShrink={0} onMouseUp={toggle}>
|
||||
<Spinner color={theme.textMuted}>{title() ? "Thinking: " + title() : "Thinking"}</Spinner>
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsedReasoningText(props: { title: string | null }) {
|
||||
function ReasoningHeader(props: { toggleable: boolean; open: boolean; done: boolean; title: string | null }) {
|
||||
const { theme } = useTheme()
|
||||
const fg = () =>
|
||||
props.open
|
||||
? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity)
|
||||
: theme.warning
|
||||
|
||||
return (
|
||||
<text fg={theme.warning} wrapMode="none">
|
||||
<span style={{ fg: theme.warning, italic: true }}>{props.title ? "+ Thought: " + props.title : "+ Thought"}</span>
|
||||
<text fg={fg()} wrapMode="none">
|
||||
<Show when={props.toggleable}>
|
||||
<span>{props.open ? "- " : "+ "}</span>
|
||||
</Show>
|
||||
<span>{props.done ? "Thought" : "Thinking"}</span>
|
||||
<Show when={props.title}>
|
||||
<span>: </span>
|
||||
<span>{props.title}</span>
|
||||
</Show>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ import * as Model from "../../util/model"
|
||||
import { formatTranscript } from "../../util/transcript"
|
||||
import { UI } from "@/cli/ui.ts"
|
||||
import { useTuiConfig } from "../../context/tui-config"
|
||||
import { nextThinkingMode, reasoningTitle, useThinkingMode, type ThinkingMode } from "../../context/thinking"
|
||||
import { nextThinkingMode, reasoningSummary, useThinkingMode, type ThinkingMode } from "../../context/thinking"
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
import { collapseToolOutput } from "../../util/collapse-tool-output"
|
||||
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
|
||||
@@ -965,7 +965,11 @@ export function Session() {
|
||||
|
||||
if (options.openWithoutSaving) {
|
||||
// Just open in editor without saving
|
||||
await Editor.open({ value: transcript, renderer })
|
||||
await Editor.open({
|
||||
value: transcript,
|
||||
renderer,
|
||||
cwd: project.instance.path().worktree || project.instance.directory() || process.cwd(),
|
||||
})
|
||||
} else {
|
||||
const exportDir = process.cwd()
|
||||
const filename = options.filename.trim()
|
||||
@@ -974,7 +978,11 @@ export function Session() {
|
||||
await Filesystem.write(filepath, transcript)
|
||||
|
||||
// Open with EDITOR if available
|
||||
const result = await Editor.open({ value: transcript, renderer })
|
||||
const result = await Editor.open({
|
||||
value: transcript,
|
||||
renderer,
|
||||
cwd: project.instance.path().worktree || project.instance.directory() || process.cwd(),
|
||||
})
|
||||
if (result !== undefined) {
|
||||
await Filesystem.write(filepath, result)
|
||||
}
|
||||
@@ -1515,12 +1523,8 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
|
||||
const end = props.part.time.end
|
||||
return end === undefined ? 0 : Math.max(0, end - props.part.time.start)
|
||||
})
|
||||
// OpenAI / Copilot / opencode-via-OpenAI emit `**Title**\n\n<body>` summary
|
||||
// blocks. Surface the title both while streaming and after settling so the
|
||||
// collapsed line carries real signal, not just a duration.
|
||||
const title = createMemo(() => reasoningTitle(content()))
|
||||
// Keep markdown emphasis for the existing thinking color/concealment, but render it without italics.
|
||||
const syntax = createMemo(() => generateSubtleSyntax(theme, { "markup.italic": { italic: false } }))
|
||||
const summary = createMemo(() => reasoningSummary(content()))
|
||||
const syntax = createMemo(() => generateSubtleSyntax(theme))
|
||||
|
||||
const toggle = () => {
|
||||
if (!inMinimal()) return
|
||||
@@ -1529,47 +1533,65 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
|
||||
|
||||
return (
|
||||
<Show when={content()}>
|
||||
<Switch>
|
||||
<Match when={!inMinimal() || expanded()}>
|
||||
{/* Full markdown block: `show` mode, or `hide` after the user opens it. */}
|
||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexDirection="column" onMouseUp={toggle}>
|
||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
|
||||
<box onMouseUp={toggle}>
|
||||
<ReasoningHeader
|
||||
toggleable={inMinimal()}
|
||||
open={!inMinimal() || expanded()}
|
||||
done={isDone()}
|
||||
title={summary().title}
|
||||
duration={isDone() ? Locale.duration(duration()) : undefined}
|
||||
/>
|
||||
</box>
|
||||
<Show when={(!inMinimal() || expanded()) && summary().body}>
|
||||
<box paddingLeft={inMinimal() ? 2 : 0} marginTop={1}>
|
||||
<code
|
||||
filetype="markdown"
|
||||
drawUnstyledText={false}
|
||||
streaming={true}
|
||||
syntaxStyle={syntax()}
|
||||
// `_Thinking:_`/`_Thought:_` still drives markdown emphasis color and conceals the underscores;
|
||||
// the syntax override above removes only the italic attribute from that emphasis token.
|
||||
content={(inMinimal() ? "- " : "") + (isDone() ? "_Thought:_ " : "_Thinking:_ ") + content()}
|
||||
content={summary().body}
|
||||
conceal={ctx.conceal()}
|
||||
fg={theme.textMuted}
|
||||
/>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={isDone()}>
|
||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0} onMouseUp={toggle}>
|
||||
<CollapsedReasoningText title={title()} duration={duration()} />
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0} onMouseUp={toggle}>
|
||||
<Spinner color={theme.textMuted}>{title() ? "Thinking: " + title() : "Thinking"}</Spinner>
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsedReasoningText(props: { title: string | null; duration: number }) {
|
||||
function ReasoningHeader(props: {
|
||||
toggleable: boolean
|
||||
open: boolean
|
||||
done: boolean
|
||||
title: string | null
|
||||
duration?: string
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const duration = () => Locale.duration(props.duration)
|
||||
const fg = () =>
|
||||
props.open
|
||||
? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity)
|
||||
: theme.warning
|
||||
|
||||
return (
|
||||
<text fg={theme.warning} wrapMode="none">
|
||||
<span style={{ fg: theme.warning }}>
|
||||
{props.title ? "+ Thought: " + props.title + " · " + duration() : "+ Thought: " + duration()}
|
||||
</span>
|
||||
<text fg={fg()} wrapMode="none">
|
||||
<Show when={props.toggleable}>
|
||||
<span>{props.open ? "- " : "+ "}</span>
|
||||
</Show>
|
||||
<span>{props.done ? "Thought" : "Thinking"}</span>
|
||||
<Show when={props.title || props.duration}>
|
||||
<span>: </span>
|
||||
</Show>
|
||||
<Show when={props.title}>
|
||||
<span>{props.title}</span>
|
||||
</Show>
|
||||
<Show when={props.duration}>
|
||||
<span>
|
||||
{props.title ? " · " : ""}
|
||||
{props.duration}
|
||||
</span>
|
||||
</Show>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CliRenderer } from "@opentui/core"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Process } from "@/util/process"
|
||||
|
||||
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
|
||||
export async function open(opts: { value: string; renderer: CliRenderer; cwd?: string }): Promise<string | undefined> {
|
||||
const editor = process.env["VISUAL"] || process.env["EDITOR"]
|
||||
if (!editor) return
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function open(opts: { value: string; renderer: CliRenderer }): Prom
|
||||
try {
|
||||
const parts = editor.split(" ")
|
||||
const proc = Process.spawn([...parts, filepath], {
|
||||
cwd: opts.cwd,
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
|
||||
@@ -48,7 +48,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
|
||||
experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"),
|
||||
outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
|
||||
bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
|
||||
experimentalNativeLlm: enabledByExperimental("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
|
||||
experimentalNativeLlm: bool("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
|
||||
client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")),
|
||||
}) {}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { and } from "drizzle-orm"
|
||||
import { and, eq, sql } from "drizzle-orm"
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
@@ -219,7 +218,10 @@ export const layer = Layer.effect(
|
||||
d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
|
||||
}
|
||||
|
||||
d.update(SessionTable).set({ project_id: newID }).where(eq(SessionTable.project_id, oldID)).run()
|
||||
d.update(SessionTable)
|
||||
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
|
||||
.where(eq(SessionTable.project_id, oldID))
|
||||
.run()
|
||||
d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
|
||||
|
||||
if (oldProject) d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { reasoningSummary } from "../../../src/cli/cmd/tui/context/thinking"
|
||||
|
||||
describe("reasoningSummary", () => {
|
||||
test("extracts a leading summary title and leaves markdown body", () => {
|
||||
expect(reasoningSummary("**Continuing Quality Review**\n\nDetails.\n\n**Next section**\n\nMore.")).toEqual({
|
||||
title: "Continuing Quality Review",
|
||||
body: "Details.\n\n**Next section**\n\nMore.",
|
||||
})
|
||||
})
|
||||
|
||||
test("extracts a completed title before its streamed body arrives", () => {
|
||||
expect(reasoningSummary("**Continuing Quality Review**")).toEqual({
|
||||
title: "Continuing Quality Review",
|
||||
body: "",
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves markdown-significant indentation in the extracted body", () => {
|
||||
expect(reasoningSummary("**Continuing Quality Review**\n\n const value = true\n")).toEqual({
|
||||
title: "Continuing Quality Review",
|
||||
body: " const value = true",
|
||||
})
|
||||
})
|
||||
|
||||
test("does not consume ordinary leading bold content", () => {
|
||||
expect(reasoningSummary("**Important:** keep this in the body.")).toEqual({
|
||||
title: null,
|
||||
body: "**Important:** keep this in the body.",
|
||||
})
|
||||
})
|
||||
|
||||
test("leaves content without a leading title in its body", () => {
|
||||
expect(reasoningSummary("Details only.")).toEqual({ title: null, body: "Details only." })
|
||||
})
|
||||
})
|
||||
@@ -58,7 +58,7 @@ describe("RuntimeFlags", () => {
|
||||
expect(flags.experimentalEventSystem).toBe(true)
|
||||
expect(flags.experimentalWorkspaces).toBe(true)
|
||||
expect(flags.experimentalIconDiscovery).toBe(true)
|
||||
expect(flags.experimentalNativeLlm).toBe(true)
|
||||
expect(flags.experimentalNativeLlm).toBe(false)
|
||||
expect(flags.client).toBe("desktop")
|
||||
}),
|
||||
)
|
||||
@@ -77,13 +77,13 @@ describe("RuntimeFlags", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("enables native LLM via dedicated or umbrella flag", () =>
|
||||
it.effect("enables native LLM via dedicated flag only", () =>
|
||||
Effect.gen(function* () {
|
||||
const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_NATIVE_LLM: "true" })))
|
||||
const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" })))
|
||||
|
||||
expect(explicit.experimentalNativeLlm).toBe(true)
|
||||
expect(umbrella.experimentalNativeLlm).toBe(true)
|
||||
expect(umbrella.experimentalNativeLlm).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const [store, setStore] = createStore({
|
||||
config: { permission: {} },
|
||||
})
|
||||
|
||||
export function useGlobalSync() {
|
||||
export function useServerSync() {
|
||||
return {
|
||||
data: {
|
||||
provider,
|
||||
|
||||
@@ -36,7 +36,6 @@ describe("apply patch file", () => {
|
||||
])[0]
|
||||
|
||||
expect(file).toBeDefined()
|
||||
expect(file?.view.patch).toContain("@@ -1,1 +1,1 @@")
|
||||
expect(text(file!.view, "deletions")).toBe("one\n")
|
||||
expect(text(file!.view, "additions")).toBe("two\n")
|
||||
})
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface BasicToolProps {
|
||||
status?: string
|
||||
hideDetails?: boolean
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
forceOpen?: boolean
|
||||
defer?: boolean
|
||||
locked?: boolean
|
||||
@@ -83,7 +85,7 @@ export function BasicTool(props: BasicToolProps) {
|
||||
open: props.defaultOpen ?? false,
|
||||
ready: !props.defer && (props.defaultOpen ?? false),
|
||||
})
|
||||
const open = () => state.open
|
||||
const open = () => props.open ?? state.open
|
||||
const ready = () => state.ready
|
||||
const pending = () => props.status === "pending" || props.status === "running"
|
||||
const hasChildren = () => (props.defer ? "children" in props : props.children)
|
||||
@@ -110,8 +112,15 @@ export function BasicTool(props: BasicToolProps) {
|
||||
if (props.defer && open()) scheduleReady(true)
|
||||
})
|
||||
|
||||
const setOpen = (value: boolean) => {
|
||||
if (props.open === undefined) setState("open", value)
|
||||
props.onOpenChange?.(value)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (props.forceOpen) setState("open", true)
|
||||
if (!props.forceOpen) return
|
||||
if (open()) return
|
||||
setOpen(true)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
@@ -166,7 +175,7 @@ export function BasicTool(props: BasicToolProps) {
|
||||
const handleOpenChange = (value: boolean) => {
|
||||
if (pending()) return
|
||||
if (props.locked && !value) return
|
||||
setState("open", value)
|
||||
setOpen(value)
|
||||
}
|
||||
|
||||
const trigger = () => (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { sampledChecksum } from "@opencode-ai/core/util/encode"
|
||||
import {
|
||||
areFilesEqual,
|
||||
areOptionsEqual,
|
||||
DEFAULT_VIRTUAL_FILE_METRICS,
|
||||
type DiffLineAnnotation,
|
||||
type FileContents,
|
||||
@@ -88,6 +90,7 @@ type DiffBaseProps<T> = FileDiffOptions<T> &
|
||||
mode: "diff"
|
||||
annotations?: DiffLineAnnotation<T>[]
|
||||
preloadedDiff?: DiffPreload<T>
|
||||
virtualize?: boolean
|
||||
}
|
||||
|
||||
type DiffPairProps<T> = DiffBaseProps<T> & {
|
||||
@@ -123,7 +126,7 @@ const sharedKeys = [
|
||||
] as const
|
||||
|
||||
const textKeys = ["file", ...sharedKeys] as const
|
||||
const diffKeys = ["fileDiff", "before", "after", ...sharedKeys] as const
|
||||
const diffKeys = ["fileDiff", "before", "after", "virtualize", ...sharedKeys] as const
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared viewer hook
|
||||
@@ -482,17 +485,24 @@ function notifyRendered(opts: {
|
||||
function renderViewer<I extends RenderTarget>(opts: {
|
||||
viewer: Viewer
|
||||
current: I | undefined
|
||||
reset?: boolean
|
||||
create: () => I
|
||||
update?: (value: I) => void
|
||||
assign: (value: I) => void
|
||||
draw: (value: I) => void
|
||||
onReady: () => void
|
||||
}) {
|
||||
clearReadyWatcher(opts.viewer.ready)
|
||||
opts.current?.cleanUp()
|
||||
const next = opts.create()
|
||||
opts.assign(next)
|
||||
const reset = opts.reset === true && opts.current !== undefined
|
||||
if (reset) opts.current?.cleanUp()
|
||||
const next = reset || !opts.current ? opts.create() : opts.current
|
||||
if (reset || !opts.current) {
|
||||
opts.viewer.container.innerHTML = ""
|
||||
opts.assign(next)
|
||||
} else {
|
||||
opts.update?.(next)
|
||||
}
|
||||
|
||||
opts.viewer.container.innerHTML = ""
|
||||
opts.draw(next)
|
||||
|
||||
applyViewerScheme(opts.viewer.getHost())
|
||||
@@ -566,7 +576,7 @@ function createLocalVirtualStrategy(host: () => HTMLDivElement | undefined, enab
|
||||
}
|
||||
}
|
||||
|
||||
function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined): VirtualStrategy {
|
||||
function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined, enabled: () => boolean): VirtualStrategy {
|
||||
let shared: NonNullable<ReturnType<typeof acquireVirtualizer>> | undefined
|
||||
|
||||
const release = () => {
|
||||
@@ -576,6 +586,10 @@ function createSharedVirtualStrategy(host: () => HTMLDivElement | undefined): Vi
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
if (!enabled()) {
|
||||
release()
|
||||
return
|
||||
}
|
||||
if (shared) return shared.virtualizer
|
||||
|
||||
const container = host()
|
||||
@@ -858,15 +872,14 @@ function TextViewer<T>(props: TextFileProps<T>) {
|
||||
createEffect(() => {
|
||||
const opts = options()
|
||||
const workerPool = getWorkerPool("unified")
|
||||
const isVirtual = virtual()
|
||||
|
||||
const virtualizer = virtuals.get()
|
||||
|
||||
renderViewer({
|
||||
viewer,
|
||||
current: instance,
|
||||
reset: instance !== undefined,
|
||||
create: () =>
|
||||
isVirtual && virtualizer
|
||||
virtualizer
|
||||
? new VirtualizedFile<T>(opts, virtualizer, codeMetrics, workerPool)
|
||||
: new PierreFile<T>(opts, workerPool),
|
||||
assign: (value) => {
|
||||
@@ -907,6 +920,12 @@ function TextViewer<T>(props: TextFileProps<T>) {
|
||||
|
||||
function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
let instance: FileDiff<T> | undefined
|
||||
let instanceVirtualizer: Virtualizer | undefined
|
||||
let instanceWorkerPool: ReturnType<typeof getWorkerPool>
|
||||
let instanceVirtualHunkSeparators: FileDiffOptions<T>["hunkSeparators"] | undefined
|
||||
let instanceFileDiff: FileDiffMetadata | undefined
|
||||
let instanceBefore: FileContents | undefined
|
||||
let instanceAfter: FileContents | undefined
|
||||
let dragSide: DiffSelectionSide | undefined
|
||||
let dragEndSide: DiffSelectionSide | undefined
|
||||
let viewer!: Viewer
|
||||
@@ -991,7 +1010,10 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
adapter,
|
||||
)
|
||||
|
||||
const virtuals = createSharedVirtualStrategy(() => viewer.container)
|
||||
const virtuals = createSharedVirtualStrategy(
|
||||
() => viewer.container,
|
||||
() => local.virtualize !== false,
|
||||
)
|
||||
|
||||
const large = createMemo(() => {
|
||||
if (local.fileDiff) {
|
||||
@@ -1067,31 +1089,66 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
return sampledChecksum(contents)
|
||||
}
|
||||
|
||||
const before = local.before
|
||||
? { ...local.before, contents: beforeContents, cacheKey: cacheKey(beforeContents) }
|
||||
: undefined
|
||||
const after = local.after
|
||||
? { ...local.after, contents: afterContents, cacheKey: cacheKey(afterContents) }
|
||||
: undefined
|
||||
const targetChanged =
|
||||
local.fileDiff !== undefined
|
||||
? instanceFileDiff !== local.fileDiff
|
||||
: instanceFileDiff !== undefined ||
|
||||
before === undefined ||
|
||||
after === undefined ||
|
||||
instanceBefore === undefined ||
|
||||
instanceAfter === undefined ||
|
||||
!areFilesEqual(instanceBefore, before) ||
|
||||
!areFilesEqual(instanceAfter, after)
|
||||
// Pierre beta virtualized instances retain their first diff target and resolve separator metrics at construction.
|
||||
// Plain timeline diffs can retain the instance as content streams; virtualized viewers reset only when that is unsafe.
|
||||
const reset =
|
||||
instance !== undefined &&
|
||||
(instanceVirtualizer !== virtualizer ||
|
||||
instanceWorkerPool !== workerPool ||
|
||||
(virtualizer !== undefined && (instanceVirtualHunkSeparators !== opts.hunkSeparators || targetChanged)))
|
||||
const forceRender = !reset && instance !== undefined && !areOptionsEqual(instance.options, opts)
|
||||
|
||||
renderViewer({
|
||||
viewer,
|
||||
current: instance,
|
||||
reset,
|
||||
create: () =>
|
||||
virtualizer
|
||||
? new VirtualizedFileDiff<T>(opts, virtualizer, virtualMetrics, workerPool)
|
||||
: new FileDiff<T>(opts, workerPool),
|
||||
update: (value) => value.setOptions(opts),
|
||||
assign: (value) => {
|
||||
instance = value
|
||||
instanceVirtualizer = virtualizer
|
||||
instanceWorkerPool = workerPool
|
||||
instanceVirtualHunkSeparators = virtualizer ? opts.hunkSeparators : undefined
|
||||
instanceFileDiff = local.fileDiff
|
||||
instanceBefore = before
|
||||
instanceAfter = after
|
||||
},
|
||||
draw: (value) => {
|
||||
if (local.fileDiff) {
|
||||
value.render({
|
||||
fileDiff: local.fileDiff,
|
||||
forceRender,
|
||||
lineAnnotations: [],
|
||||
containerWrapper: viewer.container,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!local.before || !local.after) return
|
||||
if (!before || !after) return
|
||||
|
||||
value.render({
|
||||
oldFile: { ...local.before, contents: beforeContents, cacheKey: cacheKey(beforeContents) },
|
||||
newFile: { ...local.after, contents: afterContents, cacheKey: cacheKey(afterContents) },
|
||||
oldFile: before,
|
||||
newFile: after,
|
||||
forceRender,
|
||||
lineAnnotations: [],
|
||||
containerWrapper: viewer.container,
|
||||
})
|
||||
@@ -1111,6 +1168,12 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
|
||||
onCleanup(() => {
|
||||
instance?.cleanUp()
|
||||
instance = undefined
|
||||
instanceVirtualizer = undefined
|
||||
instanceWorkerPool = undefined
|
||||
instanceVirtualHunkSeparators = undefined
|
||||
instanceFileDiff = undefined
|
||||
instanceBefore = undefined
|
||||
instanceAfter = undefined
|
||||
virtuals.cleanup()
|
||||
dragSide = undefined
|
||||
dragEndSide = undefined
|
||||
|
||||
@@ -175,7 +175,10 @@ export interface MessagePartProps {
|
||||
message: MessageType
|
||||
hideDetails?: boolean
|
||||
defaultOpen?: boolean
|
||||
toolOpen?: boolean
|
||||
onToolOpenChange?: (open: boolean) => void
|
||||
deferToolContent?: boolean
|
||||
virtualizeDiff?: boolean
|
||||
showAssistantCopyPartID?: string | null
|
||||
turnDurationMs?: number
|
||||
}
|
||||
@@ -290,7 +293,7 @@ function getDirectory(path: string | undefined) {
|
||||
}
|
||||
|
||||
import type { IconProps } from "./icon"
|
||||
import { normalize } from "./session-diff"
|
||||
import { normalize, resolveFileDiff } from "./session-diff"
|
||||
|
||||
export type ToolInfo = {
|
||||
icon: IconProps["name"]
|
||||
@@ -930,7 +933,7 @@ export function AssistantMessageDisplay(props: {
|
||||
)
|
||||
}
|
||||
|
||||
export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||
export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onSizeChange?: () => void }) {
|
||||
const i18n = useI18n()
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const pending = createMemo(
|
||||
@@ -938,11 +941,15 @@ export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||
)
|
||||
const summary = createMemo(() => contextToolSummary(props.parts))
|
||||
const handleOpenChange = (value: boolean) => {
|
||||
setOpen(value)
|
||||
props.onSizeChange?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={open()}
|
||||
onOpenChange={setOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
variant="ghost"
|
||||
class="tool-collapsible"
|
||||
data-timeline-part-ids={props.parts.map((part) => part.id).join(",")}
|
||||
@@ -1261,7 +1268,10 @@ export function Part(props: MessagePartProps) {
|
||||
message={props.message}
|
||||
hideDetails={props.hideDetails}
|
||||
defaultOpen={props.defaultOpen}
|
||||
toolOpen={props.toolOpen}
|
||||
onToolOpenChange={props.onToolOpenChange}
|
||||
deferToolContent={props.deferToolContent}
|
||||
virtualizeDiff={props.virtualizeDiff}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
/>
|
||||
@@ -1278,7 +1288,10 @@ export interface ToolProps {
|
||||
status?: string
|
||||
hideDetails?: boolean
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
deferContent?: boolean
|
||||
virtualizeDiff?: boolean
|
||||
forceOpen?: boolean
|
||||
locked?: boolean
|
||||
}
|
||||
@@ -1376,6 +1389,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
})
|
||||
|
||||
const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
|
||||
const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined)
|
||||
const handleToolOpenChange = (open: boolean) => props.onToolOpenChange?.(open)
|
||||
|
||||
return (
|
||||
<Show when={!hideQuestion()}>
|
||||
@@ -1399,6 +1414,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
error={error()}
|
||||
title={part().tool === "websearch" ? webSearchProviderLabel(partMetadata().provider) : undefined}
|
||||
defaultOpen={props.defaultOpen}
|
||||
open={controlledOpen()}
|
||||
onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined}
|
||||
subtitle={taskSubtitle()}
|
||||
href={taskHref()}
|
||||
/>
|
||||
@@ -1417,7 +1434,10 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
status={part().state.status}
|
||||
hideDetails={props.hideDetails}
|
||||
defaultOpen={props.defaultOpen}
|
||||
open={controlledOpen()}
|
||||
onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined}
|
||||
deferContent={props.deferToolContent}
|
||||
virtualizeDiff={props.virtualizeDiff}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -1921,15 +1941,29 @@ ToolRegistry.register({
|
||||
const path = createMemo(() => props.metadata?.filediff?.file || props.input.filePath || "")
|
||||
const filename = () => getFilename(props.input.filePath ?? "")
|
||||
const pending = () => props.status === "pending" || props.status === "running"
|
||||
const diffSource = createMemo(
|
||||
() => {
|
||||
const filediff = props.metadata?.filediff
|
||||
if (!filediff) return
|
||||
return {
|
||||
file: filediff.file || props.input.filePath || "",
|
||||
patch: typeof filediff.patch === "string" ? filediff.patch : undefined,
|
||||
before: typeof filediff.before === "string" ? filediff.before : undefined,
|
||||
after: typeof filediff.after === "string" ? filediff.after : undefined,
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
equals: (a, b) =>
|
||||
a?.file === b?.file && a?.patch === b?.patch && a?.before === b?.before && a?.after === b?.after,
|
||||
},
|
||||
)
|
||||
|
||||
const fileCompProps = createMemo(() => {
|
||||
try {
|
||||
if (props.metadata?.filediff) {
|
||||
const diff = normalize({
|
||||
...props.metadata?.filediff,
|
||||
status: "modified",
|
||||
})
|
||||
const fileDiff = diff.fileDiff
|
||||
const source = diffSource()
|
||||
if (source) {
|
||||
const fileDiff = resolveFileDiff(source)
|
||||
if (fileDiff) return { fileDiff, hunkSeparators: fileDiff.isPartial ? "simple" : "line-info-basic" }
|
||||
}
|
||||
} catch {}
|
||||
@@ -1987,7 +2021,7 @@ ToolRegistry.register({
|
||||
}
|
||||
>
|
||||
<div data-component="edit-content">
|
||||
<Dynamic component={fileComponent} mode="diff" {...fileCompProps()} />
|
||||
<Dynamic component={fileComponent} mode="diff" virtualize={props.virtualizeDiff} {...fileCompProps()} />
|
||||
</div>
|
||||
</ToolFileAccordion>
|
||||
</Show>
|
||||
@@ -2171,6 +2205,7 @@ ToolRegistry.register({
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
virtualize={props.virtualizeDiff}
|
||||
fileDiff={file.view.fileDiff}
|
||||
hunkSeparators={file.view.fileDiff.isPartial ? "simple" : "line-info-basic"}
|
||||
/>
|
||||
@@ -2243,7 +2278,12 @@ ToolRegistry.register({
|
||||
}
|
||||
>
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic component={fileComponent} mode="diff" fileDiff={single()!.view.fileDiff} />
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
virtualize={props.virtualizeDiff}
|
||||
fileDiff={single()!.view.fileDiff}
|
||||
/>
|
||||
</div>
|
||||
</ToolFileAccordion>
|
||||
</BasicTool>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { normalize, text } from "./session-diff"
|
||||
import { normalize, resolveFileDiff, text } from "./session-diff"
|
||||
|
||||
describe("session diff", () => {
|
||||
test("keeps unified patch content", () => {
|
||||
@@ -13,8 +13,8 @@ describe("session diff", () => {
|
||||
}
|
||||
const view = normalize(diff)
|
||||
|
||||
expect(view.patch).toBe(diff.patch)
|
||||
expect(view.fileDiff.name).toBe("a.ts")
|
||||
expect(view.fileDiff.isPartial).toBe(true)
|
||||
expect(text(view, "deletions")).toBe("one\ntwo\n")
|
||||
expect(text(view, "additions")).toBe("one\nthree\n")
|
||||
})
|
||||
@@ -34,7 +34,53 @@ describe("session diff", () => {
|
||||
expect(text(view, "additions")).toBe("one\nthree")
|
||||
})
|
||||
|
||||
test("converts legacy content into a patch", () => {
|
||||
test("keeps separated patch hunks partial without complete file contents", () => {
|
||||
const fileDiff = resolveFileDiff({
|
||||
file: "project.ts",
|
||||
patch:
|
||||
'Index: project.ts\n===================================================================\n--- project.ts\t\n+++ project.ts\t\n@@ -1,3 +1,2 @@\n import { and } from "drizzle-orm"\n-import { sql } from "drizzle-orm"\n import { ProjectTable } from "./project.sql"\n@@ -346,3 +345,3 @@\n import { Database } from "@/storage/db"\n-import { ProjectTable } from "./project.sql"\n+import { ProjectTable } from "../project/project.sql"\n import { SessionTable } from "../session/session.sql"\n',
|
||||
})
|
||||
|
||||
expect(fileDiff.isPartial).toBe(true)
|
||||
expect(fileDiff.hunks).toHaveLength(2)
|
||||
expect(fileDiff.hunks[1]?.collapsedBefore).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("renders headerless persisted patches", () => {
|
||||
const view = normalize({
|
||||
file: "a.ts",
|
||||
patch: "@@ -1 +1 @@\n-old\n+new\n",
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
status: "modified" as const,
|
||||
})
|
||||
|
||||
expect(view.fileDiff.name).toBe("a.ts")
|
||||
expect(view.fileDiff.isPartial).toBe(true)
|
||||
expect(text(view, "deletions")).toBe("old\n")
|
||||
expect(text(view, "additions")).toBe("new\n")
|
||||
})
|
||||
|
||||
test("does not share headerless patch metadata between files", () => {
|
||||
const patch = "@@ -1 +1 @@\n-old\n+new\n"
|
||||
|
||||
expect(resolveFileDiff({ file: "a.ts", patch }).name).toBe("a.ts")
|
||||
expect(resolveFileDiff({ file: "b.ts", patch }).name).toBe("b.ts")
|
||||
})
|
||||
|
||||
test("keeps capped header-only patches partial", () => {
|
||||
const fileDiff = resolveFileDiff({
|
||||
file: "a.ts",
|
||||
patch:
|
||||
"Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n",
|
||||
})
|
||||
|
||||
expect(fileDiff.name).toBe("a.ts")
|
||||
expect(fileDiff.isPartial).toBe(true)
|
||||
expect(fileDiff.hunks).toEqual([])
|
||||
})
|
||||
|
||||
test("keeps full legacy content as a complete diff", () => {
|
||||
const diff = {
|
||||
file: "a.ts",
|
||||
before: "one\n",
|
||||
@@ -45,7 +91,7 @@ describe("session diff", () => {
|
||||
}
|
||||
const view = normalize(diff)
|
||||
|
||||
expect(view.patch).toContain("@@ -1,1 +1,1 @@")
|
||||
expect(view.fileDiff.isPartial).toBe(false)
|
||||
expect(text(view, "deletions")).toBe("one\n")
|
||||
expect(text(view, "additions")).toBe("two\n")
|
||||
})
|
||||
@@ -61,7 +107,6 @@ describe("session diff", () => {
|
||||
}
|
||||
const view = normalize(diff)
|
||||
|
||||
expect(view.patch).toBe(diff.patch)
|
||||
expect(text(view, "deletions")).toBe("")
|
||||
expect(text(view, "additions")).toBe("")
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseDiffFromFile, parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs"
|
||||
import { formatPatch, parsePatch, structuredPatch } from "diff"
|
||||
import { parsePatch } from "diff"
|
||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||
|
||||
type LegacyDiff = {
|
||||
@@ -14,107 +14,35 @@ type LegacyDiff = {
|
||||
|
||||
type SnapshotDiff = SnapshotFileDiff & { file: string }
|
||||
type ReviewDiff = SnapshotDiff | VcsFileDiff | LegacyDiff
|
||||
export type DiffSource = Pick<LegacyDiff, "file" | "patch" | "before" | "after">
|
||||
|
||||
export type ViewDiff = {
|
||||
file: string
|
||||
patch: string
|
||||
additions: number
|
||||
deletions: number
|
||||
status?: "added" | "deleted" | "modified"
|
||||
fileDiff: FileDiffMetadata
|
||||
}
|
||||
|
||||
const cache = new Map<string, FileDiffMetadata>()
|
||||
const diffCacheLimit = 16
|
||||
const patchFileDiffCache = new Map<string, FileDiffMetadata>()
|
||||
|
||||
function patch(diff: ReviewDiff) {
|
||||
if (typeof diff.patch === "string") {
|
||||
try {
|
||||
const [patch] = parsePatch(diff.patch)
|
||||
const beforeLines: Array<{ text: string; newline: boolean }> = []
|
||||
const afterLines: Array<{ text: string; newline: boolean }> = []
|
||||
let previous: "-" | "+" | " " | undefined
|
||||
|
||||
const patchIsPartial = patch.hunks.every((h) => h.oldStart > 1)
|
||||
|
||||
for (const hunk of patch.hunks) {
|
||||
for (const line of hunk.lines) {
|
||||
if (line.startsWith("\\")) {
|
||||
if (previous === "-" || previous === " ") {
|
||||
const before = beforeLines.at(-1)
|
||||
if (before) before.newline = false
|
||||
}
|
||||
if (previous === "+" || previous === " ") {
|
||||
const after = afterLines.at(-1)
|
||||
if (after) after.newline = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith("-")) {
|
||||
beforeLines.push({ text: line.slice(1), newline: true })
|
||||
previous = "-"
|
||||
} else if (line.startsWith("+")) {
|
||||
afterLines.push({ text: line.slice(1), newline: true })
|
||||
previous = "+"
|
||||
} else {
|
||||
// context line (starts with ' ')
|
||||
beforeLines.push({ text: line.slice(1), newline: true })
|
||||
afterLines.push({ text: line.slice(1), newline: true })
|
||||
previous = " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
before: beforeLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""),
|
||||
after: afterLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""),
|
||||
patch: diff.patch,
|
||||
patchIsPartial,
|
||||
}
|
||||
} catch {
|
||||
return { before: "", after: "", patch: diff.patch, patchIsPartial: false }
|
||||
}
|
||||
}
|
||||
return {
|
||||
before: "before" in diff && typeof diff.before === "string" ? diff.before : "",
|
||||
after: "after" in diff && typeof diff.after === "string" ? diff.after : "",
|
||||
patch: formatPatch(
|
||||
structuredPatch(
|
||||
diff.file,
|
||||
diff.file,
|
||||
"before" in diff && typeof diff.before === "string" ? diff.before : "",
|
||||
"after" in diff && typeof diff.after === "string" ? diff.after : "",
|
||||
"",
|
||||
"",
|
||||
{ context: Number.MAX_SAFE_INTEGER },
|
||||
),
|
||||
),
|
||||
patchIsPartial: false,
|
||||
}
|
||||
}
|
||||
|
||||
function file(file: string, patch: string, before: string, after: string, partial = false) {
|
||||
const hit = cache.get(patch)
|
||||
if (hit) return hit
|
||||
|
||||
let value: FileDiffMetadata | undefined
|
||||
if (partial) value = parsePatchFiles(patch)[0]?.files[0]
|
||||
if (value === undefined) value = parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after })
|
||||
|
||||
cache.set(patch, value)
|
||||
return value
|
||||
export function resolveFileDiff(diff: DiffSource) {
|
||||
if (typeof diff.patch === "string") return fileDiffFromPatch(diff.file, diff.patch)
|
||||
return fileDiffFromContent(
|
||||
diff.file,
|
||||
typeof diff.before === "string" ? diff.before : "",
|
||||
typeof diff.after === "string" ? diff.after : "",
|
||||
)
|
||||
}
|
||||
|
||||
export function normalize(diff: ReviewDiff): ViewDiff {
|
||||
const next = patch(diff)
|
||||
const fileDiff = file(diff.file, next.patch, next.before, next.after, next.patchIsPartial)
|
||||
return {
|
||||
file: diff.file,
|
||||
patch: next.patch,
|
||||
additions: diff.additions,
|
||||
deletions: diff.deletions,
|
||||
status: diff.status,
|
||||
fileDiff,
|
||||
fileDiff: resolveFileDiff(diff),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,3 +50,40 @@ export function text(diff: ViewDiff, side: "deletions" | "additions") {
|
||||
if (side === "deletions") return diff.fileDiff.deletionLines.join("")
|
||||
return diff.fileDiff.additionLines.join("")
|
||||
}
|
||||
|
||||
function fileDiffFromPatch(file: string, patch: string) {
|
||||
const key = `${file}\0${patch}`
|
||||
const hit = patchFileDiffCache.get(key)
|
||||
if (hit) {
|
||||
patchFileDiffCache.delete(key)
|
||||
patchFileDiffCache.set(key, hit)
|
||||
return hit
|
||||
}
|
||||
|
||||
const input = patchInput(file, patch)
|
||||
const value = (input ? parsePatchFiles(input)[0]?.files[0] : undefined) ?? emptyFileDiff(file)
|
||||
patchFileDiffCache.set(key, value)
|
||||
while (patchFileDiffCache.size > diffCacheLimit) patchFileDiffCache.delete(patchFileDiffCache.keys().next().value!)
|
||||
return value
|
||||
}
|
||||
|
||||
function patchInput(file: string, patch: string) {
|
||||
try {
|
||||
const parsed = parsePatch(patch)[0]
|
||||
if (!parsed) return
|
||||
if (parsed.index || parsed.oldFileName || parsed.newFileName) return patch
|
||||
if (!parsed.hunks.length) return
|
||||
return `Index: ${file}\n===================================================================\n--- ${file}\t\n+++ ${file}\t\n${patch}`
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function fileDiffFromContent(file: string, before: string, after: string) {
|
||||
if (!before && !after) return emptyFileDiff(file)
|
||||
return parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after })
|
||||
}
|
||||
|
||||
function emptyFileDiff(file: string) {
|
||||
return parseDiffFromFile({ name: file, contents: "" }, { name: file, contents: "" })
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { Todo } from "@opencode-ai/sdk/v2"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServerSync } from "@/context/global-sync"
|
||||
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
|
||||
|
||||
export default {
|
||||
@@ -129,7 +129,7 @@ const css = `
|
||||
|
||||
export const Playground = {
|
||||
render: () => {
|
||||
const global = useGlobalSync()
|
||||
const global = useServerSync()
|
||||
const [cfg, setCfg] = createStore({
|
||||
open: true,
|
||||
step: 1,
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "c
|
||||
error: string
|
||||
title?: string
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
subtitle?: string
|
||||
href?: string
|
||||
}
|
||||
@@ -22,9 +24,22 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
|
||||
open: props.defaultOpen ?? false,
|
||||
copied: false,
|
||||
})
|
||||
const open = () => state.open
|
||||
const open = () => props.open ?? state.open
|
||||
const copied = () => state.copied
|
||||
const [split, rest] = splitProps(props, ["tool", "error", "title", "defaultOpen", "subtitle", "href"])
|
||||
const [split, rest] = splitProps(props, [
|
||||
"tool",
|
||||
"error",
|
||||
"title",
|
||||
"defaultOpen",
|
||||
"open",
|
||||
"onOpenChange",
|
||||
"subtitle",
|
||||
"href",
|
||||
])
|
||||
const setOpen = (value: boolean) => {
|
||||
if (props.open === undefined) setState("open", value)
|
||||
props.onOpenChange?.(value)
|
||||
}
|
||||
const name = createMemo(() => {
|
||||
if (split.title) return split.title
|
||||
const map: Record<string, string> = {
|
||||
@@ -77,12 +92,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
|
||||
|
||||
return (
|
||||
<Card {...rest} data-kind="tool-error-card" data-open={open() ? "true" : "false"} variant="error">
|
||||
<Collapsible
|
||||
class="tool-collapsible"
|
||||
data-open={open() ? "true" : "false"}
|
||||
open={open()}
|
||||
onOpenChange={(value) => setState("open", value)}
|
||||
>
|
||||
<Collapsible class="tool-collapsible" data-open={open() ? "true" : "false"} open={open()} onOpenChange={setOpen}>
|
||||
<Collapsible.Trigger>
|
||||
<div data-component="tool-trigger">
|
||||
<div data-slot="basic-tool-tool-trigger-content">
|
||||
|
||||
@@ -594,17 +594,23 @@ opencode upgrade v0.1.48
|
||||
|
||||
تقوم متغيرات البيئة التالية بتفعيل ميزات تجريبية قد تتغير أو تتم إزالتها.
|
||||
|
||||
| المتغير | النوع | الوصف |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | تفعيل جميع الميزات التجريبية |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | تفعيل وضع الخطة |
|
||||
| المتغير | النوع | الوصف |
|
||||
| ----------------------------------------------- | ------- | -------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | تفعيل الميزات التجريبية الخاضعة للعلم الشامل |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | تفعيل وضع الخطة |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | تفعيل مهام subagent في الخلفية |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | تفعيل نظام الأحداث التجريبي |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | تفعيل مسار طلبات LLM الأصلي |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | تفعيل تنفيذ بحث الويب بالتوازي |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | تفعيل Scout subagent |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | تفعيل دعم مساحات العمل |
|
||||
|
||||
@@ -592,17 +592,23 @@ OpenCode se može konfigurirati pomoću varijabli okruženja.
|
||||
|
||||
Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti.
|
||||
|
||||
| Varijabla | Tip | Opis |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod |
|
||||
| Varijabla | Tip | Opis |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući eksperimentalne funkcije pod zbirnom zastavicom |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Omogući pozadinske zadatke subagenata |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Omogući eksperimentalni sistem događaja |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Omogući nativnu putanju LLM zahtjeva |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Omogući paralelno izvršavanje web pretrage |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Omogući Scout subagenta |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Omogući podršku za radne prostore |
|
||||
|
||||
@@ -712,7 +712,7 @@ These environment variables enable experimental features that may change or be r
|
||||
|
||||
| Variable | Type | Description |
|
||||
| ----------------------------------------------- | ------- | --------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Enable all experimental features |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Enable the experimental umbrella flag |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Enable icon discovery |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disable copy on select in TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Default timeout for bash commands in ms |
|
||||
@@ -724,3 +724,9 @@ These environment variables enable experimental features that may change or be r
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable TY LSP for python files |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Enable plan mode |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Enable background subagent tasks |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Enable experimental event system |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Enable native LLM request path |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Enable parallel web search execution |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Enable Scout subagent |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Enable workspace support |
|
||||
|
||||
@@ -595,17 +595,23 @@ OpenCode kan konfigureres ved hjælp af miljøvariabler.
|
||||
|
||||
Disse miljøvariabler muliggør eksperimentelle funktioner, der kan ændres eller fjernes.
|
||||
|
||||
| Variabel | Skriv | Beskrivelse |
|
||||
| ----------------------------------------------- | ------- | ----------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Aktiver alle eksperimentelle funktioner |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Deaktiver kopi ved valg i TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | nummer | Standard timeout for bash-kommandoer i ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | nummer | Maks. output-tokens for LLM-svar |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Aktiver plantilstand |
|
||||
| Variabel | Skriv | Beskrivelse |
|
||||
| ----------------------------------------------- | ------- | ---------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Aktiver eksperimentelle funktioner under samleflaget |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Deaktiver kopi ved valg i TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | nummer | Standard timeout for bash-kommandoer i ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | nummer | Maks. output-tokens for LLM-svar |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Aktiver plantilstand |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Aktiver baggrundsopgaver for subagenter |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Aktiver eksperimentelt hændelsessystem |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Aktiver native LLM-anmodningssti |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Aktiver parallel udførelse af websøgning |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Aktiver Scout-subagent |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Aktiver workspace-understøttelse |
|
||||
|
||||
@@ -594,17 +594,23 @@ OpenCode kann mithilfe von Umgebungsvariablen konfiguriert werden.
|
||||
|
||||
Diese Umgebungsvariablen ermöglichen experimentelle Funktionen, die sich ändern oder entfernt werden können.
|
||||
|
||||
| Variable | Typ | Beschreibung |
|
||||
| ----------------------------------------------- | --------------- | ------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolescher Wert | Alle experimentellen Funktionen aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolescher Wert | Symbolerkennung aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolescher Wert | Kopieren bei Auswahl in TUI deaktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Zahl | Standard-Timeout für Bash-Befehle in ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Zahl | Maximale Ausgabetokens für LLM-Antworten |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolescher Wert | Dateiüberwachung für das gesamte Verzeichnis aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolescher Wert | Planmodus aktivieren |
|
||||
| Variable | Typ | Beschreibung |
|
||||
| ----------------------------------------------- | --------------- | --------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolescher Wert | Experimentelle Funktionen unter dem Sammelflag aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolescher Wert | Symbolerkennung aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolescher Wert | Kopieren bei Auswahl in TUI deaktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Zahl | Standard-Timeout für Bash-Befehle in ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Zahl | Maximale Ausgabetokens für LLM-Antworten |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolescher Wert | Dateiüberwachung für das gesamte Verzeichnis aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolescher Wert | Planmodus aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolescher Wert | Hintergrundaufgaben für Subagenten aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolescher Wert | Experimentelles Ereignissystem aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolescher Wert | Nativen LLM-Anfragepfad aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolescher Wert | Parallele Websuche aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolescher Wert | Scout-Subagent aktivieren |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolescher Wert | Workspace-Unterstützung aktivieren |
|
||||
|
||||
@@ -596,7 +596,7 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar
|
||||
|
||||
| Variable | Type | Description |
|
||||
| ----------------------------------------------- | -------- | ---------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | booleano | Habilitar todas las funciones experimentales |
|
||||
| `OPENCODE_EXPERIMENTAL` | booleano | Habilitar funciones experimentales de la bandera global |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booleano | Habilitar descubrimiento de íconos |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | booleano | Deshabilitar copia al seleccionar en TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | número | Tiempo de espera predeterminado para comandos bash en ms |
|
||||
@@ -608,3 +608,9 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | booleano | Habilitar funciones experimentales de Exa |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | booleano | Habilitar Habilitar TY LSP para archivos python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booleano | Habilitar modo de plan |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | booleano | Habilitar tareas de subagentes en segundo plano |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | booleano | Habilitar sistema de eventos experimental |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | booleano | Habilitar ruta nativa de solicitud LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | booleano | Habilitar ejecución paralela de búsqueda web |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | booleano | Habilitar subagente Scout |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | booleano | Habilitar soporte de espacios de trabajo |
|
||||
|
||||
@@ -597,7 +597,7 @@ Ces variables d'environnement activent des fonctionnalités expérimentales qui
|
||||
|
||||
| Variables | Type | Description |
|
||||
| ----------------------------------------------- | ------- | --------------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | booléen | Activer toutes les fonctionnalités expérimentales |
|
||||
| `OPENCODE_EXPERIMENTAL` | booléen | Activer les fonctionnalités expérimentales du drapeau global |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booléen | Activer la découverte d'icônes |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | booléen | Désactiver la copie lors de la sélection dans TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | numéro | Délai d'expiration par défaut pour les commandes bash en ms |
|
||||
@@ -609,3 +609,9 @@ Ces variables d'environnement activent des fonctionnalités expérimentales qui
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | booléen | Activer les fonctionnalités Exa expérimentales |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | booléen | Activer TY LSP pour les fichiers python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booléen | Activer le mode plan |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | booléen | Activer les tâches de sous-agents en arrière-plan |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | booléen | Activer le système d'événements expérimental |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | booléen | Activer le chemin de requête LLM natif |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | booléen | Activer l'exécution parallèle de la recherche web |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | booléen | Activer le sous-agent Scout |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | booléen | Activer la prise en charge des espaces de travail |
|
||||
|
||||
@@ -595,17 +595,23 @@ OpenCode può essere configurato tramite variabili d'ambiente.
|
||||
|
||||
Queste variabili d'ambiente abilitano funzionalità sperimentali che potrebbero cambiare o essere rimosse.
|
||||
|
||||
| Variabile | Tipo | Descrizione |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------ |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Abilita tutte le funzionalità sperimentali |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disabilita copy on select nella TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Timeout di default per comandi bash in ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Massimo token di output per risposte LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Abilita plan mode |
|
||||
| Variabile | Tipo | Descrizione |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Abilita funzionalità sperimentali sotto il flag globale |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disabilita copy on select nella TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Timeout di default per comandi bash in ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Massimo token di output per risposte LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Abilita plan mode |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Abilita task subagent in background |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Abilita sistema eventi sperimentale |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Abilita percorso nativo di richiesta LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Abilita esecuzione parallela della ricerca web |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Abilita subagent Scout |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Abilita supporto workspace |
|
||||
|
||||
@@ -596,7 +596,7 @@ OpenCode は環境変数を使用して構成できます。
|
||||
|
||||
| 変数 | タイプ | 説明 |
|
||||
| ----------------------------------------------- | -------- | ------------------------------------------------ |
|
||||
| `OPENCODE_EXPERIMENTAL` | ブール値 | すべての実験的機能を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL` | ブール値 | アンブレラフラグ配下の実験的機能を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | ブール値 | アイコン検出を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | ブール値 | TUI で選択時のコピーを無効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | 数値 | bash コマンドのデフォルトのタイムアウト (ミリ秒) |
|
||||
@@ -608,3 +608,9 @@ OpenCode は環境変数を使用して構成できます。
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | ブール値 | 実験的な Exa 機能を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | ブール値 | python ファイルの TY LSP を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | ブール値 | プランモードを有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | ブール値 | バックグラウンド subagent タスクを有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | ブール値 | 実験的なイベントシステムを有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | ブール値 | ネイティブ LLM リクエスト経路を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | ブール値 | 並列 Web 検索実行を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | ブール値 | Scout subagent を有効にする |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | ブール値 | ワークスペースサポートを有効にする |
|
||||
|
||||
@@ -594,17 +594,23 @@ OpenCode는 환경 변수로도 구성할 수 있습니다.
|
||||
|
||||
아래 환경 변수는 변경되거나 제거될 수 있는 실험 기능을 활성화합니다.
|
||||
|
||||
| 변수 | 타입 | 설명 |
|
||||
| ----------------------------------------------- | ------- | -------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | 모든 실험 기능 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan mode 활성화 |
|
||||
| 변수 | 타입 | 설명 |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | 엄브렐라 플래그로 제어되는 실험 기능 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan mode 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 백그라운드 subagent 작업 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 실험적 이벤트 시스템 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 네이티브 LLM 요청 경로 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 병렬 웹 검색 실행 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Scout subagent 활성화 |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | workspace 지원 활성화 |
|
||||
|
||||
@@ -595,17 +595,23 @@ OpenCode kan konfigureres ved hjelp av miljøvariabler.
|
||||
|
||||
Disse miljøvariablene muliggjør eksperimentelle funksjoner som kan endres eller fjernes.
|
||||
|
||||
| Variabel | Type | Beskrivelse |
|
||||
| ----------------------------------------------- | ------ | --------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver alle eksperimentelle funksjoner |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolsk | Deaktiver kopi ved valg i TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | tall | Standard tidsavbrudd for bash-kommandoer i ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | tall | Maks Output Tokens for LLM-svar |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolsk | Aktiver planmodus |
|
||||
| Variabel | Type | Beskrivelse |
|
||||
| ----------------------------------------------- | ------ | ----------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver eksperimentelle funksjoner under samleflagget |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolsk | Deaktiver kopi ved valg i TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | tall | Standard tidsavbrudd for bash-kommandoer i ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | tall | Maks Output Tokens for LLM-svar |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolsk | Aktiver planmodus |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolsk | Aktiver bakgrunnsoppgaver for subagenter |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolsk | Aktiver eksperimentelt hendelsessystem |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolsk | Aktiver innebygd LLM-forespørselsvei |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolsk | Aktiver parallell kjøring av websøk |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolsk | Aktiver Scout-subagent |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolsk | Aktiver arbeidsområde-støtte |
|
||||
|
||||
@@ -595,17 +595,23 @@ OpenCode można skonfigurować za pomocą zmiennych środowiskowych.
|
||||
|
||||
Te zmienne włączają funkcje eksperymentalne, które mogą ulec zmianie lub zostać usunięte.
|
||||
|
||||
| Zmienna | Typ | Opis |
|
||||
| ----------------------------------------------- | ------- | -------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Włącz wszystkie funkcje eksperymentalne |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Włącz wykrywanie ikon |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Wyłącz kopiowanie przy zaznaczaniu w TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Domyślny limit czasu dla narzędzia bash w ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksymalne tokeny wyjściowe dla LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Włącz obserwatora plików dla całego katalogu |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Włącz tryb planowania |
|
||||
| Zmienna | Typ | Opis |
|
||||
| ----------------------------------------------- | ------- | --------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Włącz funkcje eksperymentalne objęte flagą zbiorczą |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Włącz wykrywanie ikon |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Wyłącz kopiowanie przy zaznaczaniu w TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Domyślny limit czasu dla narzędzia bash w ms |
|
||||
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksymalne tokeny wyjściowe dla LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Włącz obserwatora plików dla całego katalogu |
|
||||
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików |
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Włącz tryb planowania |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Włącz zadania subagentów w tle |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Włącz eksperymentalny system zdarzeń |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Włącz natywną ścieżkę żądań LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Włącz równoległe wykonywanie wyszukiwania web |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Włącz subagenta Scout |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Włącz obsługę obszarów roboczych |
|
||||
|
||||
@@ -596,7 +596,7 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou
|
||||
|
||||
| Variável | Tipo | Descrição |
|
||||
| ----------------------------------------------- | ------- | --------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Habilitar todos os recursos experimentais |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Habilitar recursos experimentais sob o sinalizador global |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Habilitar descoberta de ícones |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Desabilitar cópia ao selecionar no TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Tempo limite padrão para comandos bash em ms |
|
||||
@@ -608,3 +608,9 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Habilitar recursos experimentais do Exa |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Habilitar TY LSP para arquivos python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Habilitar modo de plano |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Habilitar tarefas de subagentes em segundo plano |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Habilitar sistema de eventos experimental |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Habilitar caminho nativo de requisição LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Habilitar execução paralela de busca web |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Habilitar subagente Scout |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Habilitar suporte a espaços de trabalho |
|
||||
|
||||
@@ -597,7 +597,7 @@ opencode можно настроить с помощью переменных с
|
||||
|
||||
| Переменная | Тип | Описание |
|
||||
| ----------------------------------------------- | ------------------- | ------------------------------------------------------ |
|
||||
| `OPENCODE_EXPERIMENTAL` | логическое значение | Включить все экспериментальные функции |
|
||||
| `OPENCODE_EXPERIMENTAL` | логическое значение | Включить экспериментальные функции под общим флагом |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | логическое значение | Включить обнаружение значков |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | логическое значение | Отключить копирование при выборе в TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | число | Таймаут по умолчанию для команд bash в мс |
|
||||
@@ -609,3 +609,9 @@ opencode можно настроить с помощью переменных с
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | логическое значение | Включить экспериментальные функции Exa |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | логическое значение | Включить TY LSP для файлов python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | логическое значение | Включить режим плана |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | логическое значение | Включить фоновые задачи субагентов |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | логическое значение | Включить экспериментальную систему событий |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | логическое значение | Включить нативный путь запросов LLM |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | логическое значение | Включить параллельное выполнение веб-поиска |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | логическое значение | Включить субагент Scout |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | логическое значение | Включить поддержку рабочих областей |
|
||||
|
||||
@@ -598,7 +598,7 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว
|
||||
|
||||
| ตัวแปร | ชนิด | คำอธิบาย |
|
||||
| ----------------------------------------------- | ------- | ---------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองทั้งหมด |
|
||||
| `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองภายใต้แฟล็กรวม |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | Boolean | การค้นหาไอคอนทดลอง |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | Boolean | ปิดใช้งานการคัดลอกเมื่อเลือกใน TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Number | การหมดเวลาเริ่มต้นสำหรับคำสั่ง bash ในหน่วย ms |
|
||||
@@ -610,3 +610,9 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | Boolean | ฟีเจอร์ Exa ทดลอง |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | Boolean | เปิดใช้งาน TY LSP สำหรับไฟล์ python |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | Boolean | เปิดใช้งาน Plan mode |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Boolean | เปิดใช้งานงาน subagent เบื้องหลัง |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | Boolean | เปิดใช้งานระบบเหตุการณ์ทดลอง |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | Boolean | เปิดใช้งานเส้นทางคำขอ LLM แบบ native |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | Boolean | เปิดใช้งานการค้นหาเว็บแบบขนาน |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | Boolean | เปิดใช้งาน Scout subagent |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | Boolean | เปิดใช้งานการรองรับ workspace |
|
||||
|
||||
@@ -597,7 +597,7 @@ Bu ortam değişkenleri değişebilecek veya kaldırılabilecek deneysel özelli
|
||||
|
||||
| Değişken | Tip | Tanım |
|
||||
| ----------------------------------------------- | ------- | ------------------------------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Tüm deneysel özellikleri etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | Şemsiye bayrağa bağlı deneysel özellikleri etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Simge bulmayı etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI'da seçim yapıldığında kopyalamayı devre dışı bırak |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | MS cinsinden bash komutları için varsayılan zaman aşımı |
|
||||
@@ -609,3 +609,9 @@ Bu ortam değişkenleri değişebilecek veya kaldırılabilecek deneysel özelli
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Deneysel Exa özelliklerini etkinleştirin |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python dosyaları için TY LSP'yi etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan modunu etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Arka plan alt ajan görevlerini etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Deneysel olay sistemini etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Yerel LLM istek yolunu etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Paralel web araması yürütmesini etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Scout alt ajanını etkinleştir |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Çalışma alanı desteğini etkinleştir |
|
||||
|
||||
@@ -597,7 +597,7 @@ OpenCode 可以通过环境变量进行配置。
|
||||
|
||||
| 变量 | 类型 | 描述 |
|
||||
| ----------------------------------------------- | ------- | ------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | 启用所有实验性功能 |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | 启用受总开关控制的实验性功能 |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 启用图标发现 |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 禁用 TUI 中的选中即复制 |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 命令的默认超时时间(毫秒) |
|
||||
@@ -609,3 +609,9 @@ OpenCode 可以通过环境变量进行配置。
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 启用实验性 Exa 功能 |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 为 python 文件启用 TY LSP |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 启用计划模式 |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 启用后台子代理任务 |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 启用实验性事件系统 |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 启用原生 LLM 请求路径 |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 启用并行 Web 搜索执行 |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | 启用 Scout 子代理 |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | 启用工作区支持 |
|
||||
|
||||
@@ -597,7 +597,7 @@ OpenCode 可以透過環境變數進行設定。
|
||||
|
||||
| 變數 | 類型 | 說明 |
|
||||
| ----------------------------------------------- | ------- | ------------------------------- |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | 啟用所有實驗性功能 |
|
||||
| `OPENCODE_EXPERIMENTAL` | boolean | 啟用受總開關控制的實驗性功能 |
|
||||
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 啟用圖示探索 |
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 停用 TUI 中的選取即複製 |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 指令的預設逾時時間(毫秒) |
|
||||
@@ -609,3 +609,9 @@ OpenCode 可以透過環境變數進行設定。
|
||||
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 啟用實驗性 Exa 功能 |
|
||||
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 為 python 檔案啟用 TY LSP |
|
||||
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 啟用計畫模式 |
|
||||
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 啟用背景子代理任務 |
|
||||
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 啟用實驗性事件系統 |
|
||||
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 啟用原生 LLM 請求路徑 |
|
||||
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 啟用平行 Web 搜尋執行 |
|
||||
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | 啟用 Scout 子代理 |
|
||||
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | 啟用工作區支援 |
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
diff --git a/lib/solid/Virtualizer.d.ts b/lib/solid/Virtualizer.d.ts
|
||||
index 144dd7f..819aab9 100644
|
||||
--- a/lib/solid/Virtualizer.d.ts
|
||||
+++ b/lib/solid/Virtualizer.d.ts
|
||||
@@ -38,6 +38,10 @@ export interface VirtualizerHandle {
|
||||
* @param index index of item
|
||||
*/
|
||||
getItemSize(index: number): number;
|
||||
+ /**
|
||||
+ * Synchronously measure currently mounted items and update cached item sizes.
|
||||
+ */
|
||||
+ measure(): void;
|
||||
/**
|
||||
* Scroll to the item specified by index.
|
||||
* @param index index of item
|
||||
diff --git a/lib/solid/index.jsx b/lib/solid/index.jsx
|
||||
index 029201a..3949cd4 100644
|
||||
--- a/lib/solid/index.jsx
|
||||
+++ b/lib/solid/index.jsx
|
||||
@@ -1085,6 +1085,7 @@ const createResizer = (store, isHorizontal) => {
|
||||
let viewportElement;
|
||||
const sizeKey = isHorizontal ? "width" : "height";
|
||||
const mountedIndexes = new WeakMap();
|
||||
+ const mountedItems = new Map();
|
||||
const resizeObserver = createResizeObserver((entries) => {
|
||||
const resizes = [];
|
||||
for (const { target, contentRect } of entries) {
|
||||
@@ -1111,12 +1112,27 @@ const createResizer = (store, isHorizontal) => {
|
||||
},
|
||||
$observeItem: (el, i) => {
|
||||
mountedIndexes.set(el, i);
|
||||
+ mountedItems.set(i, el);
|
||||
resizeObserver._observe(el);
|
||||
return () => {
|
||||
mountedIndexes.delete(el);
|
||||
+ if (mountedItems.get(i) === el) {
|
||||
+ mountedItems.delete(i);
|
||||
+ }
|
||||
resizeObserver._unobserve(el);
|
||||
};
|
||||
},
|
||||
+ $measureItems: () => {
|
||||
+ const resizes = [];
|
||||
+ mountedItems.forEach((el, index) => {
|
||||
+ if (!el.offsetParent)
|
||||
+ return;
|
||||
+ resizes.push([index, el.getBoundingClientRect()[sizeKey]]);
|
||||
+ });
|
||||
+ if (resizes.length) {
|
||||
+ store.$update(ACTION_ITEM_RESIZE, resizes);
|
||||
+ }
|
||||
+ },
|
||||
$dispose: resizeObserver._dispose,
|
||||
};
|
||||
};
|
||||
@@ -1354,6 +1370,8 @@ const Virtualizer = (props) => {
|
||||
const range = createMemo((prev) => {
|
||||
stateVersion();
|
||||
const next = store.$getRange(props.bufferSize);
|
||||
+ next[0] = Math.max(0, next[0]);
|
||||
+ next[1] = Math.min(props.data.length - 1, next[1]);
|
||||
if (prev && isSameRange(prev, next)) {
|
||||
return prev;
|
||||
}
|
||||
@@ -1380,6 +1398,7 @@ const Virtualizer = (props) => {
|
||||
findItemIndex: store.$findItemIndex,
|
||||
getItemOffset: store.$getItemOffset,
|
||||
getItemSize: store.$getItemSize,
|
||||
+ measure: resizer.$measureItems,
|
||||
scrollToIndex: scroller.$scrollToIndex,
|
||||
scrollTo: scroller.$scrollTo,
|
||||
scrollBy: scroller.$scrollBy,
|
||||
@@ -1417,6 +1436,11 @@ const Virtualizer = (props) => {
|
||||
const indexes = [];
|
||||
if (props.keepMounted) {
|
||||
const mounted = new Set(props.keepMounted);
|
||||
+ mounted.forEach((index) => {
|
||||
+ if (index < 0 || index >= count) {
|
||||
+ mounted.delete(index);
|
||||
+ }
|
||||
+ });
|
||||
for (let [i, j] = range(); i <= j; i++) {
|
||||
mounted.add(i);
|
||||
}
|
||||
@@ -1528,6 +1552,8 @@ const WindowVirtualizer = (props) => {
|
||||
const range = createMemo((prev) => {
|
||||
stateVersion();
|
||||
const next = store.$getRange(props.bufferSize);
|
||||
+ next[0] = Math.max(0, next[0]);
|
||||
+ next[1] = Math.min(props.data.length - 1, next[1]);
|
||||
if (prev && isSameRange(prev, next)) {
|
||||
return prev;
|
||||
}
|
||||
Reference in New Issue
Block a user