Compare commits

..
Author SHA1 Message Date
jlongster d5e3dac5b2 fix(tui): soften block tool errors 2026-08-21 02:25:24 +00:00
315 changed files with 11831 additions and 9718 deletions
+570 -1096
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-PuNZrtSgh5F3KpXSM+bd+rYQuyzwWd+wCOnMJSDS2Z0=",
"aarch64-linux": "sha256-RYy8ZRf59FE/3+gICjvsZv3ekQvn+DTZaT9jefbK+0g=",
"aarch64-darwin": "sha256-1AsDK8xNj3RlzX2efbuEDEwaOLAgjFYaEvk7EkQkh4w=",
"x86_64-darwin": "sha256-8ONeOu9UmM0GRxVeOO3Uhk1yAOuW6R8tqBYswOVEkME="
"x86_64-linux": "sha256-JEqi00PCle+o5OfBlJJaZtXd+4sYB3o+rvYiESlN4dY=",
"aarch64-linux": "sha256-zk3Uk1SQyeRrQ7BuFwlOnQAptUHIkq+oPdfd+sTEq5U=",
"aarch64-darwin": "sha256-3BOd3EcqimoG3rTI6lTHe91YVlCoEi8/68eT1lbOi0c=",
"x86_64-darwin": "sha256-X7wGmjiMloF5Zhuc20kAxLC+tl613YNXRgA+dQjP2WM="
}
}
@@ -33,18 +33,6 @@ export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
export const PATH = "/messages"
export const DEFAULT_MAX_TOKENS = 32_000
const SSE_EVENTS = new Set([
"message",
"message_start",
"message_delta",
"message_stop",
"content_block_start",
"content_block_delta",
"content_block_stop",
"error",
])
export const framing = Framing.sseEvents(SSE_EVENTS)
export type ThinkingInput =
| {
readonly type: "adaptive"
@@ -246,7 +234,7 @@ export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesB
const AnthropicUsage = Schema.StructWithRest(
Schema.Struct({
input_tokens: optionalNull(Schema.Number),
input_tokens: Schema.optional(Schema.Number),
output_tokens: Schema.optional(Schema.Number),
cache_creation_input_tokens: optionalNull(Schema.Number),
cache_read_input_tokens: optionalNull(Schema.Number),
@@ -704,7 +692,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
// expose that subset through `output_tokens_details.thinking_tokens`.
const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => {
if (!usage) return undefined
const nonCached = usage.input_tokens ?? undefined
const nonCached = usage.input_tokens
const cacheRead = usage.cache_read_input_tokens ?? undefined
const cacheWrite = usage.cache_creation_input_tokens ?? undefined
const inputTokens = ProviderShared.sumTokens(nonCached, cacheRead, cacheWrite)
@@ -1051,7 +1039,7 @@ export const route = Route.make({
protocol,
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
auth: Auth.none,
framing,
framing: Framing.sse,
headers: () => ({ "anthropic-version": "2023-06-01" }),
})
+4 -13
View File
@@ -197,28 +197,19 @@ export const errorText = (error: unknown) => {
/**
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
* decoder, optionally filters named events, and drops empty / `[DONE]`
* keep-alive events so the protocol event schema sees one JSON string per
* element. The SSE channel emits a
* decoder, and drops empty / `[DONE]` keep-alive events so the protocol event
* schema sees one JSON string per element. The SSE channel emits a
* `Retry` control event on its error channel; we drop it here (we don't
* implement client-driven retries). Decoder failures become provider output
* errors so the public error channel stays `AIError`.
*/
export const sseFraming = (
bytes: Stream.Stream<Uint8Array, AIError>,
events?: ReadonlySet<string>,
): Stream.Stream<string, AIError> =>
export const sseFraming = (bytes: Stream.Stream<Uint8Array, AIError>): Stream.Stream<string, AIError> =>
bytes.pipe(
Stream.decodeText(),
Stream.pipeThroughChannel(Sse.decode()),
Stream.catchTag("Retry", () => Stream.empty),
Stream.catchTag("SseError", (error) => Stream.fail(eventError("sse", error.message))),
Stream.filter(
(event) =>
(events === undefined || events.has(event.event)) &&
event.data.length > 0 &&
(event.data !== "[DONE]" || (events !== undefined && event.event !== "message")),
),
Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"),
Stream.map((event) => event.data),
)
@@ -4,6 +4,7 @@ import { AnthropicMessages } from "../protocols/anthropic-messages.js"
import { Auth } from "../route/auth.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { Framing } from "../route/framing.js"
import { Protocol } from "../route/protocol.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import { GoogleVertexShared } from "./google-vertex-shared.js"
@@ -56,7 +57,7 @@ const route = Route.make({
}),
endpoint: Endpoint.path(({ request }) => `/${request.model.id}:streamRawPredict`),
auth: Auth.none,
framing: AnthropicMessages.framing,
framing: Framing.sse,
})
export const routes = [route]
-6
View File
@@ -24,10 +24,4 @@ export interface Definition<Frame> {
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
export const sse: Definition<string> = { id: "sse", frame: ProviderShared.sseFraming }
/** SSE framing restricted to protocol-recognized event names. */
export const sseEvents = (events: ReadonlySet<string>): Definition<string> => ({
id: "sse",
frame: (bytes) => ProviderShared.sseFraming(bytes, events),
})
export * as Framing from "./framing.js"
-3
View File
@@ -10,9 +10,6 @@ export const sseEvents = (...chunks: ReadonlyArray<unknown>): string =>
const formatChunk = (chunk: unknown) => `data: ${typeof chunk === "string" ? chunk : JSON.stringify(chunk)}\n\n`
export const sseNamedEvent = (event: string, data: unknown): string =>
`event: ${event}\ndata: ${typeof data === "string" ? data : JSON.stringify(data)}`
/**
* Build an SSE body from already-serialized strings (used when the chunk shape
* itself is part of what's being tested, e.g. malformed chunks).
@@ -8,7 +8,7 @@ import * as AnthropicMessages from "../../src/protocols/anthropic-messages.js"
import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios.js"
import { it } from "../lib/effect.js"
import { dynamicResponse, fixedResponse } from "../lib/http.js"
import { sseEvents, sseNamedEvent, sseRaw } from "../lib/sse.js"
import { sseEvents } from "../lib/sse.js"
const model = AnthropicMessages.route
.with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") })
@@ -640,60 +640,7 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("ignores unknown named SSE events", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseRaw(
sseNamedEvent("message_start", {
type: "message_start",
message: { usage: { input_tokens: 5 } },
}),
sseNamedEvent("proxy.stats", "not json"),
sseNamedEvent("content_block_start", {
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" },
}),
sseNamedEvent("content_block_delta", {
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: "Hello" },
}),
sseNamedEvent("content_block_stop", { type: "content_block_stop", index: 0 }),
sseNamedEvent("message_delta", {
type: "message_delta",
delta: { stop_reason: "end_turn" },
usage: { output_tokens: 1 },
}),
sseNamedEvent("message_stop", { type: "message_stop" }),
sseNamedEvent("proxy.done", "still not json"),
),
),
),
)
expect(response.message.content).toEqual([{ type: "text", text: "Hello" }])
expect(response.finishReason).toEqual({ normalized: "stop", raw: "end_turn" })
}),
)
it.effect("rejects malformed recognized SSE events", () =>
Effect.gen(function* () {
const error = yield* LLMClient.generate(request).pipe(
Effect.provide(fixedResponse(sseRaw(sseNamedEvent("message_start", "[DONE]")))),
Effect.flip,
)
expect(error.reason).toMatchObject({
_tag: "InvalidProviderOutput",
message: "Invalid anthropic/anthropic-messages stream event",
})
}),
)
it.effect("maps nullable input tokens and preserves unknown Anthropic usage fields", () =>
it.effect("maps thinking tokens and preserves unknown Anthropic usage fields", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
@@ -716,7 +663,6 @@ describe("Anthropic Messages route", () => {
type: "message_delta",
delta: { stop_reason: "end_turn" },
usage: {
input_tokens: null,
output_tokens: 8,
server_tool_use: { web_search_requests: 2, terminal_counter: 3 },
output_tokens_details: { terminal_detail: "preserved" },
@@ -736,7 +682,7 @@ describe("Anthropic Messages route", () => {
totalTokens: 15,
providerMetadata: {
anthropic: {
input_tokens: null,
input_tokens: 5,
cache_read_input_tokens: 2,
service_tier: "standard",
cache_creation: { ephemeral_5m_input_tokens: 1 },
@@ -11,7 +11,7 @@ import {
} from "./timeline-test-helpers"
import { waitForStableTimeline } from "./session-tab-switch-probe"
const contentSelector = '[data-message-id], [data-component="composer-editor"]'
const contentSelector = '[data-message-id], [data-component="prompt-input"]'
const draftID = "draft_first_navigation"
benchmark.describe("performance: first navigation paint", () => {
@@ -41,11 +41,11 @@ benchmark.describe("performance: first navigation paint", () => {
href,
destinationPath: href,
sourceSelector: messageSelector(fixture.expected.sourceMessageIDs.at(-1)!),
destinationSelector: '[data-component="composer-editor"]',
destinationSelector: '[data-component="prompt-input"]',
contentSelector,
navigate: async () => {
await page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first().click()
await expect(page.locator('[data-component="composer-editor"]')).toBeVisible()
await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
},
})
report(result)
@@ -46,7 +46,7 @@ test("matches the rounded panel corners to the dark new-session background", asy
)
await page.goto(`/new-session?draftId=${draftID}`)
await expectAppVisible(page.locator('[data-component="composer-editor"]'))
await expectAppVisible(page.locator('[data-component="prompt-input"]'))
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
const panel = page.locator('main div[class*="rounded-[10px]"][class*="overflow-hidden"]')
await expect(panel).toHaveCount(1)
@@ -3,9 +3,9 @@ import { base64Encode } from "@opencode-ai/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
const directory = "C:/OpenCode/ComposerEditing"
const projectID = "proj_composer_editing"
const sessionID = "ses_composer_editing"
const directory = "C:/OpenCode/PromptInputV2Editing"
const projectID = "proj_prompt_input_v2_editing"
const sessionID = "ses_prompt_input_v2_editing"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("preserves the draft when a populated command menu triggers a built-in", async ({ page }) => {
@@ -15,7 +15,7 @@ test("preserves the draft when a populated command menu triggers a built-in", as
id: projectID,
worktree: directory,
vcs: "git",
name: "composer-editing",
name: "prompt-input-v2-editing",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
@@ -23,10 +23,10 @@ test("preserves the draft when a populated command menu triggers a built-in", as
sessions: [
{
id: sessionID,
slug: "composer-editing",
slug: "prompt-input-v2-editing",
projectID,
directory,
title: "Composer editing",
title: "Prompt input V2 editing",
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
@@ -34,11 +34,11 @@ test("preserves the draft when a populated command menu triggers a built-in", as
pageMessages: () => ({ items: [] }),
})
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
const composer = page.locator('[data-component="composer"]')
const input = composer.locator('[data-component="composer-editor"]')
await expect
.poll(() => input.evaluate((element) => getComputedStyle(element, "::before").content))
.toBe(`"${String.fromCodePoint(0x200b)}"`)
const composer = page.locator('[data-component="prompt-input-v2"]')
const input = composer.locator('[data-component="prompt-input"]')
await expect.poll(() => input.evaluate((element) => getComputedStyle(element, "::before").content)).toBe(
`"${String.fromCodePoint(0x200b)}"`,
)
await expectAppVisible(composer)
await input.fill("keep me")
@@ -8,7 +8,7 @@ const projectID = "proj_prompt_thinking_level_regression"
const sessionID = "ses_prompt_thinking_level_regression"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("shows the thinking level control while relevant", async ({ page }) => {
test("shows the V2 thinking level control while relevant", async ({ page }) => {
await mockOpenCodeServer(page, {
directory,
project: {
@@ -51,8 +51,8 @@ test("shows the thinking level control while relevant", async ({ page }) => {
pageMessages: () => ({ items: [] }),
})
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
const composer = page.locator('[data-component="composer"]')
const input = composer.locator('[data-component="composer-editor"]')
const composer = page.locator('[data-component="prompt-input-v2"]')
const input = composer.locator('[data-component="prompt-input"]')
const control = composer.getByRole("button", { name: "Choose model variant" })
await expectAppVisible(composer)
@@ -224,8 +224,6 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) })
if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`))
return json(route, { data: [], cursor: {} })
if (sessions.some((session) => url.pathname === `/api/session/${session.id}/inbox`))
return json(route, { data: [] })
if (url.pathname === "/api/location") return json(route, { directory })
if (url.pathname === "/api/vcs")
return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } })
@@ -1,79 +0,0 @@
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
import { base64Encode } from "@opencode-ai/util/encode"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/SessionMessageRevert"
const projectID = "proj_session_message_revert"
const sessionID = "ses_session_message_revert"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const messages = [
{ id: "msg_first", type: "user", text: "First prompt", time: { created: 1 } },
{
id: "msg_first_reply",
type: "assistant",
agent: "build",
model: { id: "test", providerID: "opencode" },
content: [{ type: "text", text: "First reply" }],
time: { created: 2, completed: 3 },
},
{ id: "msg_second", type: "user", text: "Second prompt", time: { created: 4 } },
] satisfies SessionMessageInfo[]
test("reverts directly to the selected user message", async ({ page }) => {
const staged: { sessionID: string; messageID: string }[] = []
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
canonical: directory,
vcs: "git",
name: "session-message-revert",
time: { created: 1, updated: 1 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", variants: {}, limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [
{
id: sessionID,
slug: "session-message-revert",
projectID,
directory,
title: "Session message revert",
agent: "build",
model: { id: "test", providerID: "opencode" },
version: "dev",
time: { created: 1, updated: 4 },
},
],
pageMessages: () => ({ items: messages }),
onRevertStage: (input) => staged.push(input),
})
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Session message revert")
const message = page.locator('[data-message-id="msg_second"]')
await message.hover()
const response = page.waitForResponse(
(response) =>
response.request().method() === "POST" &&
new URL(response.url()).pathname === `/api/session/${sessionID}/revert/stage`,
)
await message.getByRole("button", { name: "Revert message" }).click()
expect((await response).ok()).toBe(true)
await expect(page.getByRole("textbox", { name: "Prompt" })).toHaveText("Second prompt")
expect(staged).toEqual([{ sessionID, messageID: "msg_second" }])
})
@@ -120,7 +120,7 @@ test("restores the draft caret before typing after a request dock closes", async
await transport.waitForConnection()
await expectSessionTitle(page, title)
const editor = page.locator('[data-component="composer-editor"][contenteditable="true"]')
const editor = page.locator('[data-component="prompt-input"][contenteditable="true"]')
const draft = "keep the caret at the end"
await editor.fill(draft)
await page.evaluate(() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve())))
@@ -103,7 +103,7 @@ test("labels completed searches with result counts", async ({ page }) => {
await expect(rows.nth(1)).toContainText("(12 matches)")
})
test("labels read tools from their path input", async ({ page }) => {
test("labels V2 read tools from their path input", async ({ page }) => {
const id = "prt_read_path"
await setupTimeline(page, {
messages: [userMessage(), assistantMessage([toolPart(id, "read", "completed", { path: "src/a.ts" })])],
@@ -114,7 +114,7 @@ test("labels read tools from their path input", async ({ page }) => {
await expect(group.locator('[data-slot="basic-tool-tool-subtitle"]')).toHaveText("a.ts")
})
test("labels skill tools from IDs and result metadata", async ({ page }) => {
test("labels V2 skill tools from IDs and result metadata", async ({ page }) => {
const pending = "prt_skill_id"
const completed = "prt_skill_name"
await setupTimeline(page, {
@@ -131,10 +131,9 @@ test("labels skill tools from IDs and result metadata", async ({ page }) => {
"aria-label",
"sample-skill",
)
await expect(page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`)).toHaveAttribute(
"aria-label",
"OpenCode",
)
await expect(
page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`),
).toHaveAttribute("aria-label", "OpenCode")
for (const id of [pending, completed]) {
const skill = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(skill.locator('[data-slot="skill-tool-label"]')).toHaveText("Skill")
@@ -153,7 +152,8 @@ function errorInput(tool: string) {
if (tool === "patch") return { patchText: "Update src/error.ts" }
if (tool === "webfetch") return { url: "https://example.com" }
if (tool === "websearch") return { query: "failure" }
if (tool === "subagent") return { description: "Fail subagent", agent: "explore", prompt: "Inspect the failure." }
if (tool === "subagent")
return { description: "Fail subagent", agent: "explore", prompt: "Inspect the failure." }
if (tool === "skill") return { name: "failure" }
return { target: "failure" }
}
@@ -87,7 +87,7 @@ test("reconnects after a stream error", async ({ page }) => {
expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
})
test("does not request replay when reconnecting the volatile event stream", async ({ page }) => {
test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10 })
const events = partUpdated(textPart("prt_transport_id", "event with id"))
const first = (
@@ -77,7 +77,7 @@ test("routes typing to the composer unless the open terminal is focused", async
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="composer-editor"]')
const composer = page.locator('[data-component="prompt-input"]')
const terminal = page.locator('[data-component="terminal"]')
await page.keyboard.press("Control+Backquote")
await expect(terminal).toBeVisible()
@@ -116,7 +116,7 @@ test("keeps composer focus when a cached terminal finishes mounting", async ({ p
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`, { waitUntil: "commit" })
await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="composer-editor"]')
const composer = page.locator('[data-component="prompt-input"]')
const terminal = page.locator('[data-component="terminal"]')
await expect(terminal).toBeVisible()
expect(created.count).toBe(0)
@@ -142,7 +142,7 @@ test("keeps newer composer focus while an explicit terminal open finishes", asyn
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="composer-editor"]')
const composer = page.locator('[data-component="prompt-input"]')
const terminal = page.locator('[data-component="terminal"]')
await page.keyboard.press("Control+Backquote")
await expect(terminal).toBeVisible()
@@ -187,7 +187,7 @@ test("focuses a terminal created from the new-terminal button", async ({ page })
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="composer-editor"]')
const composer = page.locator('[data-component="prompt-input"]')
const terminal = page.locator('[data-component="terminal"]')
await page.keyboard.press("Control+Backquote")
await expect(terminal.locator("textarea")).toHaveCount(1)
@@ -78,9 +78,9 @@ test("creates a session in a new project and selects its model", async ({ page }
await selectFolder.click()
await page.locator('[data-action="home-new-session"]').click()
await expectAppVisible(page.locator('[data-component="composer"]'))
await expectAppVisible(page.locator('[data-component="prompt-input-v2"]'))
const modelControl = page.locator('[data-action="composer-model"]')
const modelControl = page.locator('[data-action="prompt-model"]')
await modelControl.click()
await expect(page.locator('[data-option-key="opencode:free-model"]')).toBeVisible()
const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]')
-10
View File
@@ -22,7 +22,6 @@ export interface MockServerConfig {
onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
message?: (sessionID: string, messageID: string) => SessionMessageInfo | undefined
onMessage?: (input: { sessionID: string; messageID: string }) => void
onRevertStage?: (input: { sessionID: string; messageID: string }) => void
events?: () => OpenCodeEvent[]
eventRetry?: number
permissions?: unknown[] | (() => unknown[])
@@ -384,15 +383,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
) {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
}
const revertStage = path.match(/^\/api\/session\/([^/]+)\/revert\/stage$/)?.[1]
if (revertStage && route.request().method() === "POST") {
const body = route.request().postDataJSON()
if (!body || typeof body !== "object" || !("messageID" in body) || typeof body.messageID !== "string") {
return json(route, { error: "Invalid revert request" }, undefined, 400)
}
config.onRevertStage?.({ sessionID: revertStage, messageID: body.messageID })
return json(route, { data: { messageID: body.messageID } })
}
if (/^\/api\/session\/[^/]+$/.test(path) && route.request().method() === "DELETE") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
}
@@ -1,14 +1,14 @@
.command-palette {
.command-palette-v2 {
overflow: hidden;
}
/* Anchor to the top edge of where a centered 480px-tall dialog would sit, so the
top stays put while the content-driven height grows and shrinks. */
[data-component="dialog-v2"]:has(.command-palette) {
[data-component="dialog-v2"]:has(.command-palette-v2) {
align-items: flex-start;
}
[data-component="dialog-v2"]:has(.command-palette) [data-slot="dialog-container"] {
[data-component="dialog-v2"]:has(.command-palette-v2) [data-slot="dialog-container"] {
width: min(calc(100vw - 24px), 640px);
height: auto;
min-height: 280px;
@@ -19,7 +19,7 @@
box-shadow: var(--v2-elevation-floating);
}
.command-palette-body {
.command-palette-v2-body {
display: flex;
min-height: 0;
flex: 1;
@@ -28,12 +28,12 @@
padding: 0;
}
.command-palette-search {
.command-palette-v2-search {
flex-shrink: 0;
padding: 6px;
}
.command-palette-search [data-component="text-input-v2"] {
.command-palette-v2-search [data-component="text-input-v2"] {
width: 100%;
height: 36px;
border-radius: 6px;
@@ -45,46 +45,46 @@
box-shadow 120ms ease-in-out;
}
.command-palette-search [data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]),
.command-palette-search [data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) {
.command-palette-v2-search [data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]),
.command-palette-v2-search [data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) {
background: var(--v2-background-bg-layer-02);
box-shadow: none;
}
.command-palette-search [data-component="text-input-v2"] [data-slot="text-input-v2-value"] {
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-value"] {
border-radius: 6px;
background: transparent;
gap: 8px;
}
.command-palette-search [data-component="text-input-v2"] [data-slot="text-input-v2-leading-icon"] {
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-leading-icon"] {
padding-left: 12px;
}
.command-palette-search [data-component="text-input-v2"] [data-slot="text-input-v2-input"] {
.command-palette-v2-search [data-component="text-input-v2"] [data-slot="text-input-v2-input"] {
border-radius: 6px;
background: transparent;
}
.command-palette-scroll {
.command-palette-v2-scroll {
min-height: 0;
flex: 1;
}
.command-palette-results {
.command-palette-v2-results {
display: flex;
flex-direction: column;
gap: 16px;
padding: 6px 6px 8px;
}
.command-palette-group {
.command-palette-v2-group {
display: flex;
flex-direction: column;
gap: 1px;
}
.command-palette-group-title {
.command-palette-v2-group-title {
margin: 6px 0;
padding: 0 12px;
color: var(--v2-text-text-muted);
@@ -95,7 +95,7 @@
user-select: none;
}
.command-palette-row {
.command-palette-v2-row {
display: flex;
width: 100%;
height: 36px;
@@ -113,16 +113,16 @@
scroll-margin: 6px 0;
}
.command-palette-row[data-active] {
.command-palette-v2-row[data-active] {
background: var(--v2-overlay-simple-overlay-hover);
}
.command-palette-row:focus-visible {
.command-palette-v2-row:focus-visible {
background: var(--v2-overlay-simple-overlay-hover);
outline: none;
}
.command-palette-row-main {
.command-palette-v2-row-main {
display: flex;
min-width: 0;
flex: 1;
@@ -130,19 +130,19 @@
gap: 8px;
}
.command-palette-row-icon {
.command-palette-v2-row-icon {
flex-shrink: 0;
color: var(--v2-icon-icon-muted);
}
.command-palette-row-text {
.command-palette-v2-row-text {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.command-palette-title {
.command-palette-v2-title {
min-width: 0;
overflow: hidden;
color: var(--v2-text-text-base);
@@ -154,8 +154,8 @@
white-space: nowrap;
}
.command-palette-description,
.command-palette-meta {
.command-palette-v2-description,
.command-palette-v2-meta {
min-width: 0;
overflow: hidden;
color: var(--v2-text-text-muted);
@@ -167,11 +167,11 @@
white-space: nowrap;
}
.command-palette-meta {
.command-palette-v2-meta {
flex-shrink: 0;
}
.command-palette-file-path {
.command-palette-v2-file-path {
display: flex;
min-width: 0;
align-items: baseline;
@@ -180,7 +180,7 @@
letter-spacing: -0.04px;
}
.command-palette-file-dir {
.command-palette-v2-file-dir {
min-width: 0;
overflow: hidden;
color: var(--v2-text-text-muted);
@@ -189,14 +189,14 @@
white-space: nowrap;
}
.command-palette-file-name {
.command-palette-v2-file-name {
flex-shrink: 0;
color: var(--v2-text-text-base);
font-weight: 530;
white-space: nowrap;
}
.command-palette-state {
.command-palette-v2-state {
display: grid;
min-height: 120px;
place-items: center;
@@ -208,13 +208,13 @@
}
@media (max-width: 640px) {
.command-palette-row-text {
.command-palette-v2-row-text {
flex-direction: column;
align-items: flex-start;
gap: 1px;
}
.command-palette-description {
.command-palette-v2-description {
max-width: 100%;
}
}
@@ -22,7 +22,7 @@ import {
uniqueCommandPaletteEntries,
type CommandPaletteEntry,
} from "./command-palette"
import "./dialog-command-palette.css"
import "./dialog-command-palette-v2.css"
function groups(entries: CommandPaletteEntry[]) {
const map = new Map<string, CommandPaletteEntry[]>()
@@ -35,7 +35,7 @@ function matchesEntry(entry: CommandPaletteEntry, query: string) {
return [entry.title, entry.description, entry.category].some((text) => text?.toLowerCase().includes(value))
}
export function DialogCommandPalette(props: { onOpenFile?: (path: string) => void }) {
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
const palette = createCommandPaletteModel(props)
const loadItems = async (text: string) => {
const q = text.trim()
@@ -61,7 +61,7 @@ export function DialogCommandPalette(props: { onOpenFile?: (path: string) => voi
)
}
export function DialogHomeCommandPalette(props: {
export function DialogHomeCommandPaletteV2(props: {
server: ServerConnection.Any
onSelectSession: (entry: CommandPaletteEntry) => void
}) {
@@ -188,9 +188,9 @@ function CommandPaletteView(props: {
}
return (
<Dialog class="command-palette" size="large">
<DialogBody class="command-palette-body">
<div class="command-palette-search">
<Dialog class="command-palette-v2" size="large">
<DialogBody class="command-palette-v2-body">
<div class="command-palette-v2-search">
<TextInput
value={query()}
autofocus
@@ -203,21 +203,21 @@ function CommandPaletteView(props: {
onKeyDown={handleKeyDown}
/>
</div>
<ScrollView class="command-palette-scroll" viewportRef={(el) => (resultsRef = el)}>
<div class="command-palette-results" role="listbox">
<ScrollView class="command-palette-v2-scroll" viewportRef={(el) => (resultsRef = el)}>
<div class="command-palette-v2-results" role="listbox">
<Show
when={visibleEntries().length > 0}
fallback={
<div class="command-palette-state">
<div class="command-palette-v2-state">
{entries.loading ? language.t("common.loading") : language.t("palette.empty")}
</div>
}
>
<For each={groupedEntries()}>
{(group) => (
<div class="command-palette-group">
<div class="command-palette-v2-group">
<Show when={group.category}>
<div class="command-palette-group-title">{group.category}</div>
<div class="command-palette-v2-group-title">{group.category}</div>
</Show>
<For each={group.entries}>
{(item) => (
@@ -262,7 +262,7 @@ function PaletteRow(props: {
return (
<button
type="button"
class="command-palette-row group"
class="command-palette-v2-row group"
role="option"
aria-selected={props.active}
data-active={props.active ? "" : undefined}
@@ -276,21 +276,21 @@ function PaletteRow(props: {
>
<Switch
fallback={
<div class="command-palette-row-main">
<FileIcon node={{ path: props.item.path ?? "", type: "file" }} class="command-palette-row-icon size-4" />
<div class="command-palette-file-path">
<span class="command-palette-file-dir">{getDirectory(props.item.path ?? "")}</span>
<span class="command-palette-file-name">{getFilename(props.item.path ?? "")}</span>
<div class="command-palette-v2-row-main">
<FileIcon node={{ path: props.item.path ?? "", type: "file" }} class="command-palette-v2-row-icon size-4" />
<div class="command-palette-v2-file-path">
<span class="command-palette-v2-file-dir">{getDirectory(props.item.path ?? "")}</span>
<span class="command-palette-v2-file-name">{getFilename(props.item.path ?? "")}</span>
</div>
</div>
}
>
<Match when={props.item.type === "command"}>
<div class="command-palette-row-main">
<div class="command-palette-row-text">
<span class="command-palette-title">{props.item.title}</span>
<div class="command-palette-v2-row-main">
<div class="command-palette-v2-row-text">
<span class="command-palette-v2-title">{props.item.title}</span>
<Show when={props.item.description}>
<span class="command-palette-description">{props.item.description}</span>
<span class="command-palette-v2-description">{props.item.description}</span>
</Show>
</div>
</div>
@@ -299,7 +299,7 @@ function PaletteRow(props: {
</Show>
</Match>
<Match when={props.item.type === "session"}>
<div class="command-palette-row-main">
<div class="command-palette-v2-row-main">
<div class="relative shrink-0">
<Show when={props.sessionOpen}>
<span
@@ -319,19 +319,19 @@ function PaletteRow(props: {
)}
</Show>
</div>
<div class="command-palette-row-text">
<span class="command-palette-title" classList={{ "opacity-70": !!props.item.archived }}>
<div class="command-palette-v2-row-text">
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
{props.item.title}
</span>
<Show when={props.item.description}>
<span class="command-palette-description" classList={{ "opacity-70": !!props.item.archived }}>
<span class="command-palette-v2-description" classList={{ "opacity-70": !!props.item.archived }}>
{props.item.description}
</span>
</Show>
</div>
</div>
<Show when={props.item.updated}>
<span class="command-palette-meta">
<span class="command-palette-v2-meta">
{getRelativeTime(new Date(props.item.updated!).toISOString(), props.language.t)}
</span>
</Show>
+2 -2
View File
@@ -1,7 +1,7 @@
import { Component, createMemo } from "solid-js"
import { useNavigate, useParams } from "@solidjs/router"
import { useData } from "@/context/server"
import { useComposerState } from "@/composer/persistence"
import { usePrompt } from "@/context/prompt"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
@@ -30,7 +30,7 @@ export const DialogFork: Component = () => {
const data = useData()
const serverSDK = useServerSDK()
const location = useWorkspaceLocation()
const prompt = useComposerState()
const prompt = usePrompt()
const dialog = useDialog()
const language = useLanguage()
const server = useServer()
@@ -2,9 +2,11 @@ import { Button } from "@opencode-ai/ui/button"
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { List } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/switch"
import { TextInput } from "@opencode-ai/ui/text-input"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { For, Show, type Component } from "solid-js"
import { useLocal } from "@/context/local"
@@ -25,6 +27,104 @@ export const DialogManageModels: Component = () => {
const dialog = useDialog()
const directory = () => decode64(local.slug())
const handleConnectProvider = () => {
void dialog.show(() => <DialogConnectProvider directory={directory()} />)
}
const providerRank = (id: string) => popularProviders.indexOf(id)
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
const providerVisible = (providerID: string) =>
providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id }))
const setProviderVisibility = (providerID: string, checked: boolean) => {
providerList(providerID).forEach((x) => {
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked)
})
}
return (
<Dialog>
<DialogHeader hideClose>
<DialogTitleGroup
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
/>
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
{language.t("command.provider.connect")}
</Button>
</DialogHeader>
<DialogBody>
<List
class="px-3"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.id}
groupHeader={(group) => {
const provider = group.items[0].provider
return (
<>
<span>{provider.name}</span>
<Tooltip
appearance="standard"
placement="top"
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
>
<Switch
appearance="standard"
class="-mr-1"
checked={providerVisible(provider.id)}
onChange={(checked) => setProviderVisibility(provider.id, checked)}
hideLabel
>
{provider.name}
</Switch>
</Tooltip>
</>
)
}}
sortGroupsBy={(a, b) => {
const aRank = providerRank(a.items[0].provider.id)
const bRank = providerRank(b.items[0].provider.id)
const aPopular = aRank >= 0
const bPopular = bRank >= 0
if (aPopular && !bPopular) return -1
if (!aPopular && bPopular) return 1
return aRank - bRank
}}
onSelect={(x) => {
if (!x) return
const key = { modelID: x.id, providerID: x.provider.id }
local.model.setVisibility(key, !local.model.visible(key))
}}
>
{(i) => (
<div class="w-full flex items-center justify-between gap-x-3">
<span>{i.name}</span>
<div onClick={(e) => e.stopPropagation()}>
<Switch
appearance="standard"
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
onChange={(checked) => {
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
}}
/>
</div>
</div>
)}
</List>
</DialogBody>
</Dialog>
)
}
export const DialogManageModelsV2: Component = () => {
const local = useLocal()
const language = useLanguage()
const dialog = useDialog()
const directory = () => decode64(local.slug())
const handleConnectProvider = () => {
void dialog.show(() => <DialogConnectProvider directory={directory()} />)
}
@@ -2,7 +2,7 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createSignal, onMount } from "solid-js"
import { DialogSelectModelUnpaid } from "./dialog-select-model-unpaid"
import { DialogSelectModelUnpaidV2 } from "./dialog-select-model-unpaid-v2"
const names = [
"MiMo V2.5 Free",
@@ -34,7 +34,7 @@ function SelectModelWithoutProviders() {
setCurrent(models.find((item) => item.id === value?.modelID))
},
}
const open = () => dialog.show(() => <DialogSelectModelUnpaid model={model} />)
const open = () => dialog.show(() => <DialogSelectModelUnpaidV2 model={model} />)
onMount(open)
@@ -0,0 +1,177 @@
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
import { Badge } from "@opencode-ai/ui/badge"
import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme } from "@opencode-ai/ui/theme"
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { useIntegrations } from "@/hooks/use-integrations"
import { decode64 } from "@/utils/base64"
import { useLanguage } from "@/context/language"
import { ModelTooltip } from "./model-tooltip"
type ModelState = ReturnType<typeof useLocal>["model"]
const featuredProviders = ["opencode", "opencode-go", "openai", "anthropic", "google", "github-copilot"]
const displayModelName = (name: string) => name.replace(/\s+(?:\(free\)|free)$/i, "")
export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (props) => {
const local = useLocal()
const model = props.model ?? local.model
const dialog = useDialog()
const theme = useTheme()
const directory = () => decode64(local.slug())
const integrations = useIntegrations(directory)
const language = useLanguage()
const modelKey = (item: ReturnType<ModelState["list"]>[number]) => `${item.provider.id}:${item.id}`
const currentKey = createMemo(() => {
const c = model.current()
return c ? `${c.provider.id}:${c.id}` : undefined
})
const isFree = (item: ReturnType<ModelState["list"]>[number]) =>
item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)
const freeModels = createMemo(() => model.list().filter(isFree))
const openProviders = (provider?: string) => {
void import("./dialog-connect-provider").then((x) => {
const controller = x.useProviderConnectController()
controller.select(provider)
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory()} />)
})
}
const selectModel = (item: ReturnType<ModelState["list"]>[number]) => {
model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true })
dialog.close()
}
// Focus starts on the dialog's close button, outside the list, so listen at the
// document level while the dialog is mounted instead of on the list container.
let listEl: HTMLDivElement | undefined
onMount(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return
if (!listEl) return
const buttons = Array.from(listEl.querySelectorAll<HTMLButtonElement>("button"))
if (buttons.length === 0) return
const index = buttons.indexOf(document.activeElement as HTMLButtonElement)
const next =
index < 0 ? (e.key === "ArrowDown" ? 0 : buttons.length - 1) : index + (e.key === "ArrowDown" ? 1 : -1)
buttons[(next + buttons.length) % buttons.length]?.focus()
e.preventDefault()
}
document.addEventListener("keydown", handleKeyDown)
onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
})
return (
<Dialog
fit
containerClass="!h-auto max-h-[calc(100vh_-_16px)] !w-[min(calc(100vw_-_16px),640px)]"
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
>
<DialogHeader closeLabel={language.t("common.close")}>
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
</DialogHeader>
<DialogBody class="max-h-[calc(100vh_-_68px)] min-h-0 flex-none gap-0 overflow-y-auto px-2 pb-2">
<div ref={listEl} class="flex min-h-0 flex-col">
<div data-section="free-models" class="flex w-full flex-col items-start pb-3">
<div class="flex h-8 w-full flex-none select-none flex-row items-center px-3 pb-2">
<div class="flex h-5 items-center text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:var(--v2-font-family-sans)] [font-variant-numeric:tabular-nums] [font-variation-settings:'slnt'_0]">
{language.t("dialog.model.unpaid.freeModels.title")}
</div>
</div>
<For each={freeModels()}>
{(item) => (
<Tooltip
class="w-full"
placement="right-start"
gutter={6}
openDelay={0}
contentStyle={{ "font-family": "var(--v2-font-family-sans)" }}
value={
<ModelTooltip
model={{ ...item, name: displayModelName(item.name) }}
latest={item.latest}
free={isFree(item)}
v2
/>
}
>
<button
type="button"
class="flex w-full scroll-my-3.5 flex-row items-center gap-1.5 rounded-md px-3 py-2 text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:var(--v2-font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none"
onClick={() => selectModel(item)}
>
<span class="min-w-0 truncate">{displayModelName(item.name)}</span>
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
<Show when={item.latest}>
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
</Show>
<Show when={currentKey() === modelKey(item)}>
<Icon name="check" class="ml-auto size-4 shrink-0 text-v2-icon-icon-base" />
</Show>
</button>
</Tooltip>
)}
</For>
</div>
<div class="flex w-full flex-col">
<div class="flex w-full flex-col items-start rounded-lg border-[0.5px] border-v2-border-border-muted bg-v2-background-bg-layer-02 p-2.5 pt-2">
<div class="flex h-8 w-full select-none items-center px-0.5 pb-2">
<div class="flex h-5 items-center text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:var(--v2-font-family-sans)] [font-variant-numeric:tabular-nums] [font-variation-settings:'slnt'_0]">
{language.t("dialog.model.unpaid.addMore.title")}
</div>
</div>
<div class="grid w-full grid-cols-1 gap-y-1.5 gap-x-2 sm:grid-cols-2">
<For
each={integrations
.list()
.filter((provider) => featuredProviders.includes(provider.id))
.sort((a, b) => featuredProviders.indexOf(a.id) - featuredProviders.indexOf(b.id))}
>
{(provider) => (
<button
type="button"
data-provider-id={provider.id}
class="flex min-h-11 w-full scroll-my-3.5 flex-row items-start gap-2 rounded-md bg-v2-background-bg-base px-3 py-2.5 text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:var(--v2-font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-background-bg-layer-01 focus:bg-v2-background-bg-layer-01 focus:outline-none"
classList={{
"border-[0.5px] border-transparent shadow-[var(--v2-elevation-raised)]":
theme.mode() !== "dark",
"border-[0.5px] border-v2-border-border-strong": theme.mode() === "dark",
}}
onClick={() => openProviders(provider.id)}
>
<ProviderIcon id={provider.id} class="mt-0.5 size-4 shrink-0 text-v2-icon-icon-base" />
<span class="flex min-w-0 flex-col">
<span class="truncate">{provider.name}</span>
<Show when={provider.id === "opencode" || provider.id === "opencode-go"}>
<span class="truncate font-[440] text-v2-text-text-muted">
{language.t(
provider.id === "opencode"
? "dialog.provider.opencode.tagline"
: "dialog.provider.opencodeGo.tagline",
)}
</span>
</Show>
</span>
</button>
)}
</For>
<button
type="button"
class="col-span-full flex h-8 w-full scroll-my-3.5 items-center justify-start rounded-md px-3 text-left text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:var(--v2-font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none"
onClick={() => openProviders()}
>
{language.t("dialog.model.unpaid.viewMoreProviders")}
</button>
</div>
</div>
</div>
</div>
</DialogBody>
</Dialog>
)
}
@@ -1,37 +1,26 @@
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode-ai/ui/dialog"
import { Badge } from "@opencode-ai/ui/badge"
import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme } from "@opencode-ai/ui/theme"
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { List, type ListRef } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Badge } from "@opencode-ai/ui/badge"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { type Component, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { useIntegrations } from "@/hooks/use-integrations"
import { decode64 } from "@/utils/base64"
import { useLanguage } from "@/context/language"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { ModelTooltip } from "./model-tooltip"
import { useLanguage } from "@/context/language"
import { decode64 } from "@/utils/base64"
type ModelState = ReturnType<typeof useLocal>["model"]
const featuredProviders = ["opencode", "opencode-go", "openai", "anthropic", "google", "github-copilot"]
const displayModelName = (name: string) => name.replace(/\s+(?:\(free\)|free)$/i, "")
export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props) => {
const local = useLocal()
const model = props.model ?? local.model
const dialog = useDialog()
const theme = useTheme()
const directory = () => decode64(local.slug())
const integrations = useIntegrations(directory)
const providers = useProviders(directory)
const language = useLanguage()
const modelKey = (item: ReturnType<ModelState["list"]>[number]) => `${item.provider.id}:${item.id}`
const currentKey = createMemo(() => {
const c = model.current()
return c ? `${c.provider.id}:${c.id}` : undefined
})
const isFree = (item: ReturnType<ModelState["list"]>[number]) =>
item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)
const freeModels = createMemo(() => model.list().filter(isFree))
const openProviders = (provider?: string) => {
void import("./dialog-connect-provider").then((x) => {
@@ -41,132 +30,118 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
})
}
const selectModel = (item: ReturnType<ModelState["list"]>[number]) => {
model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true })
dialog.close()
const connect = (provider: string) => openProviders(provider)
const all = () => openProviders()
let listRef: ListRef | undefined
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") return
listRef?.onKeyDown(e)
}
// Focus starts on the dialog's close button, outside the list, so listen at the
// document level while the dialog is mounted instead of on the list container.
let listEl: HTMLDivElement | undefined
onMount(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return
if (!listEl) return
const buttons = Array.from(listEl.querySelectorAll<HTMLButtonElement>("button"))
if (buttons.length === 0) return
const index = buttons.indexOf(document.activeElement as HTMLButtonElement)
const next =
index < 0 ? (e.key === "ArrowDown" ? 0 : buttons.length - 1) : index + (e.key === "ArrowDown" ? 1 : -1)
buttons[(next + buttons.length) % buttons.length]?.focus()
e.preventDefault()
}
document.addEventListener("keydown", handleKeyDown)
onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
})
return (
<Dialog
fit
containerClass="!h-auto max-h-[calc(100vh_-_16px)] !w-[min(calc(100vw_-_16px),640px)]"
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
>
<DialogHeader closeLabel={language.t("common.close")}>
<Dialog class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none">
<DialogHeader>
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
</DialogHeader>
<DialogBody class="max-h-[calc(100vh_-_68px)] min-h-0 flex-none gap-0 overflow-y-auto px-2 pb-2">
<div ref={listEl} class="flex min-h-0 flex-col">
<div data-section="free-models" class="flex w-full flex-col items-start pb-3">
<div class="flex h-8 w-full flex-none select-none flex-row items-center px-3 pb-2">
<div class="flex h-5 items-center text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:var(--v2-font-family-sans)] [font-variant-numeric:tabular-nums] [font-variation-settings:'slnt'_0]">
{language.t("dialog.model.unpaid.freeModels.title")}
<DialogBody>
<div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
<List
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
ref={(ref) => (listRef = ref)}
items={model.list}
current={model.current()}
key={(x) => `${x.provider.id}:${x.id}`}
itemWrapper={(item, node) => (
<Tooltip
appearance="standard"
class="w-full"
placement="right-start"
gutter={12}
value={
<ModelTooltip
model={item}
latest={item.latest}
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
/>
}
>
{node}
</Tooltip>
)}
onSelect={(x) => {
model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
dialog.close()
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Badge appearance="standard">{language.t("model.tag.free")}</Badge>
<Show when={i.latest}>
<Badge appearance="standard">{language.t("model.tag.latest")}</Badge>
</Show>
</div>
</div>
<For each={freeModels()}>
{(item) => (
<Tooltip
class="w-full"
placement="right-start"
gutter={6}
openDelay={0}
contentStyle={{ "font-family": "var(--v2-font-family-sans)" }}
value={
<ModelTooltip
model={{ ...item, name: displayModelName(item.name) }}
latest={item.latest}
free={isFree(item)}
v2
/>
}
)}
</List>
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
<div class="w-full">
<List
class="w-full px-3"
key={(p) => p.id}
items={providers.popular}
activeIcon="plus-small"
sortBy={(a, b) => {
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
onSelect={(x) => {
if (!x) return
connect(x.id)
}}
>
<button
type="button"
class="flex w-full scroll-my-3.5 flex-row items-center gap-1.5 rounded-md px-3 py-2 text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:var(--v2-font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none"
onClick={() => selectModel(item)}
>
<span class="min-w-0 truncate">{displayModelName(item.name)}</span>
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
<Show when={item.latest}>
<Badge class="shrink-0">{language.t("model.tag.latest")}</Badge>
</Show>
<Show when={currentKey() === modelKey(item)}>
<Icon name="check" class="ml-auto size-4 shrink-0 text-v2-icon-icon-base" />
</Show>
</button>
</Tooltip>
)}
</For>
</div>
<div class="flex w-full flex-col">
<div class="flex w-full flex-col items-start rounded-lg border-[0.5px] border-v2-border-border-muted bg-v2-background-bg-layer-02 p-2.5 pt-2">
<div class="flex h-8 w-full select-none items-center px-0.5 pb-2">
<div class="flex h-5 items-center text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:var(--v2-font-family-sans)] [font-variant-numeric:tabular-nums] [font-variation-settings:'slnt'_0]">
{language.t("dialog.model.unpaid.addMore.title")}
</div>
</div>
<div class="grid w-full grid-cols-1 gap-y-1.5 gap-x-2 sm:grid-cols-2">
<For
each={integrations
.list()
.filter((provider) => featuredProviders.includes(provider.id))
.sort((a, b) => featuredProviders.indexOf(a.id) - featuredProviders.indexOf(b.id))}
>
{(provider) => (
<button
type="button"
data-provider-id={provider.id}
class="flex min-h-11 w-full scroll-my-3.5 flex-row items-start gap-2 rounded-md bg-v2-background-bg-base px-3 py-2.5 text-left text-[13px] font-[530] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:var(--v2-font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-background-bg-layer-01 focus:bg-v2-background-bg-layer-01 focus:outline-none"
classList={{
"border-[0.5px] border-transparent shadow-[var(--v2-elevation-raised)]":
theme.mode() !== "dark",
"border-[0.5px] border-v2-border-border-strong": theme.mode() === "dark",
}}
onClick={() => openProviders(provider.id)}
>
<ProviderIcon id={provider.id} class="mt-0.5 size-4 shrink-0 text-v2-icon-icon-base" />
<span class="flex min-w-0 flex-col">
<span class="truncate">{provider.name}</span>
<Show when={provider.id === "opencode" || provider.id === "opencode-go"}>
<span class="truncate font-[440] text-v2-text-text-muted">
{language.t(
provider.id === "opencode"
? "dialog.provider.opencode.tagline"
: "dialog.provider.opencodeGo.tagline",
)}
</span>
</Show>
</span>
</button>
{(i) => (
<div class="w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<div class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencode.tagline")}
</div>
</Show>
<Show when={i.id === "opencode"}>
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
</Show>
<Show when={i.id === "opencode-go"}>
<>
<div class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencodeGo.tagline")}
</div>
<Badge appearance="standard">{language.t("dialog.provider.tag.recommended")}</Badge>
</>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
</div>
)}
</For>
<button
type="button"
class="col-span-full flex h-8 w-full scroll-my-3.5 items-center justify-start rounded-md px-3 text-left text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-family:var(--v2-font-family-sans)] [font-variation-settings:'slnt'_0] hover:bg-v2-overlay-simple-overlay-hover focus:bg-v2-overlay-simple-overlay-hover focus:outline-none"
onClick={() => openProviders()}
</List>
<Button
variant="ghost"
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
icon="dot-grid"
onClick={all}
>
{language.t("dialog.model.unpaid.viewMoreProviders")}
</button>
{language.t("dialog.provider.viewAll")}
</Button>
</div>
</div>
</div>
@@ -113,7 +113,113 @@ const ModelList: Component<{
type ModelSelectorTriggerProps = Omit<ComponentProps<typeof Kobalte.Trigger>, "as" | "ref">
type ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => JSX.Element
type Dismiss = "escape" | "outside" | "select" | "manage" | "provider"
export function ModelSelectorPopover(props: {
provider?: string
model?: ModelState
trigger: ModelSelectorTrigger
onClose?: (cause: "escape" | "select") => void
}) {
const [store, setStore] = createStore<{
open: boolean
dismiss: Dismiss | null
}>({
open: false,
dismiss: null,
})
const dialog = useDialog()
const local = useLocal()
const directory = () => decode64(local.slug())
const close = (dismiss: Dismiss) => {
setStore("dismiss", dismiss)
setStore("open", false)
}
const handleManage = () => {
close("manage")
void import("./dialog-manage-models").then((x) => {
dialog.show(() => <x.DialogManageModels />)
})
}
const handleConnectProvider = () => {
close("provider")
void import("./dialog-connect-provider").then((x) => {
void dialog.show(() => <x.DialogConnectProvider directory={directory()} />)
})
}
const language = useLanguage()
return (
<Kobalte
open={store.open}
onOpenChange={(next) => {
if (next) setStore("dismiss", null)
setStore("open", next)
}}
modal={false}
placement="top-start"
gutter={4}
>
<Kobalte.Trigger as={props.trigger} />
<Kobalte.Portal>
<Kobalte.Content
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
onEscapeKeyDown={(event) => {
close("escape")
event.preventDefault()
event.stopPropagation()
}}
onPointerDownOutside={() => close("outside")}
onFocusOutside={() => close("outside")}
onCloseAutoFocus={(event) => {
const dismiss = store.dismiss
if (dismiss === "outside") event.preventDefault()
if (dismiss === "escape" || dismiss === "select") {
event.preventDefault()
props.onClose?.(dismiss)
}
setStore("dismiss", null)
}}
>
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
<ModelList
provider={props.provider}
model={props.model}
onSelect={() => close("select")}
class="p-1"
action={
<div class="flex items-center gap-1">
<Tooltip appearance="standard" placement="top" value={language.t("command.provider.connect")}>
<IconButton
icon={<Icon name="plus-small" />}
variant="ghost"
class="size-6"
aria-label={language.t("command.provider.connect")}
onClick={handleConnectProvider}
/>
</Tooltip>
<Tooltip appearance="standard" placement="top" value={language.t("dialog.model.manage")}>
<IconButton
icon={<Icon name="sliders" />}
variant="ghost"
class="size-6"
aria-label={language.t("dialog.model.manage")}
onClick={handleManage}
/>
</Tooltip>
</div>
}
/>
</Kobalte.Content>
</Kobalte.Portal>
</Kobalte>
)
}
export function ModelSelectorPopoverV2(props: {
provider?: string
model?: ModelState
trigger: ModelSelectorTrigger
@@ -127,7 +233,7 @@ export function ModelSelectorPopover(props: {
})
return (
<ModelSelectorPopoverView
<ModelSelectorPopoverV2View
trigger={props.trigger}
models={controller.models}
groups={controller.groups}
@@ -135,7 +241,7 @@ export function ModelSelectorPopover(props: {
select={controller.select}
onManage={() => {
void import("./dialog-manage-models").then((module) => {
void dialog.show(() => <module.DialogManageModels />)
void dialog.show(() => <module.DialogManageModelsV2 />)
})
}}
onClose={() => props.onClose?.()}
@@ -182,7 +288,7 @@ function createModelSelectorController(input: {
}
}
function ModelSelectorPopoverView(props: {
function ModelSelectorPopoverV2View(props: {
trigger: ModelSelectorTrigger
models: (search: string) => ModelItem[]
groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[]
@@ -1,33 +1,86 @@
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import type { ReferenceInfo } from "@opencode-ai/client/promise"
import { createComponent, createEffect, createMemo, on } from "solid-js"
import type { ComposerSuggestion } from "./types"
import { createComposerEditor, createComposerEditorState, type ComposerEditorModel } from "./editor/interaction"
import { createEffect, createMemo, on, Show } from "solid-js"
import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2"
import type { PromptInputProps } from "@/components/prompt-input/contracts"
import { normalizePromptHistoryEntry, promptLength, type PromptHistoryComment } from "@/components/prompt-input/history"
import { createPersistedPromptInputHistory } from "@/components/prompt-input/history-store"
import { promptDesignPlaceholder, promptPlaceholder } from "@/components/prompt-input/placeholder"
import { createPromptSubmit } from "@/components/prompt-input/submit"
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
import { useComments } from "@/context/comments"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { usePermission } from "@/context/permission"
import { type ImageAttachmentPart, usePrompt } from "@/context/prompt"
import { usePlatform } from "@/context/platform"
import { useWorkspaceLocation } from "@/context/location"
import { useData } from "@/context/server"
import { createSessionTabs } from "@/session/helpers"
import { showToast } from "@/utils/toast"
import { formatServerError } from "@/utils/server-errors"
import { Skill } from "@opencode-ai/schema/skill"
import type { ComposerAdapter, ComposerControls } from "./adapter"
import type { ImageAttachmentPart } from "./state"
import { normalizePromptHistoryEntry, type PromptHistoryComment } from "./history/entry"
import { createComposerHistory } from "./history/store"
import { composerPlaceholder } from "./placeholder"
import { createComposerSubmit } from "./submit"
import { PromptInputV2, type PromptInputV2Suggestion } from "@opencode-ai/session-ui/v2/prompt-input"
import {
createPromptInputV2Controller,
createPromptInputV2State,
type PromptInputV2Interaction,
} from "@opencode-ai/session-ui/v2/prompt-input/interaction"
export type ComposerModel = ComposerEditorModel & {
readonly model: ComposerControls["model"]
export type PromptInputV2ComposerProps = {
class?: string
controller: PromptInputV2ComposerController
borderUnderlay?: boolean
accentSubmit?: boolean
}
export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
export type PromptInputV2ControllerProps = Omit<PromptInputProps, "class">
export type PromptInputV2ComposerController = PromptInputV2Interaction & {
readonly model: PromptInputProps["controls"]["model"]
}
export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
const dialog = useDialog()
const command = useCommand()
const language = useLanguage()
return (
<div class="flex flex-col gap-3">
<PromptInputV2
controller={props.controller}
accentSubmit={props.accentSubmit}
borderUnderlay={props.borderUnderlay}
class={props.class}
variantControlVisible={!props.controller.model.loading}
attachKeybind={command.keybindParts("file.attach")}
attachShortcut={command.keybind("file.attach")}
modelControl={
<PromptInputV2ModelControl
loading={props.controller.model.loading}
paid={props.controller.model.paid}
title={language.t("command.model.choose")}
keybind={command.keybindParts("model.choose")}
model={props.controller.model.selection}
providerID={props.controller.model.selection.current()?.provider?.id}
modelName={props.controller.model.selection.current()?.name ?? language.t("dialog.model.select.title")}
onClose={props.controller.restoreFocus}
onUnpaidClick={() =>
dialog.show(() => <DialogSelectModelUnpaidV2 model={props.controller.model.selection} />)
}
/>
}
/>
</div>
)
}
export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): PromptInputV2ComposerController {
const sdk = useWorkspaceLocation()
const data = useData()
const files = useFile()
@@ -35,20 +88,16 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
const comments = useComments()
const dialog = useDialog()
const command = useCommand()
const permission = usePermission()
const language = useLanguage()
const platform = usePlatform()
const prompt = adapter.state
const prompt = props.state ?? usePrompt()
let editor: HTMLDivElement | undefined
const interaction = createComposerEditorState(prompt.mode.current())
createEffect(
on(adapter.ready, (ready) => {
if (ready) interaction[1]("mode", prompt.mode.current())
}),
)
const interaction = createPromptInputV2State()
const mode = () => interaction[0].mode
const history = createComposerHistory()
const tabs = () => adapter.controls().session.tabs
const history = props.history ?? createPersistedPromptInputHistory()
const tabs = () => props.controls.session.tabs
const activeFileTab = createSessionTabs({
tabs,
pathFromTab: files.pathFromTab,
@@ -64,6 +113,8 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
return [...result, path]
}, [])
})
const info = createMemo(() => (props.controls.session.id ? data.session.get(props.controls.session.id) : undefined))
const working = createMemo(() => data.session.status(props.controls.session.id ?? "") === "running")
const attachments = createMemo(() =>
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
)
@@ -78,9 +129,20 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
.join("")
return text.trim().length === 0 && attachments().length === 0 && commentCount() === 0
})
const stopping = createMemo(() => adapter.working() && blank())
const placeholder = () =>
composerPlaceholder(mode(), (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never))
const stopping = createMemo(() => working() && blank())
const placeholder = createMemo(() =>
promptPlaceholder({
mode: mode(),
commentCount: commentCount(),
example: mode() === "shell" ? "git status" : "",
suggest: false,
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
}),
)
const designPlaceholder = () =>
promptDesignPlaceholder(mode(), placeholder(), (key, params) =>
language.t(key as Parameters<typeof language.t>[0], params as never),
)
const historyComments = () => {
const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
@@ -130,6 +192,39 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
)
}
const accepting = createMemo(() => {
const id = props.controls.session.id
if (!id) return permission.isAutoAcceptingDirectory(sdk().directory)
return permission.isAutoAccepting(id, sdk().directory)
})
const submission =
props.submission ??
createPromptSubmit({
prompt,
info,
imageAttachments: attachments,
commentCount,
autoAccept: accepting,
mode,
working,
editor: () => editor,
queueScroll: () => requestAnimationFrame(() => editor?.scrollIntoView({ block: "nearest" })),
promptLength,
addToHistory: (value, mode) => controller.addHistory(value, mode),
resetHistoryNavigation: () => controller.resetHistory(),
setMode: (next) => controller.dispatch({ type: next === "shell" ? "mode.shell" : "mode.normal" }),
setPopover: (popover) => {
if (!popover) controller.dispatch({ type: "popover.close" })
},
newSessionWorktree: () => props.newSessionWorktree,
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
shouldQueue: props.shouldQueue,
onQueue: props.onQueue,
onAbort: props.onAbort,
onSubmit: props.onSubmit,
model: props.controls.model.selection,
})
const referenceDescription = (reference: ReferenceInfo) =>
reference.source.type === "git" ? reference.source.repository : reference.source.path
const references = createMemo(() =>
@@ -178,26 +273,10 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
resource,
})),
)
const skills = createMemo(() => data.location.skill.list({ directory: sdk().directory }) ?? [])
const context = createMemo<ComposerSuggestion[]>(() => [
const context = createMemo<PromptInputV2Suggestion[]>(() => [
...references(),
...skills().map((skill) => ({
id: `skill:${skill.id}`,
kind: "skill" as const,
label: `@${skill.id}`,
description: skill.description,
mention: {
type: "skill" as const,
id: Skill.ID.make(skill.id),
name: Skill.Name.make(skill.name),
content: `@${skill.id}`,
start: 0,
end: 0,
},
})),
...adapter
.controls()
.agents.available.filter((agent) => !agent.hidden && agent.mode !== "primary")
...props.controls.agents.available
.filter((agent) => !agent.hidden && agent.mode !== "primary")
.map((agent) => ({
id: `agent:${agent.name}`,
kind: "agent" as const,
@@ -232,7 +311,7 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
type: "builtin" as const,
})),
])
const commands = createMemo<ComposerSuggestion[]>(() =>
const commands = createMemo<PromptInputV2Suggestion[]>(() =>
slashCommands().map((item) => ({
id: item.id,
kind: "command",
@@ -243,46 +322,11 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
keybind: command.keybindParts(item.id),
})),
)
const variants = createMemo(() => ["default", ...adapter.controls().model.selection.variant.list()])
const submission = createComposerSubmit({
adapter,
mode,
editor: () => editor,
queueScroll: () => requestAnimationFrame(() => editor?.scrollIntoView({ block: "nearest" })),
addToHistory: (value, mode) => controller.addHistory(value, mode),
resetHistory: () => controller.resetHistory(),
setMode: (next) => controller.dispatch({ type: next === "shell" ? "mode.shell" : "mode.normal" }),
closePopover: () => controller.dispatch({ type: "popover.close" }),
notify: {
missingSelection: () =>
showToast({
title: language.t("prompt.toast.modelAgentRequired.title"),
description: language.t("prompt.toast.modelAgentRequired.description"),
}),
failed: (kind, error) =>
showToast({
title: language.t(
kind === "shell"
? "prompt.toast.shellSendFailed.title"
: kind === "command"
? "prompt.toast.commandSendFailed.title"
: "prompt.toast.promptSendFailed.title",
),
description:
kind === "command"
? formatServerError(error, language.t, language.t("common.requestFailed"))
: composerErrorMessage(language, error),
}),
},
comments: {
capture: historyComments,
clear: comments.clear,
restore: restoreHistoryComments,
},
})
const controller = createComposerEditor({
store: prompt.store,
const variants = createMemo(() => ["default", ...props.controls.model.selection.variant.list()])
const controller = createPromptInputV2Controller({
store: () => prompt.capture().store,
state: interaction,
identity: () => prompt.capture(),
history: {
entries: (mode) =>
history.entries(mode).map((value) => {
@@ -307,14 +351,14 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
if (item?.commentID) comments.remove(item.path, item.commentID)
},
openAttachment: (attachment) =>
dialog.show(() => createComponent(ImagePreview, { src: attachment.blob.url, alt: attachment.filename })),
dialog.show(() => <ImagePreview src={attachment.blob.url} alt={attachment.filename} />),
openContext(key) {
const item = controller.contextItem(key)
if (item) openComment(item, adapter.controls(), layout, files, comments)
if (item) openComment(item, props, layout, files, comments)
},
onEditor(element) {
editor = element as HTMLDivElement
if (adapter.kind === "active-session") adapter.setEditor(editor)
props.ref?.(editor)
},
onSuggestionSelect(item) {
if (item.kind !== "command") return
@@ -343,35 +387,34 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
store: platform.draftStore?.putBlob,
},
view: {
placeholder,
placeholder: designPlaceholder,
get agent() {
const agents = adapter.controls().agents
return agents.visible && agents.options.length > 0
return props.controls.agents.visible && props.controls.agents.options.length > 0
? {
options: () => adapter.controls().agents.options.map((name) => ({ id: name, label: name })),
current: () => adapter.controls().agents.current,
onSelect: (value: string) => adapter.controls().agents.select(value),
options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })),
current: () => props.controls.agents.current,
onSelect: (value: string) => props.controls.agents.select(value),
keybind: () => command.keybindParts("agent.cycle"),
}
: undefined
},
variant: {
options: () => variants().map((value) => ({ id: value, label: value })),
current: () => adapter.controls().model.selection.variant.current() ?? "default",
onSelect: (value) => adapter.controls().model.selection.variant.set(value === "default" ? undefined : value),
current: () => props.controls.model.selection.variant.current() ?? "default",
onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value),
keybind: () => command.keybindParts("model.variant.cycle"),
},
submit: {
stopping,
working: adapter.working,
onSubmit: () => void submission.submit(new Event("submit")),
onStop: () => void submission.stop(),
working,
onSubmit: () => void submission.handleSubmit(new Event("submit")),
onStop: () => void submission.abort(),
},
},
})
Object.defineProperty(controller, "model", { get: () => adapter.controls().model })
Object.defineProperty(controller, "model", { get: () => props.controls.model })
command.register("composer-editor", () => [
command.register("prompt-input", () => [
{
id: "file.attach",
title: language.t("prompt.action.attachFile"),
@@ -398,23 +441,122 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
},
])
return controller as ComposerModel
createEffect(
on(
() => props.edit?.id,
(id) => {
const edit = props.edit
if (!id || !edit) return
prompt.context.items().forEach((item) => prompt.context.remove(item.key))
edit.context.forEach((item) =>
prompt.context.add({
type: item.type,
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
commentOrigin: item.commentOrigin,
preview: item.preview,
}),
)
controller.dispatch({ type: "mode.normal" })
controller.resetHistory()
prompt.set(edit.prompt, promptLength(edit.prompt))
controller.restoreFocus()
props.onEditLoaded?.()
},
{ defer: true },
),
)
return controller as PromptInputV2ComposerController
}
function composerErrorMessage(language: ReturnType<typeof useLanguage>, error: unknown) {
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
return error.message
}
if (error && typeof error === "object" && "data" in error) {
const data = (error as { data?: { message?: string } }).data
if (data?.message) return data.message
}
return language.t("common.requestFailed")
function PromptInputV2ModelControl(props: {
loading: boolean
paid: boolean
title: string
keybind: string[]
model: PromptInputV2ComposerController["model"]["selection"]
providerID?: string
modelName: string
onClose: () => void
onUnpaidClick: () => void
}) {
const shouldAnimate = createMemo<boolean>((previous) => previous ?? props.loading)
const content = () => (
<>
<Show when={props.providerID}>
{(providerID) => (
<ProviderIcon
id={providerID()}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
)}
</Show>
<span class="truncate leading-4">{props.modelName}</span>
<span class="-ml-0.5 -mr-1 flex shrink-0">
<Icon name="chevron-down" />
</span>
</>
)
return (
<Show when={!props.loading}>
<Tooltip
placement="top"
gutter={4}
value={
<>
{props.title}
<Keybind keys={props.keybind} variant="neutral" />
</>
}
>
<Show
when={props.paid}
fallback={
<Button
data-action="prompt-model"
data-control-type="dialog"
variant="ghost-muted"
size="normal"
class="min-w-0 max-w-[220px] justify-start ![font-weight:440] group"
classList={{ "animate-in fade-in": shouldAnimate() }}
style={{ height: "28px" }}
onClick={props.onUnpaidClick}
>
{content()}
</Button>
}
>
<ModelSelectorPopoverV2
model={props.model}
trigger={(triggerProps) => (
<Button
{...triggerProps}
variant="ghost-muted"
size="normal"
style={{ height: "28px" }}
class="min-w-0 max-w-[220px] justify-start ![font-weight:440] group"
classList={{ "animate-in fade-in": shouldAnimate() }}
data-action="prompt-model"
data-control-type="popover"
>
{content()}
</Button>
)}
onClose={props.onClose}
/>
</Show>
</Tooltip>
</Show>
)
}
function openComment(
item: { path: string; commentID?: string; commentOrigin?: "review" | "file" },
controls: ComposerControls,
props: PromptInputV2ControllerProps,
layout: ReturnType<typeof useLayout>,
files: ReturnType<typeof useFile>,
comments: ReturnType<typeof useComments>,
@@ -433,16 +575,16 @@ function openComment(
})
}
const review = item.commentOrigin === "review"
if (!controls.session.reviewPanel.opened()) controls.session.reviewPanel.open()
if (!props.controls.session.reviewPanel.opened()) props.controls.session.reviewPanel.open()
if (review) {
layout.fileTree.setTab("changes")
controls.session.tabs.setActive("review")
props.controls.session.tabs.setActive("review")
queueFocus()
return
}
layout.fileTree.setTab("all")
const tab = files.tab(item.path)
void controls.session.tabs.open(tab)
controls.session.tabs.setActive(tab)
void props.controls.session.tabs.open(tab)
props.controls.session.tabs.setActive(tab)
void Promise.resolve(files.load(item.path)).finally(() => queueFocus())
}
@@ -0,0 +1,17 @@
import type { Component } from "solid-js"
import { PromptInputV2Composer, usePromptInputV2Controller } from "./prompt-input-v2"
import { createPromptInputHistory, type PromptInputHistory } from "./prompt-input/history-store"
import type {
PromptInputControls,
PromptInputProps,
PromptInputState,
PromptInputSubmission,
} from "./prompt-input/contracts"
export { createPromptInputHistory }
export type { PromptInputControls, PromptInputHistory, PromptInputProps, PromptInputState, PromptInputSubmission }
export const PromptInput: Component<PromptInputProps> = (props) => {
const controller = usePromptInputV2Controller(props)
return <PromptInputV2Composer class={props.class} controller={controller} />
}
@@ -0,0 +1,108 @@
import { describe, expect, test } from "bun:test"
import { attachmentMime, pickAttachmentFiles } from "./files"
import { pasteMode } from "./paste"
describe("attachmentMime", () => {
test("keeps PDFs when the browser reports the mime", async () => {
const file = new File(["%PDF-1.7"], "guide.pdf", { type: "application/pdf" })
expect(await attachmentMime(file)).toBe("application/pdf")
})
test("normalizes structured text types to text/plain", async () => {
const file = new File(['{"ok":true}\n'], "data.json", { type: "application/json" })
expect(await attachmentMime(file)).toBe("text/plain")
})
test("accepts text files even with a misleading browser mime", async () => {
const file = new File(["export const x = 1\n"], "main.ts", { type: "video/mp2t" })
expect(await attachmentMime(file)).toBe("text/plain")
})
test("rejects binary files", async () => {
const file = new File([Uint8Array.of(0, 255, 1, 2)], "blob.bin", { type: "application/octet-stream" })
expect(await attachmentMime(file)).toBeUndefined()
})
})
describe("pickAttachmentFiles", () => {
test("reads the current project directory for every native picker invocation", async () => {
const paths: string[] = []
const files: File[] = []
const file = new File(["hello"], "hello.txt", { type: "text/plain" })
let directory = "C:\\Projects\\LoremIpsum"
const picker = async (options?: { defaultPath?: string }, onFile?: (file: File) => Promise<unknown>) => {
paths.push(options?.defaultPath ?? "")
await onFile?.(file)
}
pickAttachmentFiles({
picker,
directory: () => directory,
fallback: () => undefined,
onFile: async (selected) => files.push(selected),
onError: () => undefined,
})
await Promise.resolve()
directory = "C:\\Projects\\DolorSit"
pickAttachmentFiles({
picker,
directory: () => directory,
fallback: () => undefined,
onFile: async (selected) => files.push(selected),
onError: () => undefined,
})
await Promise.resolve()
expect(files).toEqual([file, file])
expect(paths).toEqual(["C:\\Projects\\LoremIpsum", "C:\\Projects\\DolorSit"])
})
test("uses the browser file input when no native picker exists", async () => {
let fallback = 0
pickAttachmentFiles({
directory: () => "/projects/consectetur-adipiscing",
fallback: () => {
fallback += 1
},
onFile: async () => undefined,
onError: () => undefined,
})
expect(fallback).toBe(1)
})
test("reports native picker failures without rejecting", async () => {
const error = new Error("picker unavailable")
const errors: unknown[] = []
const handled = Promise.withResolvers<void>()
pickAttachmentFiles({
picker: async () => Promise.reject(error),
directory: () => "C:\\Projects\\LoremIpsum",
fallback: () => undefined,
onFile: async () => undefined,
onError: (cause) => {
errors.push(cause)
handled.resolve()
},
})
await handled.promise
expect(errors).toEqual([error])
})
})
describe("pasteMode", () => {
test("uses native paste for short single-line text", () => {
expect(pasteMode("hello world")).toBe("native")
})
test("uses manual paste for multiline text", () => {
expect(
pasteMode(`{
"ok": true
}`),
).toBe("manual")
expect(pasteMode("a\r\nb")).toBe("manual")
})
test("uses manual paste for large text", () => {
expect(pasteMode("x".repeat(8000))).toBe("manual")
})
})
@@ -0,0 +1,213 @@
import { onMount } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { showToast } from "@/utils/toast"
import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { uuid } from "@/utils/uuid"
import { getCursorPosition } from "./editor-dom"
import { createBlobReference, type DraftStore } from "@/utils/draft-store"
import { attachmentMime } from "./files"
import { normalizePaste, pasteMode } from "./paste"
type PromptTarget = Pick<ReturnType<ReturnType<typeof usePrompt>["capture"]>, "current" | "cursor" | "set">
type AttachmentTarget = { prompt: PromptTarget; cursor: number | undefined }
type PromptAttachmentsCoreInput = {
capture: () => PromptTarget
editor: () => HTMLDivElement | undefined
focusEditor?: () => void
addPart?: (part: ContentPart) => boolean
warn?: () => void
readClipboardImage?: () => Promise<File | null>
getPathForFile?: (file: File) => string
draftStore?: DraftStore
}
export type PromptAttachmentsInput = {
prompt: ReturnType<typeof usePrompt>
editor: () => HTMLDivElement | undefined
isDialogActive: () => boolean
setDraggingType: (type: "image" | "@mention" | null) => void
focusEditor: () => void
addPart: (part: ContentPart) => boolean
readClipboardImage?: () => Promise<File | null>
getPathForFile?: (file: File) => string
}
export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) {
const capture = (): AttachmentTarget | undefined => {
const prompt = input.capture()
const editor = input.editor()
if (!editor) return
return { prompt, cursor: prompt.cursor() ?? getCursorPosition(editor) }
}
const add = async (file: File, toast = true, target = capture()) => {
if (!target) return false
const mime = await attachmentMime(file)
if (!mime) {
if (toast) input.warn?.()
return false
}
const attachment: ImageAttachmentPart = {
type: "image",
id: uuid(),
filename: file.name,
sourcePath: input.getPathForFile?.(file) || undefined,
mime,
blob: input.draftStore ? await input.draftStore.putBlob(file) : await createBlobReference(file),
}
target.prompt.set([...target.prompt.current(), attachment], target.cursor)
return true
}
const addAttachment = (file: File) => add(file)
const addAttachments = async (files: File[], toast = true, target = capture()) => {
let found = false
for (const file of files) {
const ok = await add(file, false, target)
if (ok) found = true
}
if (!found && files.length > 0 && toast) input.warn?.()
return found
}
const addClipboardAttachment = async (pending: Promise<File | null>, target = capture()) => {
const file = await pending
if (!file) return false
return add(file, true, target)
}
const removeAttachment = (id: string) => {
const target = input.capture()
const current = target.current()
const next = current.filter((part) => part.type !== "image" || part.id !== id)
target.set(next, target.cursor())
}
const handlePaste = async (event: ClipboardEvent) => {
const clipboardData = event.clipboardData
if (!clipboardData) return
const target = capture()
if (!target) return
event.preventDefault()
event.stopPropagation()
const files = Array.from(clipboardData.items).flatMap((item) => {
if (item.kind !== "file") return []
const file = item.getAsFile()
return file ? [file] : []
})
if (files.length > 0) {
await addAttachments(files, true, target)
return
}
const plainText = clipboardData.getData("text/plain") ?? ""
// Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images
if (input.readClipboardImage && !plainText) {
if (await addClipboardAttachment(input.readClipboardImage(), target)) return
}
if (!plainText) return
const text = normalizePaste(plainText)
const put = () => {
if (input.addPart?.({ type: "text", content: text, start: 0, end: 0 })) return true
input.focusEditor?.()
return input.addPart?.({ type: "text", content: text, start: 0, end: 0 }) ?? false
}
if (pasteMode(text) === "manual") {
put()
return
}
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text)
if (inserted) return
put()
}
return {
addAttachment,
addAttachments,
addClipboardAttachment,
removeAttachment,
handlePaste,
}
}
export function createPromptAttachments(input: PromptAttachmentsInput) {
const language = useLanguage()
const platform = usePlatform()
const attachments = createPromptAttachmentsCore({
...input,
draftStore: platform.draftStore,
capture: input.prompt.capture,
warn: () => {
showToast({
title: language.t("prompt.toast.pasteUnsupported.title"),
description: language.t("prompt.toast.pasteUnsupported.description"),
})
},
})
const handleGlobalDragOver = (event: DragEvent) => {
if (input.isDialogActive()) return
event.preventDefault()
const hasFiles = event.dataTransfer?.types.includes("Files")
const hasText = event.dataTransfer?.types.includes("text/plain")
if (hasFiles) {
input.setDraggingType("image")
} else if (hasText) {
input.setDraggingType("@mention")
}
}
const handleGlobalDragLeave = (event: DragEvent) => {
if (input.isDialogActive()) return
if (!event.relatedTarget) {
input.setDraggingType(null)
}
}
const handleGlobalDrop = async (event: DragEvent) => {
if (input.isDialogActive()) return
event.preventDefault()
input.setDraggingType(null)
const plainText = event.dataTransfer?.getData("text/plain")
const filePrefix = "file:"
if (plainText?.startsWith(filePrefix)) {
const filePath = plainText.slice(filePrefix.length)
input.focusEditor()
input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 })
return
}
const dropped = event.dataTransfer?.files
if (!dropped) return
await attachments.addAttachments(Array.from(dropped))
}
onMount(() => {
makeEventListener(document, "dragover", handleGlobalDragOver)
makeEventListener(document, "dragleave", handleGlobalDragLeave)
makeEventListener(document, "drop", handleGlobalDrop)
})
return attachments
}
@@ -1,7 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Skill } from "@opencode-ai/schema/skill"
import type { Prompt } from "@/composer/state"
import { buildPromptRequest } from "./request"
import type { Prompt } from "@/context/prompt"
import { buildPromptRequest } from "./build-prompt-request"
describe("buildPromptRequest", () => {
test("builds text, files, and agents from the prompt", () => {
@@ -319,30 +318,4 @@ describe("buildPromptRequest", () => {
// Should preserve .. segments (backend normalizes)
expect(file!.uri).toContain("/..")
})
test("keeps skill mentions out of file attachments", () => {
const skill = {
id: "skill-review",
name: "review",
}
const result = buildPromptRequest({
prompt: [
{
type: "skill",
id: Skill.ID.make(skill.id),
name: Skill.Name.make(skill.name),
content: "@review",
start: 0,
end: 7,
},
],
context: [],
images: [],
text: "@review",
sessionDirectory: "/repo",
})
expect(result.files).toEqual([])
expect(result.skills).toEqual([{ id: skill.id, name: skill.name, mention: { start: 0, end: 7, text: "@review" } }])
})
})
@@ -1,7 +1,7 @@
import { getFilename } from "@opencode-ai/util/path"
import type { FileSelection } from "@/context/file"
import { encodeFilePath } from "@/context/file/path"
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt, SkillPart } from "@/composer/state"
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
import { formatCommentNote, type PromptComment } from "@/utils/comment-note"
// Network fields feed both boundaries; display fields keep desktop-only rendering details in the local echo.
@@ -10,7 +10,6 @@ type PromptRequest = {
displayText: string
files: { uri: string; mime: string; name?: string; mention?: { start: number; end: number; text: string } }[]
agents: { name: string; mention?: { start: number; end: number; text: string } }[]
skills: { id: string; name: string; mention?: { start: number; end: number; text: string } }[]
comments: PromptComment[]
}
@@ -55,14 +54,8 @@ const parseCommentMentions = (comment: string) => {
const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file"
const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent"
const isSkillAttachment = (part: Prompt[number]): part is SkillPart => part.type === "skill"
export function buildPromptRequest(input: BuildPromptRequestInput): PromptRequest {
const skills = input.prompt.filter(isSkillAttachment).map((attachment) => ({
id: attachment.id,
name: attachment.name,
mention: { start: attachment.start, end: attachment.end, text: attachment.content },
}))
const files = input.prompt.filter(isFileAttachment).map((attachment) => {
const path = absolute(input.sessionDirectory, attachment.path)
return {
@@ -117,7 +110,6 @@ export function buildPromptRequest(input: BuildPromptRequestInput): PromptReques
displayText: input.text,
files: [...files, ...context, ...images],
agents,
skills,
comments,
}
}
@@ -0,0 +1,57 @@
import type { useLocal } from "@/context/local"
import type { Prompt, usePrompt } from "@/context/prompt"
import type { PromptInputHistory } from "./history-store"
import type { FollowupDraft } from "./submit"
export type PromptInputState = ReturnType<typeof usePrompt>
export type PromptInputSubmission = {
abort: () => Promise<void> | void
handleSubmit: (event: Event) => Promise<void> | void
}
export type PromptInputControls = {
agents: {
available: { name: string; hidden?: boolean; mode: string }[]
options: string[]
current: string
loading: boolean
visible: boolean
select: (name: string | undefined) => void
}
model: {
selection: ReturnType<typeof useLocal>["model"]
paid: boolean
loading: boolean
}
session: {
id?: string
tabs: {
active: () => string | undefined
all: () => string[]
open: (tab: string) => void | Promise<void>
setActive: (tab: string) => void
}
reviewPanel: {
opened: () => boolean
open: () => void
}
}
}
export interface PromptInputProps {
class?: string
state?: PromptInputState
history?: PromptInputHistory
submission?: PromptInputSubmission
controls: PromptInputControls
ref?: (el: HTMLDivElement) => void
newSessionWorktree?: string
onNewSessionWorktreeReset?: () => void
edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] }
onEditLoaded?: () => void
shouldQueue?: () => boolean
onQueue?: (draft: FollowupDraft) => void
onAbort?: () => void
onSubmit?: () => void
}
@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test"
import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom"
describe("prompt-input editor dom", () => {
test("createTextFragment preserves newlines with consecutive br nodes", () => {
const fragment = createTextFragment("foo\n\nbar")
const container = document.createElement("div")
container.appendChild(fragment)
expect(container.childNodes.length).toBe(4)
expect(container.childNodes[0]?.textContent).toBe("foo")
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR")
expect(container.childNodes[3]?.textContent).toBe("bar")
})
test("createTextFragment keeps trailing newline as terminal break", () => {
const fragment = createTextFragment("foo\n")
const container = document.createElement("div")
container.appendChild(fragment)
expect(container.childNodes.length).toBe(2)
expect(container.childNodes[0]?.textContent).toBe("foo")
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
})
test("createTextFragment avoids break-node explosion for large multiline content", () => {
const content = Array.from({ length: 220 }, () => "line").join("\n")
const fragment = createTextFragment(content)
const container = document.createElement("div")
container.appendChild(fragment)
expect(container.childNodes.length).toBe(1)
expect(container.childNodes[0]?.nodeType).toBe(Node.TEXT_NODE)
expect(container.textContent).toBe(content)
})
test("createTextFragment keeps terminal break in large multiline fallback", () => {
const content = `${Array.from({ length: 220 }, () => "line").join("\n")}\n`
const fragment = createTextFragment(content)
const container = document.createElement("div")
container.appendChild(fragment)
expect(container.childNodes.length).toBe(2)
expect(container.childNodes[0]?.textContent).toBe(content.slice(0, -1))
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
})
test("length helpers treat breaks as one char and ignore zero-width chars", () => {
const container = document.createElement("div")
container.appendChild(document.createTextNode("ab\u200B"))
container.appendChild(document.createElement("br"))
container.appendChild(document.createTextNode("cd"))
expect(getNodeLength(container.childNodes[0]!)).toBe(2)
expect(getNodeLength(container.childNodes[1]!)).toBe(1)
expect(getTextLength(container)).toBe(5)
})
test("setCursorPosition and getCursorPosition round-trip with pills and breaks", () => {
const container = document.createElement("div")
const pill = document.createElement("span")
pill.dataset.type = "file"
pill.textContent = "@file"
container.appendChild(document.createTextNode("ab"))
container.appendChild(pill)
container.appendChild(document.createElement("br"))
container.appendChild(document.createTextNode("cd"))
document.body.appendChild(container)
setCursorPosition(container, 2)
expect(getCursorPosition(container)).toBe(2)
setCursorPosition(container, 7)
expect(getCursorPosition(container)).toBe(7)
setCursorPosition(container, 8)
expect(getCursorPosition(container)).toBe(8)
container.remove()
})
test("setCursorPosition and getCursorPosition round-trip across blank lines", () => {
const container = document.createElement("div")
container.appendChild(document.createTextNode("a"))
container.appendChild(document.createElement("br"))
container.appendChild(document.createElement("br"))
container.appendChild(document.createTextNode("b"))
document.body.appendChild(container)
setCursorPosition(container, 2)
expect(getCursorPosition(container)).toBe(2)
setCursorPosition(container, 3)
expect(getCursorPosition(container)).toBe(3)
container.remove()
})
})
@@ -1,3 +1,32 @@
const MAX_BREAKS = 200
export function createTextFragment(content: string): DocumentFragment {
const fragment = document.createDocumentFragment()
let breaks = 0
for (const char of content) {
if (char !== "\n") continue
breaks += 1
if (breaks > MAX_BREAKS) {
const tail = content.endsWith("\n")
const text = tail ? content.slice(0, -1) : content
if (text) fragment.appendChild(document.createTextNode(text))
if (tail) fragment.appendChild(document.createElement("br"))
return fragment
}
}
const segments = content.split("\n")
segments.forEach((segment, index) => {
if (segment) {
fragment.appendChild(document.createTextNode(segment))
}
if (index < segments.length - 1) {
fragment.appendChild(document.createElement("br"))
}
})
return fragment
}
export function getNodeLength(node: Node): number {
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
return (node.textContent ?? "").replace(/\u200B/g, "").length
@@ -30,7 +59,9 @@ export function setCursorPosition(parent: HTMLElement, position: number) {
while (node) {
const length = getNodeLength(node)
const isText = node.nodeType === Node.TEXT_NODE
const isPill = node.nodeType === Node.ELEMENT_NODE && !!(node as HTMLElement).dataset.mention
const isPill =
node.nodeType === Node.ELEMENT_NODE &&
((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
if (isText && remaining <= length) {
@@ -85,3 +116,33 @@ export function setCursorPosition(parent: HTMLElement, position: number) {
fallbackSelection?.removeAllRanges()
fallbackSelection?.addRange(fallbackRange)
}
export function setRangeEdge(parent: HTMLElement, range: Range, edge: "start" | "end", offset: number) {
let remaining = offset
const nodes = Array.from(parent.childNodes)
for (const node of nodes) {
const length = getNodeLength(node)
const isText = node.nodeType === Node.TEXT_NODE
const isPill =
node.nodeType === Node.ELEMENT_NODE &&
((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
if (isText && remaining <= length) {
if (edge === "start") range.setStart(node, remaining)
if (edge === "end") range.setEnd(node, remaining)
return
}
if ((isPill || isBreak) && remaining <= length) {
if (edge === "start" && remaining === 0) range.setStartBefore(node)
if (edge === "start" && remaining > 0) range.setStartAfter(node)
if (edge === "end" && remaining === 0) range.setEndBefore(node)
if (edge === "end" && remaining > 0) range.setEndAfter(node)
return
}
remaining -= length
}
}
@@ -0,0 +1,98 @@
import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker"
export { ACCEPTED_FILE_TYPES }
type AttachmentPicker = (
options: {
defaultPath?: string
multiple?: boolean
accept?: string[]
},
onFile: (file: File) => Promise<unknown>,
) => Promise<void>
export function pickAttachmentFiles(input: {
picker?: AttachmentPicker
directory: () => string
fallback: () => void
onFile: (file: File) => Promise<unknown>
onError: (error: unknown) => void
}) {
if (!input.picker) {
input.fallback()
return
}
void input
.picker(
{
defaultPath: input.directory(),
multiple: true,
accept: ACCEPTED_FILE_TYPES,
},
input.onFile,
)
.catch(input.onError)
}
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
const IMAGE_EXTS = new Map([
["gif", "image/gif"],
["jpeg", "image/jpeg"],
["jpg", "image/jpeg"],
["png", "image/png"],
["webp", "image/webp"],
])
const TEXT_MIMES = new Set([
"application/json",
"application/ld+json",
"application/toml",
"application/x-toml",
"application/x-yaml",
"application/xml",
"application/yaml",
])
const SAMPLE = 4096
function kind(type: string) {
return type.split(";", 1)[0]?.trim().toLowerCase() ?? ""
}
function ext(name: string) {
const idx = name.lastIndexOf(".")
if (idx === -1) return ""
return name.slice(idx + 1).toLowerCase()
}
function textMime(type: string) {
if (!type) return false
if (type.startsWith("text/")) return true
if (TEXT_MIMES.has(type)) return true
if (type.endsWith("+json")) return true
return type.endsWith("+xml")
}
function textBytes(bytes: Uint8Array) {
if (bytes.length === 0) return true
let count = 0
for (const byte of bytes) {
if (byte === 0) return false
if (byte < 9 || (byte > 13 && byte < 32)) count += 1
}
return count / bytes.length <= 0.3
}
export async function attachmentMime(file: File) {
const type = kind(file.type)
if (IMAGE_MIMES.has(type)) return type
if (type === "application/pdf") return type
const suffix = ext(file.name)
const fallback = IMAGE_EXTS.get(suffix) ?? (suffix === "pdf" ? "application/pdf" : undefined)
if ((!type || type === "application/octet-stream") && fallback) return fallback
if (textMime(type)) return "text/plain"
const bytes = new Uint8Array(await file.slice(0, SAMPLE).arrayBuffer())
if (!textBytes(bytes)) return
return "text/plain"
}
@@ -1,5 +1,5 @@
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import type { Prompt } from "@/composer/state"
import type { Prompt } from "@/context/prompt"
import { Persist, persisted } from "@/utils/persist"
import {
clonePromptHistoryComments,
@@ -7,9 +7,9 @@ import {
prependHistoryEntry,
type PromptHistoryComment,
type PromptHistoryStoredEntry,
} from "./entry"
} from "./history"
export type ComposerHistoryStore = {
export type PromptInputHistory = {
entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[]
add: (prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) => void
}
@@ -31,12 +31,12 @@ export function upgradeHistoryState(value: unknown) {
}
}
function createComposerHistoryStore(
function createPromptInputHistoryStore(
normal: Store<PromptHistoryState>,
setNormal: SetStoreFunction<PromptHistoryState>,
shell: Store<PromptHistoryState>,
setShell: SetStoreFunction<PromptHistoryState>,
): ComposerHistoryStore {
): PromptInputHistory {
return {
entries: (mode) => (mode === "shell" ? shell.entries : normal.entries),
add(prompt, mode, comments) {
@@ -49,7 +49,13 @@ function createComposerHistoryStore(
}
}
export function createComposerHistory() {
export function createPromptInputHistory(): PromptInputHistory {
const [normal, setNormal] = createStore<PromptHistoryState>({ entries: [] })
const [shell, setShell] = createStore<PromptHistoryState>({ entries: [] })
return createPromptInputHistoryStore(normal, setNormal, shell, setShell)
}
export function createPersistedPromptInputHistory() {
const [normal, setNormal, normalInit] = persisted(
{ ...Persist.prompt(Persist.global("prompt-history")), migrate: upgradeHistoryState },
createStore<PromptHistoryState>({ entries: [] }),
@@ -58,7 +64,7 @@ export function createComposerHistory() {
{ ...Persist.prompt(Persist.global("prompt-history-shell")), migrate: upgradeHistoryState },
createStore<PromptHistoryState>({ entries: [] }),
)
const history = createComposerHistoryStore(normal, setNormal, shell, setShell)
const history = createPromptInputHistoryStore(normal, setNormal, shell, setShell)
return {
...history,
add(prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) {
@@ -0,0 +1,154 @@
import { describe, expect, test } from "bun:test"
import type { Prompt } from "@/context/prompt"
import {
canNavigateHistoryAtCursor,
clonePromptParts,
navigatePromptHistory,
prependHistoryEntry,
promptLength,
type PromptHistoryComment,
} from "./history"
import { upgradeHistoryState } from "./history-store"
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }]
const entry = (value: string) => ({ prompt: text(value), comments: [] })
const comment = (id: string, value = "note"): PromptHistoryComment => ({
id,
path: "src/a.ts",
selection: { start: 2, end: 4 },
comment: value,
time: 1,
origin: "review",
preview: "const a = 1",
})
describe("prompt-input history", () => {
test("prependHistoryEntry skips empty prompt and deduplicates consecutive entries", () => {
const first = prependHistoryEntry([], DEFAULT_PROMPT)
expect(first).toEqual([])
const commentsOnly = prependHistoryEntry([], DEFAULT_PROMPT, [comment("c1")])
expect(commentsOnly).toHaveLength(1)
const withOne = prependHistoryEntry([], text("hello"))
expect(withOne).toHaveLength(1)
const deduped = prependHistoryEntry(withOne, text("hello"))
expect(deduped).toBe(withOne)
const dedupedComments = prependHistoryEntry(commentsOnly, DEFAULT_PROMPT, [comment("c1")])
expect(dedupedComments).toBe(commentsOnly)
})
test("navigatePromptHistory restores saved prompt when moving down from newest", () => {
const entries = [entry("third"), entry("second"), entry("first")]
const up = navigatePromptHistory({
direction: "up",
entries,
historyIndex: -1,
currentPrompt: text("draft"),
currentComments: [comment("draft")],
savedPrompt: null,
})
expect(up.handled).toBe(true)
if (!up.handled) throw new Error("expected handled")
expect(up.historyIndex).toBe(0)
expect(up.cursor).toBe("start")
expect(up.entry.comments).toEqual([])
const down = navigatePromptHistory({
direction: "down",
entries,
historyIndex: up.historyIndex,
currentPrompt: text("ignored"),
currentComments: [],
savedPrompt: up.savedPrompt,
})
expect(down.handled).toBe(true)
if (!down.handled) throw new Error("expected handled")
expect(down.historyIndex).toBe(-1)
expect(down.entry.prompt[0]?.type === "text" ? down.entry.prompt[0].content : "").toBe("draft")
expect(down.entry.comments).toEqual([comment("draft")])
})
test("navigatePromptHistory keeps entry comments when moving through history", () => {
const entries = [
{
prompt: text("with comment"),
comments: [comment("c1")],
},
]
const up = navigatePromptHistory({
direction: "up",
entries,
historyIndex: -1,
currentPrompt: text("draft"),
currentComments: [],
savedPrompt: null,
})
expect(up.handled).toBe(true)
if (!up.handled) throw new Error("expected handled")
expect(up.entry.prompt[0]?.type === "text" ? up.entry.prompt[0].content : "").toBe("with comment")
expect(up.entry.comments).toEqual([comment("c1")])
})
test("upgrades stored prompt arrays once at the persistence boundary", () => {
expect(upgradeHistoryState({ entries: [text("stored")] })).toEqual({
entries: [{ prompt: text("stored"), comments: [] }],
})
})
test("helpers clone prompt and count text content length", () => {
const original: Prompt = [
{ type: "text", content: "one", start: 0, end: 3 },
{
type: "file",
path: "src/a.ts",
content: "@src/a.ts",
start: 3,
end: 12,
selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 },
},
{ type: "image", id: "1", filename: "img.png", mime: "image/png", blob: { id: "blob", url: "blob:test" } },
]
const copy = clonePromptParts(original)
expect(copy).not.toBe(original)
expect(promptLength(copy)).toBe(12)
if (copy[1]?.type !== "file") throw new Error("expected file")
copy[1].selection!.startLine = 9
if (original[1]?.type !== "file") throw new Error("expected file")
expect(original[1].selection?.startLine).toBe(1)
})
test("canNavigateHistoryAtCursor only allows prompt boundaries", () => {
const value = "a\nb\nc"
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false)
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false)
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false)
expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true)
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false)
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false)
expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true)
expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true)
expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true)
expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true)
expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true)
expect(canNavigateHistoryAtCursor("down", "abc", 3, true)).toBe(true)
expect(canNavigateHistoryAtCursor("up", "abc", 1, true)).toBe(false)
expect(canNavigateHistoryAtCursor("down", "abc", 1, true)).toBe(false)
})
})
@@ -1,6 +1,8 @@
import type { Prompt } from "@/composer/state"
import type { Prompt } from "@/context/prompt"
import type { SelectedLineRange } from "@/context/file"
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
export const MAX_HISTORY = 100
export type PromptHistoryComment = {
@@ -20,12 +22,20 @@ export type PromptHistoryEntry = {
export type PromptHistoryStoredEntry = PromptHistoryEntry
export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) {
const position = Math.max(0, Math.min(cursor, text.length))
const atStart = position === 0
const atEnd = position === text.length
if (inHistory) return atStart || atEnd
if (direction === "up") return position === 0 && text.length === 0
return position === text.length
}
export function clonePromptParts(prompt: Prompt): Prompt {
return prompt.map((part) => {
if (part.type === "text") return { ...part }
if (part.type === "image") return { ...part }
if (part.type === "agent") return { ...part }
if (part.type === "skill") return { ...part }
return {
...part,
selection: part.selection ? { ...part.selection } : undefined,
@@ -120,9 +130,6 @@ function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistory
if (!sameSelection) return false
}
if (partA.type === "agent" && partA.name !== (partB.type === "agent" ? partB.name : "")) return false
if (partA.type === "skill") {
if (partB.type !== "skill" || partA.id !== partB.id || partA.name !== partB.name) return false
}
if (partA.type === "image" && partA.id !== (partB.type === "image" ? partB.id : "")) return false
}
if (entryA.comments.length !== entryB.comments.length) return false
@@ -133,3 +140,111 @@ function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistory
}
return true
}
type HistoryNavInput = {
direction: "up" | "down"
entries: PromptHistoryStoredEntry[]
historyIndex: number
currentPrompt: Prompt
currentComments: PromptHistoryComment[]
savedPrompt: PromptHistoryEntry | null
}
type HistoryNavResult =
| {
handled: false
historyIndex: number
savedPrompt: PromptHistoryEntry | null
}
| {
handled: true
historyIndex: number
savedPrompt: PromptHistoryEntry | null
entry: PromptHistoryEntry
cursor: "start" | "end"
}
export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult {
if (input.direction === "up") {
if (input.entries.length === 0) {
return {
handled: false,
historyIndex: input.historyIndex,
savedPrompt: input.savedPrompt,
}
}
if (input.historyIndex === -1) {
const entry = normalizePromptHistoryEntry(input.entries[0])
return {
handled: true,
historyIndex: 0,
savedPrompt: {
prompt: clonePromptParts(input.currentPrompt),
comments: clonePromptHistoryComments(input.currentComments),
},
entry,
cursor: "start",
}
}
if (input.historyIndex < input.entries.length - 1) {
const next = input.historyIndex + 1
const entry = normalizePromptHistoryEntry(input.entries[next])
return {
handled: true,
historyIndex: next,
savedPrompt: input.savedPrompt,
entry,
cursor: "start",
}
}
return {
handled: false,
historyIndex: input.historyIndex,
savedPrompt: input.savedPrompt,
}
}
if (input.historyIndex > 0) {
const next = input.historyIndex - 1
const entry = normalizePromptHistoryEntry(input.entries[next])
return {
handled: true,
historyIndex: next,
savedPrompt: input.savedPrompt,
entry,
cursor: "end",
}
}
if (input.historyIndex === 0) {
if (input.savedPrompt) {
return {
handled: true,
historyIndex: -1,
savedPrompt: null,
entry: input.savedPrompt,
cursor: "end",
}
}
return {
handled: true,
historyIndex: -1,
savedPrompt: null,
entry: {
prompt: DEFAULT_PROMPT,
comments: [],
},
cursor: "end",
}
}
return {
handled: false,
historyIndex: input.historyIndex,
savedPrompt: input.savedPrompt,
}
}
@@ -0,0 +1,24 @@
const LARGE_PASTE_CHARS = 8000
const LARGE_PASTE_BREAKS = 120
function largePaste(text: string) {
if (text.length >= LARGE_PASTE_CHARS) return true
let breaks = 0
for (const char of text) {
if (char !== "\n") continue
breaks += 1
if (breaks >= LARGE_PASTE_BREAKS) return true
}
return false
}
export function normalizePaste(text: string) {
if (!text.includes("\r")) return text
return text.replace(/\r\n?/g, "\n")
}
export function pasteMode(text: string) {
if (largePaste(text)) return "manual"
if (text.includes("\n") || text.includes("\r")) return "manual"
return "native"
}
@@ -0,0 +1,63 @@
import { describe, expect, test } from "bun:test"
import { promptDesignPlaceholder, promptPlaceholder } from "./placeholder"
describe("promptPlaceholder", () => {
const t = (key: string, params?: Record<string, string>) => `${key}${params?.example ? `:${params.example}` : ""}`
test("returns shell placeholder in shell mode", () => {
const value = promptPlaceholder({
mode: "shell",
commentCount: 0,
example: "example",
suggest: true,
t,
})
expect(value).toBe("prompt.placeholder.shell:example")
})
test("returns summarize placeholders for comment context", () => {
expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", suggest: true, t })).toBe(
"prompt.placeholder.summarizeComment",
)
expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", suggest: true, t })).toBe(
"prompt.placeholder.summarizeComments",
)
})
test("returns default placeholder with example when suggestions enabled", () => {
const value = promptPlaceholder({
mode: "normal",
commentCount: 0,
example: "translated-example",
suggest: true,
t,
})
expect(value).toBe("prompt.placeholder.normal:translated-example")
})
test("returns simple placeholder when suggestions disabled", () => {
const value = promptPlaceholder({
mode: "normal",
commentCount: 0,
example: "translated-example",
suggest: false,
t,
})
expect(value).toBe("prompt.placeholder.simple")
})
})
describe("promptDesignPlaceholder", () => {
const t = (key: string, params?: Record<string, string>) => {
if (key !== "ui.promptInput.placeholder.normal") return key
return `Ask anything, ${params?.slash} for commands, ${params?.at} for context...`
}
test("composes the design placeholder from localized fragments", () => {
expect(promptDesignPlaceholder("normal", "fallback", t)).toBe("Ask anything, / for commands, @ for context...")
})
test("preserves the shell placeholder", () => {
expect(promptDesignPlaceholder("shell", "Enter shell command...", t)).toBe("Enter shell command...")
})
})
@@ -0,0 +1,24 @@
type PromptPlaceholderInput = {
mode: "normal" | "shell"
commentCount: number
example: string
suggest: boolean
t: (key: string, params?: Record<string, string>) => string
}
export function promptPlaceholder(input: PromptPlaceholderInput) {
if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example })
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
if (!input.suggest) return input.t("prompt.placeholder.simple")
return input.t("prompt.placeholder.normal", { example: input.example })
}
export function promptDesignPlaceholder(
mode: PromptPlaceholderInput["mode"],
placeholder: string,
t: PromptPlaceholderInput["t"],
) {
if (mode === "shell") return placeholder
return t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" })
}
@@ -1,9 +1,9 @@
import type { ComposerState, ContextItem, Prompt } from "./state"
import { type ContextItem, type Prompt, type usePrompt } from "@/context/prompt"
export type ComposerStateTarget = ReturnType<ComposerState["capture"]>
type PromptTarget = ReturnType<ReturnType<typeof usePrompt>["capture"]>
export function createComposerSubmission(input: {
target: ComposerStateTarget
export function createPromptSubmissionState(input: {
target: PromptTarget
prompt: Prompt
context: (ContextItem & { key: string })[]
}) {
@@ -20,11 +20,11 @@ export function createComposerSubmission(input: {
target.reset()
cleared = target.current()
},
retarget(next: ComposerStateTarget) {
input.context.forEach((item) => next.context.add(item))
retarget(next: PromptTarget) {
input.context.forEach(next.context.add)
target = next
},
current: (value: ComposerStateTarget) => target === value,
current: (value: PromptTarget) => target === value,
restore() {
if (cleared !== undefined && target.current() !== cleared) return
return { target, prompt: input.prompt, context: input.context }
@@ -0,0 +1,480 @@
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
import { createStore } from "solid-js/store"
import type { Prompt, PromptStore } from "@/context/prompt"
import { ServerScope } from "@/utils/server-scope"
let createPromptSubmit: typeof import("./submit").createPromptSubmit
const createdSessions: string[] = []
type SessionCreateInput = {
agent?: string
model?: { id: string; providerID: string; variant?: string }
location?: { directory: string }
}
const sentShell: Array<{ sessionID: string; id?: string; command: string }> = []
const sentShellDirectories: string[] = []
const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = []
const sentPrompts: string[] = []
const promptInputs: unknown[] = []
const sentCommands: unknown[] = []
const switchedAgents: Array<{ sessionID: string; agent: string }> = []
const switchedModels: Array<{
sessionID: string
model: { id: string; providerID: string; variant?: string }
}> = []
const sessionRequestOrder: string[] = []
const updatedDrafts: Array<{ draftID: string; worktree?: string }> = []
const promptCaptures: Array<{ scope?: unknown; target?: unknown }> = []
const navigations: string[] = []
let serverSessionSyncs = 0
let restoredPrompts = 0
let params: { id?: string } = {}
let search: { draftId?: string } = {}
let selected = "/repo/worktree-a"
let variant: string | undefined
let createSessionGate: Promise<void> | undefined
let createWorktreeGate: Promise<void> | undefined
let worktreeFailure: Error | undefined
let locationFailure: Error | undefined
let promptFailure: Error | undefined
let worktreeCreates = 0
let activeSDK = "server-a"
let activeServer = "server-a"
let commands: Array<{ name: string }> = []
let worktreeDirectory = "/repo/new-0"
let worktreeID = 0
const sessionDirectories: Record<string, string> = {}
let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
const [promptStore, setPromptStore] = createStore<PromptStore>({
prompt: promptValue,
cursor: 0,
context: { items: [] },
})
const prompt = {
store: [() => promptStore, setPromptStore] as [() => PromptStore, typeof setPromptStore],
ready: Object.assign(() => true, { promise: Promise.resolve(true) }),
current: () => promptValue,
cursor: () => 0,
dirty: () => true,
model: {
current: () => undefined,
set: () => undefined,
},
reset: () => undefined,
set: () => restoredPrompts++,
context: {
add: () => undefined,
remove: () => undefined,
removeComment: () => undefined,
updateComment: () => undefined,
replaceComments: () => undefined,
items: () => [],
},
capture: (scope?: unknown, target?: unknown) => {
promptCaptures.push({ scope, target })
return prompt
},
}
const settle = () => new Promise((resolve) => setTimeout(resolve, 0))
const clientFor = (directory: string) => {
return {
api: {
session: {
create: async (input: SessionCreateInput) => {
await createSessionGate
const location = input.location?.directory ?? directory
createdSessions.push(location)
const id = `session-${createdSessions.length}`
sessionDirectories[id] = location
return {
id,
projectID: "project",
agent: input.agent,
model: input.model,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1, updated: 1 },
title: `New session ${createdSessions.length}`,
location: { directory: location },
}
},
prompt: async (input: unknown) => {
sessionRequestOrder.push("prompt")
sentPrompts.push(sessionDirectories[(input as { sessionID: string }).sessionID] ?? directory)
promptInputs.push(input)
if (promptFailure) throw promptFailure
const prompt = input as { sessionID: string; id: string; text: string }
return {
id: prompt.id,
sessionID: prompt.sessionID,
timeCreated: 1,
type: "user" as const,
delivery: "steer" as const,
payload: { text: prompt.text },
}
},
switchAgent: async (input: { sessionID: string; agent: string }) => {
sessionRequestOrder.push("agent")
switchedAgents.push(input)
},
switchModel: async (input: {
sessionID: string
model: { id: string; providerID: string; variant?: string }
}) => {
sessionRequestOrder.push("model")
switchedModels.push(input)
},
command: async (input: unknown) => {
sentCommands.push(input)
},
shell: async (input: { sessionID: string; id?: string; command: string }) => {
sentShell.push(input)
sentShellDirectories.push(sessionDirectories[input.sessionID] ?? directory)
},
},
worktree: {
create: async (_input: unknown) => {
worktreeCreates++
await createWorktreeGate
if (worktreeFailure) throw worktreeFailure
return { directory: worktreeDirectory }
},
},
location: {
get: async () => {
if (locationFailure) throw locationFailure
return { directory: worktreeDirectory }
},
},
},
session: {
command: async () => ({ data: undefined }),
abort: async () => ({ data: undefined }),
},
}
}
beforeAll(async () => {
const rootClient = clientFor("/repo/main")
mock.module("@solidjs/router", () => ({
useNavigate: () => (href: string) => navigations.push(href),
useParams: () => params,
useLocation: () => ({}),
useSearchParams: () => [search, () => undefined],
}))
mock.module("@opencode-ai/ui/toast", () => ({
Toast: { Region: () => null },
toaster: { create: () => undefined, show: () => undefined, dismiss: () => undefined },
showToast: () => 0,
}))
mock.module("@opencode-ai/util/encode", () => ({
base64Decode: (value: string) => value,
base64Encode: (value: string) => value,
checksum: (value: string) => value,
sampledChecksum: (value: string) => value,
}))
mock.module("@/context/local", () => ({
useLocal: () => ({
model: {
current: () => ({ id: "model", provider: { id: "provider" } }),
variant: { current: () => variant },
},
agent: {
current: () => ({ name: "agent" }),
},
session: {
promote: () => undefined,
},
}),
}))
mock.module("@/context/permission", () => {
return { usePermission: () => ({ currentServerState: () => ({ enableAutoAccept: () => undefined }) }) }
})
mock.module("@/context/tabs", () => ({
useTabs: () => ({
updateDraft: (draftID: string, draft: { worktree?: string }) => {
updatedDrafts.push({ draftID, ...draft })
},
promoteDraft: (draftID: string, session: { server: string; sessionId: string }) => {
promotedDrafts.push({ draftID, ...session })
},
}),
}))
mock.module("@/context/prompt", () => ({
usePrompt: () => prompt,
}))
mock.module("@/context/location", () => ({
useWorkspaceLocation: () => {
return () => ({
directory: activeSDK === "server-a" ? "/repo/main" : "/repo/other",
})
},
}))
mock.module("@/context/server-sdk", () => ({
useServerSDK: () => ({
scope: activeSDK === "server-a" ? ServerScope.local : "server-b",
api: rootClient.api,
}),
}))
mock.module("@/context/server", () => ({
useServer: () => ({ key: activeServer }),
useData: () => ({
session: {
remember: () => undefined,
setStatus: () => undefined,
// Delegates straight to the API client; optimistic admission and
// rollback are covered by the data-layer tests in packages/tui.
prompt: (input: unknown) => rootClient.api.session.prompt(input as never),
},
location: {
info: () => ({ project: { id: "project", directory: "/repo/main" } }),
command: {
list: () => commands,
},
},
}),
}))
mock.module("@/context/platform", () => ({
usePlatform: () => ({
fetch: fetch,
}),
}))
mock.module("@/context/language", () => ({
useLanguage: () => ({
t: (key: string) => key,
}),
}))
const mod = await import("./submit")
createPromptSubmit = mod.createPromptSubmit
})
beforeEach(() => {
createdSessions.length = 0
promotedDrafts.length = 0
updatedDrafts.length = 0
sentCommands.length = 0
sentPrompts.length = 0
promptInputs.length = 0
switchedAgents.length = 0
switchedModels.length = 0
sessionRequestOrder.length = 0
promptCaptures.length = 0
navigations.length = 0
restoredPrompts = 0
params = {}
search = {}
sentShell.length = 0
sentShellDirectories.length = 0
selected = "/repo/worktree-a"
variant = undefined
activeSDK = "server-a"
activeServer = "server-a"
commands = []
promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }]
worktreeDirectory = `/repo/new-${++worktreeID}`
createSessionGate = undefined
serverSessionSyncs = 0
createWorktreeGate = undefined
worktreeFailure = undefined
locationFailure = undefined
promptFailure = undefined
worktreeCreates = 0
for (const key of Object.keys(sessionDirectories)) delete sessionDirectories[key]
})
const event = { preventDefault: () => undefined } as unknown as Event
const makeSubmit = (overrides: Partial<Parameters<typeof createPromptSubmit>[0]> = {}) =>
createPromptSubmit({
prompt,
info: () => undefined,
imageAttachments: () => [],
commentCount: () => 0,
autoAccept: () => false,
mode: () => "normal",
working: () => false,
editor: () => undefined,
queueScroll: () => undefined,
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
addToHistory: () => undefined,
resetHistoryNavigation: () => undefined,
setMode: () => undefined,
setPopover: () => undefined,
newSessionWorktree: () => selected,
onNewSessionWorktreeReset: () => undefined,
onSubmit: () => undefined,
...overrides,
})
describe("prompt submit worktree selection", () => {
test("admits only one concurrent new-workspace submission", async () => {
selected = "create"
let release = () => {}
createWorktreeGate = new Promise<void>((resolve) => {
release = resolve
})
const submit = makeSubmit()
const first = submit.handleSubmit(event)
const duplicate = submit.handleSubmit(event)
expect(worktreeCreates).toBe(1)
release()
await Promise.all([first, duplicate])
expect(createdSessions).toEqual([worktreeDirectory])
await settle()
expect(worktreeCreates).toBe(1)
expect(createdSessions).toHaveLength(1)
expect(sentPrompts).toEqual([worktreeDirectory])
expect(navigations).toEqual(["/server/server-a/session/session-1"])
})
test("stops when the created workspace cannot initialize", async () => {
selected = "create"
locationFailure = new Error("initialization failed")
await makeSubmit().handleSubmit(event)
expect(worktreeCreates).toBe(1)
expect(createdSessions).toEqual([])
expect(sentPrompts).toEqual([])
})
test("keeps async submission effects bound to the initiating context", async () => {
search = { draftId: "draft-1" }
let release = () => {}
createSessionGate = new Promise<void>((resolve) => {
release = resolve
})
let submitted = 0
const submit = makeSubmit({
onSubmit: () => submitted++,
})
const result = submit.handleSubmit(event)
activeSDK = "server-b"
activeServer = "server-b"
search.draftId = "draft-2"
release()
await result
await settle()
expect(updatedDrafts).toEqual([{ draftID: "draft-1", worktree: undefined }])
expect(promotedDrafts).toEqual([{ draftID: "draft-1", server: "server-a", sessionId: "session-1" }])
expect(promptCaptures.at(-1)?.target).toEqual({ server: "server-a", scope: ServerScope.local })
expect(submitted).toBe(0)
})
test("switches the selected agent and model before prompting", async () => {
params = { id: "session-1" }
variant = "high"
const submit = makeSubmit({
info: () => ({
id: "session-1",
agent: "old-agent",
model: { id: "old-model", providerID: "old-provider" },
}),
})
await submit.handleSubmit(event)
await Bun.sleep(0)
expect(sentPrompts).toEqual(["/repo/main"])
expect(switchedAgents).toEqual([{ sessionID: "session-1", agent: "agent" }])
expect(switchedModels).toEqual([
{
sessionID: "session-1",
model: { id: "model", providerID: "provider", variant: "high" },
},
])
expect(sessionRequestOrder).toEqual(["agent", "model", "prompt"])
expect(promptInputs[0]).toMatchObject({
sessionID: "session-1",
text: "ls",
files: [],
agents: [],
metadata: {
displayText: "ls",
comments: [],
agent: "agent",
model: { providerID: "provider", modelID: "model", variant: "high" },
},
})
// ID minting is delegated to the data layer, which mints a client ID when
// none is supplied (covered by the data-layer tests in packages/tui).
expect((promptInputs[0] as { id?: string }).id).toBeUndefined()
})
test("restores the prompt when sending fails", async () => {
params = { id: "session-1" }
promptFailure = new Error("connection lost")
const submit = makeSubmit({
info: () => ({ id: "session-1", agent: "agent", model: { id: "model", providerID: "provider" } }),
})
await submit.handleSubmit(event)
await settle()
expect(restoredPrompts).toBe(1)
})
test("submits slash commands through the current session API", async () => {
params = { id: "session-1" }
variant = "high"
commands.push({ name: "review" })
promptValue = [{ type: "text", content: "/review staged changes", start: 0, end: 22 }]
const submit = makeSubmit({
info: () => ({ id: "session-1" }),
})
await submit.handleSubmit(event)
await settle()
expect(sentCommands).toEqual([
{
sessionID: "session-1",
id: expect.stringMatching(/^msg_/),
command: "review",
arguments: "staged changes",
agent: "agent",
model: { id: "model", providerID: "provider", variant: "high" },
files: [],
},
])
expect(serverSessionSyncs).toBe(0)
})
test("sends an initial shell after synchronous workspace creation", async () => {
selected = "create"
const submit = makeSubmit({
mode: () => "shell",
})
await submit.handleSubmit(event)
await settle()
expect(sentShellDirectories).toEqual([worktreeDirectory])
expect(sentShell[0]).toMatchObject({
sessionID: "session-1",
command: "ls",
})
})
})
@@ -0,0 +1,505 @@
import type { Data } from "@opencode-ai/client/solid"
import { showToast } from "@/utils/toast"
import { base64Encode } from "@opencode-ai/util/encode"
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
import { startTransition, type Accessor } from "solid-js"
import { useTabs } from "@/context/tabs"
import { useData } from "@/context/server"
import { useLanguage } from "@/context/language"
import { useLocal, type ModelSelection } from "@/context/local"
import { usePermission } from "@/context/permission"
import { type ContextItem, type ImageAttachmentPart, type Prompt, type usePrompt } from "@/context/prompt"
import { useWorkspaceLocation } from "@/context/location"
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { getDirectory } from "@opencode-ai/util/path"
import { buildPromptRequest } from "./build-prompt-request"
import { setCursorPosition } from "./editor-dom"
import { formatServerError } from "@/utils/server-errors"
import { ScopedKey } from "@/utils/server-scope"
import { createPromptSubmissionState } from "./submission-state"
import { Event } from "@opencode-ai/schema/event"
import { blobDataUrl } from "@/utils/draft-store"
import { useServer } from "@/context/server"
import { sessionHref } from "@/utils/session-route"
const submitting = new Set<string>()
export type FollowupDraft = {
sessionID: string
sessionDirectory: string
prompt: Prompt
context: (ContextItem & { key: string })[]
agent: string
model: { providerID: string; modelID: string }
variant?: string
}
type FollowupSendInput = {
api: ServerSDK["api"]["session"]
data: Data
session: Accessor<{ agent?: string; model?: { id: string; providerID: string; variant?: string } } | undefined>
draft: FollowupDraft
optimisticBusy?: boolean
}
const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ? part.content : "")).join("")
const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttachmentPart => part.type === "image")
export async function sendFollowupDraft(input: FollowupSendInput) {
const text = draftText(input.draft.prompt)
const images = draftImages(input.draft.prompt)
const setBusy = () => {
if (!input.optimisticBusy) return
input.data.session.setStatus(input.draft.sessionID, "running")
}
const setIdle = () => {
if (!input.optimisticBusy) return
input.data.session.setStatus(input.draft.sessionID, "idle")
}
const [head, ...tail] = text.split(" ")
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
if (
cmd &&
input.data.location.command.list({ directory: input.draft.sessionDirectory })?.some((item) => item.name === cmd)
) {
setBusy()
try {
await input.api.command({
sessionID: input.draft.sessionID,
id: SessionMessage.ID.create(),
command: cmd,
arguments: tail.join(" "),
agent: input.draft.agent,
model: {
id: input.draft.model.modelID,
providerID: input.draft.model.providerID,
variant: input.draft.variant,
},
files: await Promise.all(
images.map(async (attachment) => ({
uri: await blobDataUrl(attachment.blob, attachment.mime),
name: attachment.filename,
})),
),
})
return true
} catch (err) {
setIdle()
throw err
}
}
const encodedImages = await Promise.all(
images.map(async (attachment) => ({
...attachment,
dataUrl: await blobDataUrl(attachment.blob, attachment.mime),
})),
)
const request = buildPromptRequest({
prompt: input.draft.prompt,
context: input.draft.context,
images: encodedImages,
text,
sessionDirectory: input.draft.sessionDirectory,
})
setBusy()
try {
const session = input.session()
if (session?.agent !== input.draft.agent) {
await input.api.switchAgent({ sessionID: input.draft.sessionID, agent: input.draft.agent })
}
if (
session?.model?.providerID !== input.draft.model.providerID ||
session.model.id !== input.draft.model.modelID ||
(session.model.variant ?? "default") !== (input.draft.variant ?? "default")
) {
await input.api.switchModel({
sessionID: input.draft.sessionID,
model: {
id: input.draft.model.modelID,
providerID: input.draft.model.providerID,
variant: input.draft.variant,
},
})
}
// The data layer admits optimistically under a client-minted ID: the
// prompt renders immediately and rolls back if the server rejects it.
await input.data.session.prompt({
sessionID: input.draft.sessionID,
text: request.text,
files: request.files.map((file) => ({ uri: file.uri, name: file.name, mention: file.mention })),
agents: request.agents,
metadata: {
displayText: request.displayText,
comments: request.comments,
agent: input.draft.agent,
model: {
...input.draft.model,
...(input.draft.variant ? { variant: input.draft.variant } : {}),
},
},
})
return true
} catch (err) {
setIdle()
throw err
}
}
type PromptSubmitInput = {
prompt: ReturnType<typeof usePrompt>
info: Accessor<
{ id: string; agent?: string; model?: { id: string; providerID: string; variant?: string } } | undefined
>
imageAttachments: Accessor<ImageAttachmentPart[]>
commentCount: Accessor<number>
autoAccept: Accessor<boolean>
mode: Accessor<"normal" | "shell">
working: Accessor<boolean>
editor: () => HTMLDivElement | undefined
queueScroll: () => void
promptLength: (prompt: Prompt) => number
addToHistory: (prompt: Prompt, mode: "normal" | "shell") => void
resetHistoryNavigation: () => void
setMode: (mode: "normal" | "shell") => void
setPopover: (popover: "at" | "slash" | null) => void
newSessionWorktree?: Accessor<string | undefined>
onNewSessionWorktreeReset?: () => void
shouldQueue?: Accessor<boolean>
onQueue?: (draft: FollowupDraft) => void
onAbort?: () => void
onSubmit?: () => void
model?: ModelSelection
}
export function createPromptSubmit(input: PromptSubmitInput) {
const navigate = useNavigate()
const sdk = useWorkspaceLocation()
const serverSDK = useServerSDK()
const data = useData()
const server = useServer()
const local = useLocal()
const permission = usePermission()
const prompt = input.prompt
const language = useLanguage()
const params = useParams()
const [search] = useSearchParams<{ draftId?: string }>()
const tabs = useTabs()
const errorMessage = (err: unknown) => {
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message
if (err && typeof err === "object" && "data" in err) {
const data = (err as { data?: { message?: string } }).data
if (data?.message) return data.message
}
if (err instanceof Error) return err.message
return language.t("common.requestFailed")
}
const abort = async () => {
const sessionID = params.id
if (!sessionID) return Promise.resolve()
input.onAbort?.()
return serverSDK.api.session.interrupt({ sessionID }).catch(() => {})
}
const restoreCommentItems = (
target: ReturnType<ReturnType<typeof usePrompt>["capture"]>,
items: (ContextItem & { key: string })[],
) => {
for (const item of items) {
target.context.add({
type: "file",
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
commentOrigin: item.commentOrigin,
preview: item.preview,
})
}
}
const clearContext = (target: ReturnType<ReturnType<typeof usePrompt>["capture"]>) => {
for (const item of target.context.items()) {
target.context.remove(item.key)
}
}
const handleSubmit = async (event: Event) => {
event.preventDefault()
const target = prompt.capture()
const submission = createPromptSubmissionState({
target,
prompt: target.current(),
context: target.context.items().slice(),
})
const currentPrompt = submission.prompt
const context = submission.context
const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("")
const images = input.imageAttachments().slice()
const mode = input.mode()
if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) {
if (input.working()) void abort()
return
}
const modelSelection = input.model ?? local.model
const currentModel = modelSelection.current()
const currentAgent = local.agent.current()
const variant = modelSelection.variant.current()
if (!currentModel || !currentAgent) {
showToast({
title: language.t("prompt.toast.modelAgentRequired.title"),
description: language.t("prompt.toast.modelAgentRequired.description"),
})
return
}
const submissionSDK = sdk()
const submissionServerSDK = serverSDK
const submissionData = data
const submissionScope = submissionServerSDK.scope
const submissionServer = server.key
const projectDirectory = submissionSDK.directory
const sessionID = params.id
const isNewSession = !sessionID
const currentSession = input.info()
const draftID = search.draftId
const capturePrompt = prompt.capture
const localSession = local.session
const resetWorktree = input.onNewSessionWorktreeReset
const onSubmit = input.onSubmit
const permissionState = permission
const shouldAutoAccept = isNewSession && input.autoAccept()
const worktreeSelection = input.newSessionWorktree?.() || "main"
const submissionKey = ScopedKey.from(
submissionScope,
draftID ? `draft:${draftID}` : sessionID ? `session:${sessionID}` : `directory:${projectDirectory}`,
)
if (submitting.has(submissionKey)) return
submitting.add(submissionKey)
try {
input.addToHistory(currentPrompt, mode)
input.resetHistoryNavigation()
let sessionDirectory = projectDirectory
if (isNewSession) {
if (worktreeSelection === "create") {
const createdWorktree = await submissionServerSDK.api.worktree
.create({
projectID: submissionData.location.info({ directory: projectDirectory })?.project.id ?? "",
strategy: "git",
directory: getDirectory(
submissionData.location.info({ directory: projectDirectory })?.project.directory ?? projectDirectory,
),
})
.then(async (created) => {
await submissionServerSDK.api.location.get({ location: { directory: created.directory } })
return created
})
.catch((err) => {
showToast({
title: language.t("prompt.toast.worktreeCreateFailed.title"),
description: errorMessage(err),
})
})
if (!createdWorktree) return
sessionDirectory = createdWorktree.directory
}
if (worktreeSelection !== "main" && worktreeSelection !== "create") {
sessionDirectory = worktreeSelection
}
}
let session = currentSession
if (!session && isNewSession) {
const created = await submissionServerSDK.api.session
.create({
agent: currentAgent.name,
model: { id: currentModel.id, providerID: currentModel.provider.id, variant },
location: { directory: sessionDirectory },
})
.catch((err) => {
showToast({
title: language.t("prompt.toast.sessionCreateFailed.title"),
description: errorMessage(err),
})
return undefined
})
if (created) {
submissionData.session.remember(created)
session = created
await startTransition(() => {
if (!session) return
if (draftID) tabs.updateDraft(draftID, { worktree: undefined })
if (!draftID) resetWorktree?.()
if (shouldAutoAccept) permissionState.enableAutoAccept(session.id, sessionDirectory)
localSession.promote(sessionDirectory, session.id, {
agent: currentAgent.name,
model: { providerID: currentModel.provider.id, modelID: currentModel.id },
variant: variant ?? null,
})
if (draftID) tabs.promoteDraft(draftID, { server: submissionServer, sessionId: session.id })
else navigate(sessionHref(submissionServer, session.id))
submission.retarget(
capturePrompt(
{ dir: base64Encode(sessionDirectory), id: session.id },
{ server: submissionServer, scope: submissionScope },
),
)
})
}
}
if (!session) {
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
description: language.t("prompt.toast.promptSendFailed.description"),
})
return
}
const model = {
modelID: currentModel.id,
providerID: currentModel.provider.id,
}
const agent = currentAgent.name
const draft: FollowupDraft = {
sessionID: session.id,
sessionDirectory,
prompt: currentPrompt,
context,
agent,
model,
variant,
}
const clearInput = () => {
submission.clear()
input.setMode("normal")
input.setPopover(null)
}
const restoreInput = () => {
const restored = submission.restore()
if (!restored) return false
restored.target.set(restored.prompt, input.promptLength(restored.prompt))
if (!submission.current(prompt.capture())) return true
input.setMode(mode)
input.setPopover(null)
requestAnimationFrame(() => {
const editor = input.editor()
if (!editor) return
editor.focus()
setCursorPosition(editor, input.promptLength(currentPrompt))
input.queueScroll()
})
return true
}
if (!isNewSession && mode === "normal" && input.shouldQueue?.()) {
input.onQueue?.(draft)
clearContext(submission.target())
clearInput()
return
}
if (!draftID || search.draftId === draftID) onSubmit?.()
if (mode === "shell") {
clearInput()
const eventID = Event.ID.create()
void submissionServerSDK.api.session
.shell({
sessionID: session.id,
id: eventID,
command: text,
})
.catch((err) => {
showToast({
title: language.t("prompt.toast.shellSendFailed.title"),
description: errorMessage(err),
})
restoreInput()
})
return
}
if (text.startsWith("/")) {
const [cmdName, ...args] = text.split(" ")
const commandName = cmdName.slice(1)
const customCommand = submissionData.location.command
.list({ directory: sessionDirectory })
?.find((command) => command.name === commandName)
if (customCommand) {
clearInput()
submissionData.session.setStatus(session.id, "running")
void submissionServerSDK.api.session
.command({
sessionID: session.id,
id: SessionMessage.ID.create(),
command: commandName,
arguments: args.join(" "),
agent,
model: { id: model.modelID, providerID: model.providerID, variant },
files: await Promise.all(
images.map(async (attachment) => ({
uri: await blobDataUrl(attachment.blob, attachment.mime),
name: attachment.filename,
})),
),
})
.catch((err) => {
submissionData.session.setStatus(session.id, "idle")
showToast({
title: language.t("prompt.toast.commandSendFailed.title"),
description: formatServerError(err, language.t, language.t("common.requestFailed")),
})
restoreInput()
})
return
}
}
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
for (const item of commentItems) submission.target().context.remove(item.key)
clearInput()
void sendFollowupDraft({
api: submissionServerSDK.api.session,
data: submissionData,
session: () => session,
draft,
optimisticBusy: sessionDirectory === projectDirectory,
}).catch((err) => {
if (sessionDirectory === projectDirectory) {
submissionData.session.setStatus(session.id, "idle")
}
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
description: errorMessage(err),
})
if (restoreInput()) restoreCommentItems(submission.target(), commentItems)
})
} finally {
submitting.delete(submissionKey)
}
}
return {
abort,
handleSubmit,
}
}
@@ -1,3 +1,4 @@
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Popover } from "@opencode-ai/ui/popover"
@@ -35,6 +36,72 @@ export function StatusPopover() {
lsp: [],
}),
)
return (
<Popover
open={shown()}
onOpenChange={setShown}
triggerAs={Button}
triggerProps={{
variant: "ghost",
class: "titlebar-icon w-8 h-6 p-0 box-border",
"aria-label": language.t("status.popover.trigger"),
style: { scale: 1 },
}}
trigger={
<div class="relative size-4">
<div class="badge-mask-tight size-4 flex items-center justify-center">
<Icon name={shown() ? "status-active" : "status"} size="small" />
</div>
<div
class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({
ready: ready(),
serverHealth: serverHealth(),
attention: attention(),
issue: issue(),
})}`}
/>
</div>
}
class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
gutter={4}
placement="bottom-end"
shift={-168}
>
<Show when={shown()}>
<Suspense
fallback={
<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />
}
>
<Body shown={shown()} />
</Suspense>
</Show>
</Popover>
)
}
export function StatusPopoverV2() {
const language = useLanguage()
const server = useServer()
const global = useGlobal()
const data = useData()
const sdk = useWorkspaceLocation()
const [shown, setShown] = createSignal(false)
const serverHealth = () => global.servers.health[server.key]?.healthy
const mcp = () => data.location.mcp.server.list({ directory: sdk().directory })
const ready = createMemo(() => serverHealth() === false || mcp() !== undefined)
const attention = createMemo(() =>
hasServiceNeedingAttention({
mcp: (mcp() ?? []).map((item) => item.status.status),
}),
)
const issue = createMemo(() =>
hasNonBlockingServiceIssue({
mcp: (mcp() ?? []).map((item) => item.status.status),
lsp: [],
}),
)
const state = createMemo<StatusPopoverState>(() => ({
shown: shown(),
ready: ready(),
@@ -14,7 +14,7 @@ import { useGlobal, useServerCtx, type ServerCtx } from "@/context/global"
import { useLanguage } from "@/context/language"
import { useCommand } from "@/context/command"
import { useTabs } from "@/context/tabs"
import { createTabComposerState } from "@/composer/persistence"
import { createTabPromptState } from "@/context/prompt"
import { base64Encode } from "@opencode-ai/util/encode"
import { showToast } from "@/utils/toast"
import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture"
@@ -146,7 +146,7 @@ function SessionTabEntry(props: {
tabs.rememberSessionInfo(props.tab, value)
const current = sdk()
if (!current) return
createTabComposerState(tabs, props.tab, current.scope, {
createTabPromptState(tabs, props.tab, current.scope, {
dir: base64Encode(value.location.directory),
id: value.id,
})
+3 -3
View File
@@ -31,7 +31,7 @@ import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/comp
import { useGlobal } from "@/context/global"
import { ServerConnection } from "@/context/servers"
import { tabKey, useTabs } from "@/context/tabs"
import type { ComposerState } from "@/composer/persistence"
import type { PromptSession } from "@/context/prompt"
import "./titlebar.css"
import { newTabTooltipKeybind } from "./command-tooltip-keybind"
@@ -240,7 +240,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
server: route.server,
sessionId: activeSession.id,
}
const model = tabs.stateValue<ComposerState>(sessionTab, "prompt")?.model.current()
const model = tabs.stateValue<PromptSession>(sessionTab, "prompt")?.model.current()
void tabs.newDraft(
{ server: sessionTab.server, directory: activeSession.location.directory },
"",
@@ -252,7 +252,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
const activeTab = currentTab()
if (activeTab?.type !== "draft") return
const model = tabs.stateValue<ComposerState>(activeTab, "prompt")?.model.current()
const model = tabs.stateValue<PromptSession>(activeTab, "prompt")?.model.current()
void tabs.newDraft({ server: activeTab.server, directory: activeTab.directory }, "", model)
return
}
-83
View File
@@ -1,83 +0,0 @@
import type { Data } from "@opencode-ai/client/solid"
import type { Accessor } from "solid-js"
import type { ModelSelection } from "@/context/local"
import type { ServerSDK } from "@/context/server-sdk"
import type { ComposerStateTarget } from "./submission-state"
import type { createComposerSubmission } from "./submission-state"
export type ComposerControls = {
agents: {
available: { name: string; hidden?: boolean; mode: string }[]
options: string[]
current: string
visible: boolean
select: (name: string | undefined) => void
}
model: {
selection: ModelSelection
paid: boolean
loading: boolean
}
session: {
tabs: {
active: () => string | undefined
all: () => string[]
open: (tab: string) => void | Promise<void>
setActive: (tab: string) => void
}
reviewPanel: {
opened: () => boolean
open: () => void
}
}
}
export type ComposerSelection = {
agent: string
model: { providerID: string; modelID: string }
variant?: string
}
export type ComposerSession = {
id: string
directory: string
api: {
command: (input: Parameters<ServerSDK["api"]["session"]["command"]>[0]) => Promise<unknown>
shell: (input: Parameters<ServerSDK["api"]["session"]["shell"]>[0]) => Promise<unknown>
switchAgent: (input: Parameters<ServerSDK["api"]["session"]["switchAgent"]>[0]) => Promise<unknown>
switchModel: (input: Parameters<ServerSDK["api"]["session"]["switchModel"]>[0]) => Promise<unknown>
}
data: {
location: { command: Pick<Data["location"]["command"], "list"> }
session: {
prompt: (input: Parameters<Data["session"]["prompt"]>[0]) => Promise<unknown>
}
}
current: Accessor<{ agent?: string; model?: { id: string; providerID: string; variant?: string } } | undefined>
admitted: (messageID: string) => boolean
}
type ComposerAdapterBase = {
state: ComposerStateTarget
ready: Accessor<boolean>
controls: Accessor<ComposerControls>
working: Accessor<boolean>
submitted: () => void
}
export type ActiveComposerAdapter = ComposerAdapterBase & {
kind: "active-session"
session: () => ComposerSession
interrupt: () => Promise<void>
setEditor: (element: HTMLDivElement) => void
}
export type NewSessionComposerAdapter = ComposerAdapterBase & {
kind: "new-session"
start: (
selection: ComposerSelection,
submission: ReturnType<typeof createComposerSubmission>,
) => Promise<ComposerSession | undefined>
}
export type ComposerAdapter = ActiveComposerAdapter | NewSessionComposerAdapter
@@ -1,43 +0,0 @@
@keyframes composer-attachments-fade-left {
from {
visibility: hidden;
}
to {
visibility: visible;
}
}
@keyframes composer-attachments-fade-right {
from {
visibility: visible;
}
to {
visibility: hidden;
}
}
[data-component="composer-attachments"] {
timeline-scope: --composer-attachments-scroll;
[data-slot^="composer-attachments-fade-"] {
visibility: hidden;
}
}
@supports (animation-timeline: --composer-attachments-scroll) and (timeline-scope: --composer-attachments-scroll) {
[data-component="composer-attachments"] [data-slot="composer-attachments-scroll"] {
scroll-timeline: --composer-attachments-scroll x;
}
[data-component="composer-attachments"] [data-slot="composer-attachments-fade-left"] {
animation: composer-attachments-fade-left linear both;
animation-timeline: --composer-attachments-scroll;
animation-range: 0 0.1px;
}
[data-component="composer-attachments"] [data-slot="composer-attachments-fade-right"] {
animation: composer-attachments-fade-right linear both;
animation-timeline: --composer-attachments-scroll;
animation-range: calc(100% - 1.1px) calc(100% - 1px);
}
}
@@ -1,474 +0,0 @@
import { Show, createMemo, onMount, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import type { ModelSelection } from "@/context/local"
import { STORY_MODEL, emptySessionDocument, pendingAndQueuedDocument } from "@opencode-ai/session-ui/storybook"
import { Composer } from "./composer"
import type { ComposerModel } from "./model"
import { createComposerEditor } from "./editor/interaction"
import type { ComposerPersistedState, ComposerSuggestion } from "./types"
import { buildPromptRequest } from "./request"
import { SessionPreview } from "@/session/story-model"
import { Skill } from "@opencode-ai/schema/skill"
import { resolveSessionComposerSelection } from "@/session/composer/selection"
const selectedModel = {
id: STORY_MODEL.id,
providerID: STORY_MODEL.providerID,
api: { id: STORY_MODEL.id, url: "https://api.anthropic.com", npm: "@ai-sdk/anthropic" },
name: "Claude Sonnet 4",
family: "claude-sonnet",
capabilities: {
temperature: true,
reasoning: true,
attachment: true,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: true },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: true,
},
cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
limit: { context: 200_000, output: 64_000 },
status: "active",
options: {},
headers: {},
release_date: "2025-05-22",
variants: { balanced: {}, high: {} },
provider: {
id: STORY_MODEL.providerID,
name: "Anthropic",
source: "custom",
env: [],
options: {},
models: {},
},
latest: true,
} satisfies NonNullable<ReturnType<ModelSelection["current"]>>
function ComposerStory(props: {
prompt?: ComposerPersistedState["prompt"]
comments?: ComposerPersistedState["context"]["items"]
working?: boolean
stopping?: boolean
suggestions?: "command" | "context"
failure?: boolean
label?: string
inspectRequest?: boolean
continueOnStop?: boolean
}) {
const [draft, setDraft] = createStore<ComposerPersistedState>({
prompt: props.prompt ?? [{ type: "text", content: "", start: 0, end: 0 }],
cursor: props.prompt?.reduce((length, part) => length + ("content" in part ? part.content.length : 0), 0) ?? 0,
model: { providerID: STORY_MODEL.providerID, modelID: STORY_MODEL.id, variant: STORY_MODEL.variant },
context: { items: props.comments ?? [] },
})
const [story, setStory] = createStore({
activity: props.label ?? "Ready",
variant: STORY_MODEL.variant,
})
const modelSelection = {
ready: Object.assign(() => true, { promise: undefined }),
current: () => selectedModel,
recent: () => [selectedModel],
list: () => [selectedModel],
cycle() {},
set() {},
visible: () => true,
setVisibility() {},
variant: {
configured: () => STORY_MODEL.variant,
selected: () => story.variant,
current: () => story.variant,
list: () => ["balanced", "high"],
set: (variant: string | undefined) => setStory("variant", variant ?? "balanced"),
cycle() {},
},
} satisfies ModelSelection
const commands: ComposerSuggestion[] = [
{ id: "command.test", kind: "command", label: "/test", trigger: "test", title: "Run tests" },
{ id: "command.review", kind: "command", label: "/review", trigger: "review", title: "Review changes" },
]
const context: ComposerSuggestion[] = [
{
id: "file:src/app.tsx",
kind: "file",
label: "src/app.tsx",
path: "src/app.tsx",
mention: { type: "file", path: "src/app.tsx", content: "@src/app.tsx", start: 0, end: 0 },
},
{
id: "agent:review",
kind: "agent",
label: "@review",
mention: { type: "agent", name: "review", content: "@review", start: 0, end: 0 },
},
{
id: "skill:effect",
kind: "skill",
label: "@effect",
description: "Build Effect applications",
mention: {
type: "skill",
id: Skill.ID.make("effect"),
name: Skill.Name.make("Effect"),
content: "@effect",
start: 0,
end: 0,
},
},
]
const editor = createComposerEditor({
store: [draft, setDraft],
commands: () => commands,
context: () => context,
searchContextFiles: () => [],
view: {
placeholder: () => "Ask anything, / for commands, @ for context...",
agent: {
options: () => [
{ id: "build", label: "build" },
{ id: "review", label: "review" },
],
current: () => "build",
onSelect: (agent) => setStory("activity", `Selected ${agent}`),
},
variant: {
options: () => [
{ id: "balanced", label: "balanced" },
{ id: "high", label: "high" },
],
current: () => story.variant,
onSelect: (variant) => setStory("variant", variant),
},
submit: {
stopping: () => !!props.stopping,
working: () => !!props.working,
onSubmit: () => {
const value = draft.prompt.map((part) => ("content" in part ? part.content : `[${part.filename}]`)).join("")
const request = props.inspectRequest
? buildPromptRequest({
prompt: draft.prompt,
context: draft.context.items,
images: [],
text: value,
sessionDirectory: "C:/repo",
})
: undefined
setDraft("prompt", [{ type: "text", content: "", start: 0, end: 0 }])
setDraft("cursor", 0)
if (props.failure) {
setDraft("prompt", props.prompt ?? [{ type: "text", content: "", start: 0, end: 0 }])
setStory("activity", "Submission failed; draft restored")
return
}
setStory(
"activity",
request
? JSON.stringify({ files: request.files, agents: request.agents, skills: request.skills })
: `Submitted: ${value}`,
)
},
onStop: () =>
setStory("activity", props.continueOnStop ? "POST /interrupt · continue: true" : "Stop requested"),
},
},
})
const model = {
...editor,
model: { selection: modelSelection, paid: true, loading: false },
} satisfies ComposerModel
onMount(() => {
if (props.suggestions === "command") model.openCommands()
if (props.suggestions === "context") model.openContext()
})
return (
<div class="mx-auto flex min-h-80 w-full max-w-200 flex-col justify-end gap-3 rounded-xl bg-v2-background-bg-deep p-6">
<output class="text-12-regular text-text-weak" aria-live="polite">
{story.activity}
</output>
<Composer model={model} borderUnderlay />
</div>
)
}
const text = (content: string): ComposerPersistedState["prompt"] => [
{ type: "text", content, start: 0, end: content.length },
]
export default {
title: "OpenCode/Composer/Flow",
component: Composer,
parameters: { layout: "centered" },
}
export const EmptyDraft = { render: () => <ComposerStory /> }
export const TextDraft = { render: () => <ComposerStory prompt={text("Explain this change")} /> }
export const MultilineDraft = {
render: () => <ComposerStory prompt={text("Review the implementation\nThen run the focused tests")} />,
}
export const MixedAttachments = {
render: () => (
<ComposerStory
prompt={[
{ type: "text", content: "Review ", start: 0, end: 7 },
{ type: "file", path: "src/app.tsx", content: "@src/app.tsx", start: 7, end: 19 },
{ type: "text", content: " with ", start: 19, end: 25 },
{ type: "agent", name: "review", content: "@review", start: 25, end: 32 },
{ type: "text", content: " and ", start: 32, end: 37 },
{
type: "skill",
id: Skill.ID.make("effect"),
name: Skill.Name.make("Effect"),
content: "@effect",
start: 37,
end: 44,
},
{
type: "image",
id: "image-story",
filename: "layout.png",
mime: "image/png",
blob: { id: "image-story", url: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" },
},
]}
comments={[
{
type: "file",
key: "comment:src/app.tsx",
path: "src/app.tsx",
selection: { startLine: 12, startChar: 0, endLine: 14, endChar: 0 },
comment: "Keep the normal flow flat",
},
]}
/>
),
}
export const ModelAndVariant = { render: () => <ComposerStory prompt={text("Compare both variants")} /> }
export const SlashSuggestions = { render: () => <ComposerStory suggestions="command" /> }
export const ContextSuggestions = { render: () => <ComposerStory suggestions="context" /> }
export const RunningAndStopping = { render: () => <ComposerStory working stopping label="Session is running" /> }
export const SteeringFollowUp = {
render: () => <ComposerStory prompt={text("Use this correction at the next boundary")} working />,
}
export const FailedSubmissionRestoration = {
render: () => <ComposerStory prompt={text("Preserve this draft on failure")} failure />,
}
export const NewSessionFirstPrompt = {
render: () => (
<ComposerStory prompt={text("Create the Session and implement the change")} label="New Session draft" />
),
}
export const ActiveSessionFollowUp = {
render: () => <ComposerStory prompt={text("Now add focused coverage")} label="Active Session follow-up" />,
}
export const RightToLeft = {
globals: { direction: "rtl" },
render: () => <ComposerStory prompt={text("راجع src/app.tsx ثم شغّل bun test")} />,
}
export const NarrowLayout = {
parameters: { viewport: { defaultViewport: "mobile1" } },
render: () => (
<div class="w-[340px]">
<ComposerStory prompt={text("Verify the narrow Composer")} />
</div>
),
}
export const DemoFirstClassSkillIDs = {
name: "Demo: First-class skill IDs",
render: () => (
<DemoFrame
title="First-class skill IDs"
description="Choose @effect, then Send. The output shows the durable skill ID sent to the prompt API."
>
<ComposerStory suggestions="context" inspectRequest label="Select a skill from the context menu" />
</DemoFrame>
),
}
export const DemoStructuredCustomCommand = {
name: "Demo: Structured custom command",
render: () => (
<DemoFrame
title="Structured custom-command input"
description="Send the draft. Files, agents, and skills remain structured instead of becoming plain command text."
>
<ComposerStory
inspectRequest
prompt={[
{ type: "text", content: "/review ", start: 0, end: 8 },
{ type: "file", path: "src/app.tsx", content: "@src/app.tsx", start: 8, end: 20 },
{ type: "text", content: " ", start: 20, end: 21 },
{ type: "agent", name: "review", content: "@review", start: 21, end: 28 },
{ type: "text", content: " ", start: 28, end: 29 },
{
type: "skill",
id: Skill.ID.make("effect"),
name: Skill.Name.make("Effect"),
content: "@effect",
start: 29,
end: 36,
},
]}
/>
</DemoFrame>
),
}
export const DemoPendingInboxHydration = {
name: "Demo: Pending inbox hydration",
render: () => <PendingInboxDemo />,
}
export const DemoServerOwnedExecutionStatus = {
name: "Demo: Server-owned execution status",
render: () => <ServerStatusDemo />,
}
export const DemoDurableSelectionPrecedence = {
name: "Demo: Durable selection precedence",
render: () => <SelectionPrecedenceDemo />,
}
export const DemoContinueOnStop = {
name: "Demo: Continue on Stop",
render: () => (
<DemoFrame
title="Continue admitted work after Stop"
description="Press Stop. The output shows the interrupt request used by the active Session adapter."
>
<ComposerStory working stopping continueOnStop label="Session execution is running" />
</DemoFrame>
),
}
function DemoFrame(props: { title: string; description: string; children: JSX.Element }) {
return (
<section class="flex w-[min(920px,calc(100vw-32px))] flex-col gap-3 rounded-xl bg-v2-background-bg-deep p-4">
<div class="flex flex-col gap-1">
<h2 class="text-16-medium text-text-strong">{props.title}</h2>
<p class="text-13-regular text-text-weak">{props.description}</p>
</div>
{props.children}
</section>
)
}
function PendingInboxDemo() {
const [store, setStore] = createStore({ hydrated: false })
return (
<DemoFrame
title="Active pending-inbox hydration"
description="Toggle hydration to simulate the active Session loading durable pending inbox rows with its messages."
>
<div class="flex flex-col gap-3">
<button
type="button"
class="self-start rounded-md bg-background-base px-3 py-2 text-13-medium text-text-strong"
onClick={() => setStore("hydrated", (value) => !value)}
>
{store.hydrated ? "Clear pending data" : "Hydrate pending data"}
</button>
<Show
when={store.hydrated}
fallback={<SessionPreview title="Pending inbox" description="Not hydrated" document={emptySessionDocument} />}
>
<SessionPreview
title="Pending inbox"
description="Hydrated from Client Data"
document={pendingAndQueuedDocument}
/>
</Show>
</div>
</DemoFrame>
)
}
function ServerStatusDemo() {
const [store, setStore] = createStore({ running: false, activity: "Idle from server projection" })
const document = createMemo(() => ({
...emptySessionDocument,
status: store.running ? ({ type: "busy" } as const) : ({ type: "idle" } as const),
}))
return (
<DemoFrame
title="Server-owned execution status"
description="Submitting does not force running or idle. Only the simulated execution event changes status."
>
<div class="flex flex-col gap-3">
<div class="flex gap-2">
<button
type="button"
class="rounded-md bg-background-base px-3 py-2 text-13-medium text-text-strong"
onClick={() => setStore("activity", "Prompt admitted; status unchanged")}
>
Admit prompt
</button>
<button
type="button"
class="rounded-md bg-background-base px-3 py-2 text-13-medium text-text-strong"
onClick={() => {
setStore("running", (value) => !value)
setStore("activity", store.running ? "execution.started" : "execution.succeeded")
}}
>
Toggle execution event
</button>
</div>
<output class="text-12-regular text-text-weak">{store.activity}</output>
<SessionPreview title="Execution status" description={store.activity} document={document()} />
</div>
</DemoFrame>
)
}
function SelectionPrecedenceDemo() {
const [store, setStore] = createStore({ durable: true })
const selection = createMemo(() =>
resolveSessionComposerSelection(
store.durable ? { agent: "build", model: { id: "claude-sonnet-4", providerID: "anthropic" } } : undefined,
{ agent: "review", model: { modelID: "gpt-5", providerID: "openai" } },
),
)
return (
<DemoFrame
title="Durable Session selection precedence"
description="The current Session model wins over historical message metadata. Clear it to see the history fallback."
>
<div class="flex flex-col gap-3">
<div class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-13-regular">
<span class="text-text-weak">SessionInfo.model</span>
<strong class="text-text-strong">{store.durable ? "anthropic/claude-sonnet-4" : "Unavailable"}</strong>
<span class="text-text-weak">Last message metadata</span>
<strong class="text-text-strong">openai/gpt-5</strong>
<span class="text-text-weak">Resolved selection</span>
<strong class="text-text-strong">
{selection().model ? `${selection().model?.providerID}/${selection().model?.modelID}` : "Unavailable"}
</strong>
</div>
<button
type="button"
class="self-start rounded-md bg-background-base px-3 py-2 text-13-medium text-text-strong"
onClick={() => setStore("durable", (value) => !value)}
>
{store.durable ? "Remove durable Session state" : "Restore durable Session state"}
</button>
<ComposerStory prompt={text("Continue with the resolved Session selection")} label="Composer is ready" />
</div>
</DemoFrame>
)
}
-133
View File
@@ -1,133 +0,0 @@
import { Show, createMemo } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ComposerEditor } from "./editor/editor"
import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import type { ComposerModel } from "./model"
export function Composer(props: {
class?: string
model: ComposerModel
borderUnderlay?: boolean
accentSubmit?: boolean
}) {
const dialog = useDialog()
const command = useCommand()
const language = useLanguage()
return (
<div class="flex flex-col gap-3">
<ComposerEditor
controller={props.model}
accentSubmit={props.accentSubmit}
borderUnderlay={props.borderUnderlay}
class={props.class}
variantControlVisible={!props.model.model.loading}
attachKeybind={command.keybindParts("file.attach")}
attachShortcut={command.keybind("file.attach")}
modelControl={
<ComposerModelControl
loading={props.model.model.loading}
paid={props.model.model.paid}
title={language.t("command.model.choose")}
keybind={command.keybindParts("model.choose")}
model={props.model.model.selection}
providerID={props.model.model.selection.current()?.provider?.id}
modelName={props.model.model.selection.current()?.name ?? language.t("dialog.model.select.title")}
onClose={props.model.restoreFocus}
onUnpaidClick={() => dialog.show(() => <DialogSelectModelUnpaid model={props.model.model.selection} />)}
/>
}
/>
</div>
)
}
function ComposerModelControl(props: {
loading: boolean
paid: boolean
title: string
keybind: string[]
model: ComposerModel["model"]["selection"]
providerID?: string
modelName: string
onClose: () => void
onUnpaidClick: () => void
}) {
const shouldAnimate = createMemo<boolean>((previous) => previous ?? props.loading)
const content = () => (
<>
<Show when={props.providerID}>
{(providerID) => (
<ProviderIcon
id={providerID()}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
)}
</Show>
<span class="truncate leading-4">{props.modelName}</span>
<span class="-ml-0.5 -mr-1 flex shrink-0">
<Icon name="chevron-down" />
</span>
</>
)
return (
<Show when={!props.loading}>
<Tooltip
placement="top"
gutter={4}
value={
<>
{props.title}
<Keybind keys={props.keybind} variant="neutral" />
</>
}
>
<Show
when={props.paid}
fallback={
<Button
data-action="composer-model"
data-control-type="dialog"
variant="ghost-muted"
size="normal"
class="min-w-0 max-w-[220px] justify-start ![font-weight:440] group"
classList={{ "animate-in fade-in": shouldAnimate() }}
style={{ height: "28px" }}
onClick={props.onUnpaidClick}
>
{content()}
</Button>
}
>
<ModelSelectorPopover
model={props.model}
trigger={(triggerProps) => (
<Button
{...triggerProps}
variant="ghost-muted"
size="normal"
style={{ height: "28px" }}
class="min-w-0 max-w-[220px] justify-start ![font-weight:440] group"
classList={{ "animate-in fade-in": shouldAnimate() }}
data-action="composer-model"
data-control-type="popover"
>
{content()}
</Button>
)}
onClose={props.onClose}
/>
</Show>
</Tooltip>
</Show>
)
}
@@ -1,55 +0,0 @@
import { describe, expect, test } from "bun:test"
import { getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./dom"
describe("Composer editor DOM", () => {
test("length helpers treat breaks as one char and ignore zero-width chars", () => {
const container = document.createElement("div")
container.appendChild(document.createTextNode("ab\u200B"))
container.appendChild(document.createElement("br"))
container.appendChild(document.createTextNode("cd"))
expect(getNodeLength(container.childNodes[0]!)).toBe(2)
expect(getNodeLength(container.childNodes[1]!)).toBe(1)
expect(getTextLength(container)).toBe(5)
})
test("setCursorPosition and getCursorPosition round-trip with pills and breaks", () => {
const container = document.createElement("div")
const pill = document.createElement("span")
pill.dataset.mention = "file"
pill.textContent = "@file"
container.appendChild(document.createTextNode("ab"))
container.appendChild(pill)
container.appendChild(document.createElement("br"))
container.appendChild(document.createTextNode("cd"))
document.body.appendChild(container)
setCursorPosition(container, 2)
expect(getCursorPosition(container)).toBe(2)
setCursorPosition(container, 7)
expect(getCursorPosition(container)).toBe(7)
setCursorPosition(container, 8)
expect(getCursorPosition(container)).toBe(8)
container.remove()
})
test("setCursorPosition and getCursorPosition round-trip across blank lines", () => {
const container = document.createElement("div")
container.appendChild(document.createTextNode("a"))
container.appendChild(document.createElement("br"))
container.appendChild(document.createElement("br"))
container.appendChild(document.createTextNode("b"))
document.body.appendChild(container)
setCursorPosition(container, 2)
expect(getCursorPosition(container)).toBe(2)
setCursorPosition(container, 3)
expect(getCursorPosition(container)).toBe(3)
container.remove()
})
})
@@ -1,3 +0,0 @@
[data-component="composer-editor"]:empty::before {
content: "\200B";
}
@@ -1,64 +0,0 @@
import { describe, expect, test } from "bun:test"
import type { Prompt } from "@/composer/state"
import { clonePromptParts, prependHistoryEntry, promptLength, type PromptHistoryComment } from "./entry"
import { upgradeHistoryState } from "./store"
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }]
const comment = (id: string, value = "note"): PromptHistoryComment => ({
id,
path: "src/a.ts",
selection: { start: 2, end: 4 },
comment: value,
time: 1,
origin: "review",
preview: "const a = 1",
})
describe("Composer history", () => {
test("prependHistoryEntry skips empty prompt and deduplicates consecutive entries", () => {
const first = prependHistoryEntry([], DEFAULT_PROMPT)
expect(first).toEqual([])
const commentsOnly = prependHistoryEntry([], DEFAULT_PROMPT, [comment("c1")])
expect(commentsOnly).toHaveLength(1)
const withOne = prependHistoryEntry([], text("hello"))
expect(withOne).toHaveLength(1)
const deduped = prependHistoryEntry(withOne, text("hello"))
expect(deduped).toBe(withOne)
const dedupedComments = prependHistoryEntry(commentsOnly, DEFAULT_PROMPT, [comment("c1")])
expect(dedupedComments).toBe(commentsOnly)
})
test("upgrades stored prompt arrays once at the persistence boundary", () => {
expect(upgradeHistoryState({ entries: [text("stored")] })).toEqual({
entries: [{ prompt: text("stored"), comments: [] }],
})
})
test("helpers clone prompt and count text content length", () => {
const original: Prompt = [
{ type: "text", content: "one", start: 0, end: 3 },
{
type: "file",
path: "src/a.ts",
content: "@src/a.ts",
start: 3,
end: 12,
selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 },
},
{ type: "image", id: "1", filename: "img.png", mime: "image/png", blob: { id: "blob", url: "blob:test" } },
]
const copy = clonePromptParts(original)
expect(copy).not.toBe(original)
expect(promptLength(copy)).toBe(12)
if (copy[1]?.type !== "file") throw new Error("expected file")
copy[1].selection!.startLine = 9
if (original[1]?.type !== "file") throw new Error("expected file")
expect(original[1].selection?.startLine).toBe(1)
})
})
@@ -1,15 +0,0 @@
import { describe, expect, test } from "bun:test"
import { composerPlaceholder } from "./placeholder"
describe("Composer placeholder", () => {
const t = (key: string, params?: Record<string, string>) =>
`${key}${params?.example ? `:${params.example}` : ""}${params?.slash ?? ""}${params?.at ?? ""}`
test("uses the shell command placeholder in shell mode", () => {
expect(composerPlaceholder("shell", t)).toBe("prompt.placeholder.shell:git status")
})
test("uses the command and context hint in normal mode", () => {
expect(composerPlaceholder("normal", t)).toBe("ui.promptInput.placeholder.normal/@")
})
})
-7
View File
@@ -1,7 +0,0 @@
export function composerPlaceholder(
mode: "normal" | "shell",
t: (key: string, params?: Record<string, string>) => string,
) {
if (mode === "shell") return t("prompt.placeholder.shell", { example: "git status" })
return t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" })
}
-214
View File
@@ -1,214 +0,0 @@
import { batch, type Accessor, createMemo, startTransition } from "solid-js"
import type { ComposerControls } from "./adapter"
import type { PromptProjectControls } from "@/components/prompt-project-selector"
import { useDirectoryPicker } from "@/components/directory-picker"
import { useGlobal, useServerCtx } from "@/context/global"
import { useLayout } from "@/context/layout"
import { useLocal, type ModelKey, type ModelSelection } from "@/context/local"
import { useServerSDK } from "@/context/server-sdk"
import { serverName, ServerConnection, useServers } from "@/context/servers"
import { useWorkspaceLocation } from "@/context/location"
import { useTabs } from "@/context/tabs"
import { useProviders } from "@/hooks/use-providers"
import { useData } from "@/context/server"
import { normalizeAgentList } from "@/context/global-sync/utils"
import { useModels } from "@/context/models"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "@/context/model-variant"
import { useComposerState } from "./persistence"
export function createComposerControls(input: { sessionKey: Accessor<string>; model?: ModelSelection }) {
const layout = useLayout()
const local = useLocal()
const sdk = useWorkspaceLocation()
const data = useData()
const providers = useProviders(() => sdk().directory)
const view = layout.view(input.sessionKey)
return createMemo<ComposerControls>(() => {
return {
agents: {
available: normalizeAgentList(data.location.agent.list({ directory: sdk().directory }) ?? []),
options: local.agent.list().map((agent) => agent.name),
current: local.agent.current()?.name ?? "",
visible: local.agent.visible(),
select: local.agent.set,
},
model: {
selection: input.model ?? local.model,
paid: providers.paid().length > 0,
loading:
(local.agent.visible() && data.location.agent.list({ directory: sdk().directory }) === undefined) ||
!providers.ready(),
},
session: {
tabs: layout.tabs(input.sessionKey),
reviewPanel: view.reviewPanel,
},
}
})
}
export function createComposerModelSelection(input: {
agent: () => { model?: ModelKey; variant?: string } | undefined
}) {
const sdk = useWorkspaceLocation()
const models = useModels()
const prompt = useComposerState()
const providers = useProviders(() => sdk().directory)
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
const valid = (model: ModelKey) => {
const provider = providers.all().get(model.providerID)
return !!provider?.models[model.modelID] && connected().has(model.providerID)
}
const recent = () => models.recent.list().find(valid)
const fallback = () =>
providers.connected().flatMap((provider) => {
const modelID = Object.values(provider.models)[0]?.id
return modelID ? [{ providerID: provider.id, modelID }] : []
})[0]
const current = () => {
const key = [prompt.model.current(), input.agent()?.model, recent(), fallback()].find(
(item): item is ModelKey => !!item && valid(item),
)
return key ? models.find(key) : undefined
}
const recentModels = createMemo(() =>
models.recent
.list()
.map(models.find)
.filter((item): item is NonNullable<typeof item> => !!item),
)
const selection = {
ready: models.ready,
current,
recent: recentModels,
list: models.list,
cycle(direction: 1 | -1) {
const items = recentModels()
const item = current()
if (!item) return
const index = items.findIndex((entry) => entry.provider.id === item.provider.id && entry.id === item.id)
if (index === -1) return
const next = items[(index + direction + items.length) % items.length]
if (next) selection.set({ providerID: next.provider.id, modelID: next.id })
},
set(item: ModelKey | undefined, options?: { recent?: boolean }) {
void startTransition(() =>
batch(() => {
prompt.model.set(item ? { ...item, variant: prompt.model.current()?.variant } : undefined)
if (!item) return
models.setVisibility(item, true)
if (options?.recent) models.recent.push(item)
}),
)
},
visible: models.visible,
setVisibility: models.setVisibility,
variant: {
configured() {
const item = input.agent()
const model = current()
if (!item || !model) return
return getConfiguredAgentVariant({
agent: { model: item.model, variant: item.variant },
model: { providerID: model.provider.id, modelID: model.id, variants: model.variants },
})
},
selected() {
return prompt.model.current()?.variant
},
current() {
const resolved = resolveModelVariant({
variants: this.list(),
selected: this.selected(),
configured: this.configured(),
})
if (resolved) return resolved
const model = current()
if (!model) return
const saved = models.variant.get({ providerID: model.provider.id, modelID: model.id })
if (saved && this.list().includes(saved)) return saved
},
list() {
return Object.keys(current()?.variants ?? {})
},
set(value: string | undefined) {
void startTransition(() =>
batch(() => {
const model = current()
if (!model) return
prompt.model.set({ providerID: model.provider.id, modelID: model.id, variant: value ?? null })
models.variant.set({ providerID: model.provider.id, modelID: model.id }, value)
}),
)
},
cycle() {
const variants = this.list()
if (variants.length === 0) return
this.set(
cycleModelVariant({
variants,
selected: this.selected(),
configured: this.configured(),
}),
)
},
},
} satisfies ModelSelection
return selection
}
export function createComposerProjectControls(props: { draftId: string }) {
const server = useServers()
const serverSDK = useServerSDK()
const sdk = useWorkspaceLocation()
const tabs = useTabs()
const global = useGlobal()
const pickDirectory = useDirectoryPicker()
const projectServer = () => serverSDK.server
const projectServerCtx = useServerCtx(projectServer)
const projects = createMemo(() => {
if (server.list.length <= 1) {
return projectServerCtx().projects.list()
}
return server.list.flatMap((conn) => {
const item = { key: ServerConnection.key(conn), name: serverName(conn) }
return global
.ensureServerCtx(conn)
.projects.list()
.map((project) => ({ ...project, server: item }))
})
})
const selectProject = (worktree: string, serverKey?: string) => {
const conn = serverKey ? server.list.find((conn) => ServerConnection.key(conn) === serverKey) : projectServer()
if (!conn) return
const target = global.ensureServerCtx(conn)
target.projects.open(worktree)
target.projects.touch(worktree)
tabs.updateDraft(props.draftId, { server: ServerConnection.key(conn), directory: worktree, worktree: undefined })
}
const addProject = (title: string, serverKey?: string) => {
const conn = serverKey ? server.list.find((conn) => ServerConnection.key(conn) === serverKey) : projectServer()
if (!conn) return
pickDirectory({
server: conn,
title,
onSelect: (result) => {
const directory = Array.isArray(result) ? result[0] : result
if (directory) selectProject(directory, serverKey)
},
})
}
return createMemo<PromptProjectControls>(() => ({
available: projects(),
directory: sdk().directory,
server: server.list.length > 1 ? ServerConnection.key(projectServer()) : undefined,
select: selectProject,
add: addProject,
}))
}
-110
View File
@@ -1,110 +0,0 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { Skill } from "@opencode-ai/schema/skill"
import { createMemoryComposerState, DEFAULT_PROMPT, parseComposerStore } from "./state"
describe("prompt state initialization", () => {
test("initializes prompt text, cursor, and model together", () => {
createRoot((dispose) => {
const model = { providerID: "anthropic", modelID: "claude", variant: "high" }
const prompt = createMemoryComposerState({ prompt: "hello", model })
expect(prompt.current()).toEqual([{ type: "text", content: "hello", start: 0, end: 5 }])
expect(prompt.cursor()).toBe(5)
expect(prompt.model.current()).toEqual(model)
expect(prompt.model.current()).not.toBe(model)
dispose()
})
})
test("uses the default prompt without initial values", () => {
createRoot((dispose) => {
const prompt = createMemoryComposerState()
expect(prompt.current()).toEqual(DEFAULT_PROMPT)
expect(prompt.cursor()).toBeUndefined()
expect(prompt.model.current()).toBeUndefined()
dispose()
})
})
test("parses persisted state into one trusted current shape", () => {
const parsed = parseComposerStore({
prompt: [
{ type: "text", content: "hello", start: 0, end: 5 },
{ type: "skill", id: "effect", name: "Effect", content: "@effect", start: 5, end: 12 },
{ type: "image", id: "broken", filename: "broken.png", mime: "image/png", blob: { id: 42 } },
{
type: "image",
id: "missing-blob",
filename: "missing.png",
mime: "image/png",
blob: { id: "content-hash-without-a-url" },
},
{
type: "image",
id: "invalid-url",
filename: "invalid.png",
mime: "image/png",
blob: { id: "hash", url: "relative-url" },
},
{
type: "image",
id: "legacy",
filename: "legacy.png",
mime: "image/png",
dataUrl: "data:image/png;base64,AAA",
},
],
cursor: -2,
model: { providerID: "anthropic", modelID: "claude", variant: "high" },
retry: { id: "invalid", agent: "build", providerID: "anthropic", modelID: "claude" },
context: {
items: [
{
type: "file",
path: "src/app.ts",
selection: { startLine: 1, startChar: 0, endLine: 2, endChar: 3 },
comment: "Check this",
key: "untrusted",
},
{ type: "file", path: 42 },
],
},
})
expect(parsed).toEqual({
prompt: [
{ type: "text", content: "hello", start: 0, end: 5 },
{
type: "skill",
id: Skill.ID.make("effect"),
name: Skill.Name.make("Effect"),
content: "@effect",
start: 5,
end: 12,
},
{
type: "image",
id: "legacy",
filename: "legacy.png",
mime: "image/png",
blob: { id: "data:image/png;base64,AAA", url: "data:image/png;base64,AAA" },
},
],
cursor: 0,
model: { providerID: "anthropic", modelID: "claude", variant: "high" },
context: {
items: [
{
type: "file",
path: "src/app.ts",
selection: { startLine: 1, startChar: 0, endLine: 2, endChar: 3 },
comment: "Check this",
key: expect.stringMatching(/^file:src\/app\.ts:1:2:c=/),
},
],
},
})
expect(parseComposerStore("not an object")).toBeUndefined()
})
})
-506
View File
@@ -1,506 +0,0 @@
import { checksum } from "@opencode-ai/util/encode"
import { batch, type Accessor } from "solid-js"
import { createStore, type SetStoreFunction } from "solid-js/store"
import type { FileSelection } from "@/context/file"
import { Persist, persisted } from "@/utils/persist"
import { ServerScope } from "@/utils/server-scope"
import type { BlobReference } from "@/utils/draft-store"
import type { Platform } from "@/context/platform"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Skill } from "@opencode-ai/schema/skill"
interface PartBase {
content: string
start: number
end: number
}
type FilePartSourceText = { value: string; start: number; end: number }
type FilePartSource =
| { text: FilePartSourceText; type: "file"; path: string }
| {
text: FilePartSourceText
type: "symbol"
path: string
range: { start: { line: number; character: number }; end: { line: number; character: number } }
name: string
kind: number
}
| { text: FilePartSourceText; type: "resource"; clientName: string; uri: string }
export interface TextPart extends PartBase {
type: "text"
}
export interface FileAttachmentPart extends PartBase {
type: "file"
path: string
selection?: FileSelection
mime?: string
filename?: string
url?: string
source?: FilePartSource
}
export interface AgentPart extends PartBase {
type: "agent"
name: string
}
export interface SkillPart extends PartBase {
type: "skill"
id: Skill.ID
name: Skill.Name
}
export interface ImageAttachmentPart {
type: "image"
id: string
filename: string
sourcePath?: string
mime: string
blob: BlobReference
}
export type ContentPart = TextPart | FileAttachmentPart | AgentPart | SkillPart | ImageAttachmentPart
export type Prompt = ContentPart[]
export type PromptModel = {
providerID: string
modelID: string
variant?: string | null
}
export type FileContextItem = {
type: "file"
path: string
selection?: FileSelection
comment?: string
commentID?: string
commentOrigin?: "review" | "file"
preview?: string
}
export type ContextItem = FileContextItem
export type PromptScope = { draftID: string } | { dir: string; id?: string }
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
export type ComposerStore = {
prompt: Prompt
cursor?: number
model?: PromptModel
mode?: "normal" | "shell"
retry?: {
id: SessionMessage.ID
agent: string
providerID: string
modelID: string
variant?: string
}
context: {
items: (ContextItem & { key: string })[]
}
}
type InitialPrompt = {
prompt?: string
model?: PromptModel
}
function cloneSelection(selection?: FileSelection) {
if (!selection) return undefined
return { ...selection }
}
function clonePart(part: ContentPart): ContentPart {
if (part.type === "text") return { ...part }
if (part.type === "image") return { ...part }
if (part.type === "agent") return { ...part }
if (part.type === "skill") return { ...part }
return {
...part,
selection: cloneSelection(part.selection),
}
}
function clonePrompt(prompt: Prompt): Prompt {
return prompt.map(clonePart)
}
function contextItemKey(item: ContextItem) {
if (item.type !== "file") return item.type
const start = item.selection?.startLine
const end = item.selection?.endLine
const key = `${item.type}:${item.path}:${start}:${end}`
if (item.commentID) return `${key}:c=${item.commentID}`
const comment = item.comment?.trim()
if (!comment) return key
const digest = checksum(comment) ?? comment
return `${key}:c=${digest.slice(0, 8)}`
}
export function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
return item.type === "file" && !!item.comment?.trim()
}
function createComposerActions(setStore: SetStoreFunction<ComposerStore>) {
return {
set(prompt: Prompt, cursorPosition?: number) {
const next = clonePrompt(prompt)
batch(() => {
setStore("prompt", next)
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
setStore("retry", undefined)
})
},
reset() {
batch(() => {
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
setStore("cursor", 0)
setStore("retry", undefined)
})
},
}
}
function composerTarget(serverScope: ServerScope, scope: PromptScope) {
const target =
"draftID" in scope
? Persist.prompt(Persist.draft(scope.draftID, "prompt"))
: Persist.prompt({
...Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt"),
...(serverScope === ServerScope.local
? { previousKey: `${scope.dir}/prompt${scope.id ? "/" + scope.id : ""}.v2` }
: {}),
})
return { ...target, migrate: parseComposerStore }
}
function initialComposerStore(initial?: InitialPrompt): ComposerStore {
const text = initial?.prompt
return {
prompt:
text === undefined ? clonePrompt(DEFAULT_PROMPT) : [{ type: "text", content: text, start: 0, end: text.length }],
cursor: text === undefined ? undefined : text.length,
model: initial?.model ? { ...initial.model } : undefined,
context: {
items: [],
},
}
}
export function parseComposerStore(value: unknown): ComposerStore | undefined {
if (!record(value)) return
const prompt = Array.isArray(value.prompt) ? value.prompt.flatMap(parsePart) : []
const context = record(value.context) && Array.isArray(value.context.items) ? value.context.items : []
const model = parseModel(value.model)
const retry = parseRetry(value.retry)
return {
prompt: prompt.length ? prompt : clonePrompt(DEFAULT_PROMPT),
...(typeof value.cursor === "number" && Number.isFinite(value.cursor) ? { cursor: Math.max(0, value.cursor) } : {}),
...(model ? { model } : {}),
...(value.mode === "normal" || value.mode === "shell" ? { mode: value.mode } : {}),
...(retry ? { retry } : {}),
context: {
items: context.flatMap((item) => {
const parsed = parseContextItem(item)
return parsed ? [{ ...parsed, key: contextItemKey(parsed) }] : []
}),
},
}
}
function parseRetry(value: unknown): ComposerStore["retry"] {
if (
!record(value) ||
typeof value.id !== "string" ||
!value.id.startsWith("msg_") ||
typeof value.agent !== "string" ||
typeof value.providerID !== "string" ||
typeof value.modelID !== "string"
) {
return
}
return {
id: SessionMessage.ID.make(value.id),
agent: value.agent,
providerID: value.providerID,
modelID: value.modelID,
...(typeof value.variant === "string" ? { variant: value.variant } : {}),
}
}
function parsePart(value: unknown): ContentPart[] {
if (!record(value) || typeof value.type !== "string") return []
if (value.type === "image") {
const legacy = typeof value.dataUrl === "string" ? value.dataUrl : undefined
const blobID = record(value.blob) && typeof value.blob.id === "string" ? value.blob.id : legacy
const hydrated = record(value.blob) && typeof value.blob.url === "string" ? value.blob.url : undefined
const blobURL =
hydrated?.startsWith("blob:") || hydrated?.startsWith("data:")
? hydrated
: blobID?.startsWith("data:")
? blobID
: undefined
if (
typeof value.id !== "string" ||
typeof value.filename !== "string" ||
typeof value.mime !== "string" ||
!blobID ||
!blobURL
) {
return []
}
return [
{
type: "image",
id: value.id,
filename: value.filename,
mime: value.mime,
blob: { id: blobID, url: blobURL },
...(typeof value.sourcePath === "string" ? { sourcePath: value.sourcePath } : {}),
},
]
}
if (typeof value.content !== "string" || typeof value.start !== "number" || typeof value.end !== "number") return []
if (value.type === "text") return [{ type: "text", content: value.content, start: value.start, end: value.end }]
if (value.type === "agent" && typeof value.name === "string") {
return [{ type: "agent", name: value.name, content: value.content, start: value.start, end: value.end }]
}
if (value.type === "skill" && typeof value.id === "string" && typeof value.name === "string") {
return [
{
type: "skill",
id: Skill.ID.make(value.id),
name: Skill.Name.make(value.name),
content: value.content,
start: value.start,
end: value.end,
},
]
}
if (value.type !== "file" || typeof value.path !== "string") return []
const selection = parseSelection(value.selection)
const source = parseSource(value.source)
return [
{
type: "file",
path: value.path,
content: value.content,
start: value.start,
end: value.end,
...(typeof value.mime === "string" ? { mime: value.mime } : {}),
...(typeof value.filename === "string" ? { filename: value.filename } : {}),
...(typeof value.url === "string" ? { url: value.url } : {}),
...(selection ? { selection } : {}),
...(source ? { source } : {}),
},
]
}
function parseContextItem(value: unknown): ContextItem | undefined {
if (!record(value) || value.type !== "file" || typeof value.path !== "string") return
const selection = parseSelection(value.selection)
const origin = value.commentOrigin === "review" || value.commentOrigin === "file" ? value.commentOrigin : undefined
return {
type: "file",
path: value.path,
...(selection ? { selection } : {}),
...(typeof value.comment === "string" ? { comment: value.comment } : {}),
...(typeof value.commentID === "string" ? { commentID: value.commentID } : {}),
...(origin ? { commentOrigin: origin } : {}),
...(typeof value.preview === "string" ? { preview: value.preview } : {}),
}
}
function parseModel(value: unknown): PromptModel | undefined {
if (!record(value) || typeof value.providerID !== "string" || typeof value.modelID !== "string") return
return {
providerID: value.providerID,
modelID: value.modelID,
...(typeof value.variant === "string" || value.variant === null ? { variant: value.variant } : {}),
}
}
function parseSelection(value: unknown): FileSelection | undefined {
if (!record(value)) return
if (
typeof value.startLine !== "number" ||
typeof value.startChar !== "number" ||
typeof value.endLine !== "number" ||
typeof value.endChar !== "number"
) {
return
}
return {
startLine: value.startLine,
startChar: value.startChar,
endLine: value.endLine,
endChar: value.endChar,
}
}
function parseSource(value: unknown): FilePartSource | undefined {
if (!record(value) || !record(value.text)) return
if (
typeof value.text.value !== "string" ||
typeof value.text.start !== "number" ||
typeof value.text.end !== "number"
) {
return
}
const text = { value: value.text.value, start: value.text.start, end: value.text.end }
if (value.type === "file" && typeof value.path === "string") return { type: "file", path: value.path, text }
if (value.type === "resource" && typeof value.clientName === "string" && typeof value.uri === "string") {
return { type: "resource", clientName: value.clientName, uri: value.uri, text }
}
if (
value.type !== "symbol" ||
typeof value.path !== "string" ||
typeof value.name !== "string" ||
typeof value.kind !== "number" ||
!record(value.range) ||
!record(value.range.start) ||
!record(value.range.end) ||
typeof value.range.start.line !== "number" ||
typeof value.range.start.character !== "number" ||
typeof value.range.end.line !== "number" ||
typeof value.range.end.character !== "number"
) {
return
}
return {
type: "symbol",
path: value.path,
name: value.name,
kind: value.kind,
text,
range: {
start: { line: value.range.start.line, character: value.range.start.character },
end: { line: value.range.end.line, character: value.range.end.character },
},
}
}
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
function createComposerStateValue(store: ComposerStore, setStore: SetStoreFunction<ComposerStore>) {
const actions = createComposerActions(setStore)
const clearRetry = () => setStore("retry", undefined)
const value = {
store: [() => store, setStore] as [Accessor<ComposerStore>, SetStoreFunction<ComposerStore>],
current: () => store.prompt,
cursor: () => store.cursor,
model: {
current: () => store.model,
set: (model: PromptModel | undefined) => {
setStore("model", model)
clearRetry()
},
},
mode: {
current: () => store.mode ?? "normal",
set: (mode: "normal" | "shell") => {
setStore("mode", mode)
clearRetry()
},
},
retry: {
current: () => store.retry,
set: (retry: NonNullable<ComposerStore["retry"]>) => setStore("retry", retry),
},
context: {
items: () => store.context.items,
add(item: ContextItem) {
const key = contextItemKey(item)
if (store.context.items.find((x) => x.key === key)) return
setStore("context", "items", (items) => [...items, { key, ...item }])
clearRetry()
},
remove(key: string) {
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
clearRetry()
},
removeComment(path: string, commentID: string) {
setStore("context", "items", (items) =>
items.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
)
clearRetry()
},
updateComment(path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) {
setStore("context", "items", (items) =>
items.map((item) => {
if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
const value = { ...item, ...next }
return { ...value, key: contextItemKey(value) }
}),
)
clearRetry()
},
replaceComments(items: FileContextItem[]) {
setStore("context", "items", (current) => [
...current.filter((item) => !isCommentItem(item)),
...items.map((item) => ({ ...item, key: contextItemKey(item) })),
])
clearRetry()
},
},
set: (prompt: Prompt, cursorPosition?: number) => actions.set(prompt, cursorPosition),
reset: () => actions.reset(),
capture: () => value,
}
return value
}
function createPersistedComposer(
target: ReturnType<typeof composerTarget>,
initial?: InitialPrompt,
platform?: Platform,
) {
const [store, setStore, _, ready] = persisted(
target,
createStore<ComposerStore>(initialComposerStore(initial)),
platform,
)
return { ready, ...createComposerStateValue(store, setStore) }
}
export function createComposerState(
serverScope: ServerScope,
scope: PromptScope,
initial?: InitialPrompt,
platform?: Platform,
) {
return createPersistedComposer(composerTarget(serverScope, scope), initial, platform)
}
export function createDraftComposerState(draftID: string, initial?: InitialPrompt) {
return createPersistedComposer(
{
...Persist.prompt(Persist.draft(draftID, "prompt")),
migrate: parseComposerStore,
},
initial,
)
}
export type ComposerState = ReturnType<typeof createComposerState>
export function createComposerReady(session: Accessor<ComposerState>) {
return Object.defineProperty(() => session().ready(), "promise", {
get: () => session().ready.promise,
}) as (() => boolean) & { readonly promise: Promise<unknown> | undefined }
}
export function createMemoryComposerState(initial?: InitialPrompt) {
const [store, setStore] = createStore<ComposerStore>(initialComposerStore(initial))
const ready = Object.assign(() => true, { promise: Promise.resolve(true) })
return {
ready,
...createComposerStateValue(store, setStore),
}
}
-364
View File
@@ -1,364 +0,0 @@
import { describe, expect, test } from "bun:test"
import type { ModelSelection } from "@/context/local"
import { Skill } from "@opencode-ai/schema/skill"
import type { ActiveComposerAdapter, ComposerControls, ComposerSession, NewSessionComposerAdapter } from "./adapter"
import { createMemoryComposerState } from "./state"
import { createComposerSubmit } from "./submit"
const selectedModel = {
id: "model-1",
name: "Model 1",
provider: { id: "provider-1" },
} as NonNullable<ReturnType<ModelSelection["current"]>>
const selection = {
ready: Object.assign(() => true, { promise: undefined }),
current: () => selectedModel,
recent: () => [selectedModel],
list: () => [selectedModel],
cycle() {},
set() {},
visible: () => true,
setVisibility() {},
variant: {
configured: () => undefined,
selected: () => "balanced",
current: () => "balanced",
list: () => ["balanced"],
set() {},
cycle() {},
},
} satisfies ModelSelection
function controls(): ComposerControls {
return {
agents: {
available: [{ name: "build", mode: "primary" }],
options: ["build"],
current: "build",
visible: true,
select() {},
},
model: { selection, paid: true, loading: false },
session: {
tabs: { active: () => undefined, all: () => [], open() {}, setActive() {} },
reviewPanel: { opened: () => false, open() {} },
},
}
}
function submitInput(
adapter: ActiveComposerAdapter | NewSessionComposerAdapter,
notify = { missingSelection() {}, failed(_kind: "shell" | "command" | "prompt", _error: unknown) {} },
mode: "normal" | "shell" = "normal",
) {
return createComposerSubmit({
adapter,
mode: () => mode,
editor: () => undefined,
queueScroll() {},
addToHistory() {},
resetHistory() {},
setMode() {},
closePopover() {},
notify,
comments: { capture: () => [], clear() {}, restore() {} },
})
}
function session(input: {
calls: string[]
prompt: (value: Parameters<ComposerSession["data"]["session"]["prompt"]>[0]) => Promise<void>
current?: ComposerSession["current"]
admitted?: (messageID: string) => boolean
shell?: () => Promise<unknown>
command?: ComposerSession["api"]["command"]
}): ComposerSession {
return {
id: "session-1",
directory: "C:/repo",
current: input.current ?? (() => undefined),
admitted: input.admitted ?? (() => false),
api: {
switchAgent: async () => {
input.calls.push("switch-agent")
},
switchModel: async () => {
input.calls.push("switch-model")
},
shell: input.shell ?? (async () => undefined),
command: input.command ?? (async () => undefined),
},
data: {
location: { command: { list: () => [] } },
session: {
prompt: async (value) => {
input.calls.push("prompt")
await input.prompt(value)
},
},
},
}
}
describe("Composer submission", () => {
test("sends one captured value with explicit delivery after selection switches", async () => {
const state = createMemoryComposerState({ prompt: "ship it" }).capture()
const calls: string[] = []
const admitted = Promise.withResolvers<Parameters<ComposerSession["data"]["session"]["prompt"]>[0]>()
const target = session({
calls,
current: () => ({ agent: "plan", model: { id: "old", providerID: "old" } }),
prompt: async (value) => admitted.resolve(value),
})
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => target,
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
await submitInput(adapter).submit(new Event("submit"))
const request = await admitted.promise
expect(calls).toEqual(["switch-agent", "switch-model", "prompt"])
expect(request.delivery).toBe("steer")
expect(request.text).toBe("ship it")
expect(request.id).toMatch(/^msg_/)
expect(request.metadata).toMatchObject({
displayText: "ship it",
agent: "build",
model: { providerID: "provider-1", modelID: "model-1", variant: "balanced" },
})
expect(state.current()).toEqual([{ type: "text", content: "", start: 0, end: 0 }])
})
test("starts and promotes a New Session once before admitting its first prompt", async () => {
const draft = createMemoryComposerState({ prompt: "first prompt" }).capture()
const promoted = createMemoryComposerState().capture()
const calls: string[] = []
const admitted = Promise.withResolvers<Parameters<ComposerSession["data"]["session"]["prompt"]>[0]>()
const target = session({ calls, prompt: async (value) => admitted.resolve(value) })
const adapter: NewSessionComposerAdapter = {
kind: "new-session",
state: draft,
ready: () => true,
controls,
working: () => false,
submitted() {
calls.push("submitted")
},
async start(_selection, submission) {
calls.push("start")
submission.retarget(promoted)
return target
},
}
await submitInput(adapter).submit(new Event("submit"))
const request = await admitted.promise
expect(calls).toEqual(["start", "submitted", "switch-agent", "switch-model", "prompt"])
expect(request.delivery).toBe("steer")
expect(request.text).toBe("first prompt")
expect(draft.current()).toEqual([{ type: "text", content: "", start: 0, end: 0 }])
expect(promoted.current()).toEqual([{ type: "text", content: "", start: 0, end: 0 }])
})
test("does not restore a prompt already acknowledged by the durable inbox", async () => {
const state = createMemoryComposerState({ prompt: "admitted prompt" }).capture()
const checked = Promise.withResolvers<void>()
const attempts: string[] = []
const target = session({
calls: [],
admitted: () => {
checked.resolve()
return true
},
prompt: async (value) => {
attempts.push(value.id ?? "")
throw new Error("response lost")
},
})
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => target,
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
await submitInput(adapter).submit(new Event("submit"))
await checked.promise
expect(state.current()).toEqual([{ type: "text", content: "", start: 0, end: 0 }])
expect(attempts).toHaveLength(2)
expect(new Set(attempts).size).toBe(1)
})
test("restores first-prompt comments into the promoted Session", async () => {
const draft = createMemoryComposerState({ prompt: "first prompt" }).capture()
draft.store[1]("context", "items", [
{
key: "file:src/app.ts:1:1:comment",
type: "file",
path: "src/app.ts",
comment: "Keep this comment",
selection: { startLine: 1, startChar: 0, endLine: 1, endChar: 4 },
},
])
expect(draft.context.items()).toHaveLength(1)
const promoted = createMemoryComposerState().capture()
const failed = Promise.withResolvers<void>()
const target = session({
calls: [],
prompt: async () => undefined,
shell: async () => Promise.reject(new Error("send failed")),
})
const adapter: NewSessionComposerAdapter = {
kind: "new-session",
state: draft,
ready: () => true,
controls,
working: () => false,
submitted() {},
async start(_selection, submission) {
submission.retarget(promoted)
return target
},
}
await submitInput(adapter, { missingSelection() {}, failed: () => failed.resolve() }, "shell").submit(
new Event("submit"),
)
await failed.promise
expect(promoted.current()).toMatchObject([{ type: "text", content: "first prompt" }])
expect(promoted.context.items()).toMatchObject([{ type: "file", path: "src/app.ts", comment: "Keep this comment" }])
expect(promoted.mode.current()).toBe("shell")
})
test("reuses the message ID when an unacknowledged admission is retried", async () => {
const state = createMemoryComposerState({ prompt: "retry me" }).capture()
const attempts: string[] = []
const first = Promise.withResolvers<void>()
const second = Promise.withResolvers<void>()
const target = session({
calls: [],
prompt: async (value) => {
attempts.push(value.id ?? "")
throw new Error("network unavailable")
},
})
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => target,
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
const notify = {
missingSelection() {},
failed: () => (attempts.length === 2 ? first.resolve() : second.resolve()),
}
const submission = submitInput(adapter, notify)
await submission.submit(new Event("submit"))
await first.promise
await submission.submit(new Event("submit"))
await second.promise
expect(attempts).toHaveLength(4)
expect(new Set(attempts).size).toBe(1)
expect(state.current()).toMatchObject([{ type: "text", content: "retry me" }])
})
test("forwards structured mentions to custom commands", async () => {
const state = createMemoryComposerState().capture()
state.set([
{ type: "text", content: "/review ", start: 0, end: 8 },
{ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 8, end: 19 },
{ type: "text", content: " ", start: 19, end: 20 },
{ type: "agent", name: "review", content: "@review", start: 20, end: 27 },
{ type: "text", content: " ", start: 27, end: 28 },
{
type: "skill",
id: Skill.ID.make("effect"),
name: Skill.Name.make("Effect"),
content: "@effect",
start: 28,
end: 35,
},
])
const sent = Promise.withResolvers<Parameters<ComposerSession["api"]["command"]>[0]>()
const target = session({
calls: [],
prompt: async () => undefined,
command: async (value) => sent.resolve(value),
})
target.data.location.command.list = () => [{ name: "review", description: "Review changes", template: "" }]
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => target,
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
await submitInput(adapter).submit(new Event("submit"))
const request = await sent.promise
expect(request.files).toMatchObject([{ name: "app.ts", mention: { text: "@src/app.ts" } }])
expect(request.agents).toMatchObject([{ name: "review", mention: { text: "@review" } }])
expect(request.skills).toMatchObject([{ id: "effect", name: "Effect", mention: { text: "@effect" } }])
expect(request.delivery).toBe("steer")
})
test("does not run an empty shell command from hidden attachments", async () => {
const state = createMemoryComposerState().capture()
state.set([
{ type: "text", content: "", start: 0, end: 0 },
{
type: "image",
id: "attachment",
filename: "notes.txt",
mime: "text/plain",
blob: { id: "attachment", url: "data:text/plain;base64,bm90ZXM=" },
},
])
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => {
throw new Error("shell should not run")
},
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
await submitInput(adapter, undefined, "shell").submit(new Event("submit"))
expect(state.current().some((part) => part.type === "image")).toBe(true)
})
})
-323
View File
@@ -1,323 +0,0 @@
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Event } from "@opencode-ai/schema/event"
import type { Accessor } from "solid-js"
import { clonePromptParts, type PromptHistoryComment } from "./history/entry"
import type { ImageAttachmentPart, Prompt } from "./state"
import type { ComposerAdapter, ComposerSelection, ComposerSession } from "./adapter"
import { createComposerSubmission } from "./submission-state"
import { buildPromptRequest } from "./request"
import { setCursorPosition } from "./editor/dom"
import { blobDataUrl } from "@/utils/draft-store"
const submitting = new WeakSet<object>()
type ComposerSubmission = {
id: SessionMessage.ID
mode: "normal" | "shell"
prompt: Prompt
context: ReturnType<ComposerAdapter["state"]["context"]["items"]>
text: string
images: ImageAttachmentPart[]
selection: ComposerSelection
delivery: "steer"
}
type ComposerSubmitInput = {
adapter: ComposerAdapter
mode: Accessor<"normal" | "shell">
editor: () => HTMLDivElement | undefined
queueScroll: () => void
addToHistory: (prompt: Prompt, mode: "normal" | "shell") => void
resetHistory: () => void
setMode: (mode: "normal" | "shell") => void
closePopover: () => void
notify: {
missingSelection: () => void
failed: (kind: "shell" | "command" | "prompt", error: unknown) => void
}
comments: {
capture: () => PromptHistoryComment[]
clear: () => void
restore: (comments: PromptHistoryComment[]) => void
}
}
export function createComposerSubmit(input: ComposerSubmitInput) {
const submit = async (event: globalThis.Event) => {
event.preventDefault()
const submission = createComposerSubmission({
target: input.adapter.state,
prompt: clonePromptParts(input.adapter.state.current()),
context: input.adapter.state.context.items().map((item) => ({
...item,
selection: item.selection ? { ...item.selection } : undefined,
})),
})
const value = readSubmission(input, submission.prompt, submission.context)
if (!value) {
if (input.adapter.working() && input.adapter.kind === "active-session") void input.adapter.interrupt()
return
}
if (submitting.has(input.adapter.state)) return
submitting.add(input.adapter.state)
const comments = input.comments.capture()
try {
const session =
input.adapter.kind === "active-session"
? input.adapter.session()
: await input.adapter.start(value.selection, submission)
if (!session) return
input.addToHistory(value.prompt, value.mode)
input.resetHistory()
const restore = () => restoreSubmission(input, submission, value, comments)
input.adapter.submitted()
if (value.mode === "shell") {
clearSubmission(input, submission)
void sendShell(session, value).catch((error) => failSubmission(input, session, "shell", error, restore))
return
}
const command = findCommand(session, value.text)
if (command) {
clearSubmission(input, submission)
void sendCommand(session, value, command).catch((error) =>
failSubmission(input, session, "command", error, restore, value.id),
)
return
}
submission.context
.filter((item) => !!item.comment?.trim())
.forEach((item) => submission.target().context.remove(item.key))
input.comments.clear()
clearSubmission(input, submission)
void sendPrompt(session, value).catch((error) =>
failSubmission(input, session, "prompt", error, restore, value.id),
)
} finally {
submitting.delete(input.adapter.state)
}
}
return {
submit,
stop: () => (input.adapter.kind === "active-session" ? input.adapter.interrupt() : Promise.resolve()),
}
}
function readSubmission(
input: ComposerSubmitInput,
prompt: Prompt,
context: ComposerSubmission["context"],
): ComposerSubmission | undefined {
const text = prompt.map((part) => ("content" in part ? part.content : "")).join("")
const mode = input.mode()
if (mode === "shell" && !text.trim()) return
const images = prompt.filter((part): part is ImageAttachmentPart => part.type === "image")
const comments = context.filter((item) => !!item.comment?.trim()).length
if (!text.trim() && images.length === 0 && comments === 0) return
const controls = input.adapter.controls()
const model = controls.model.selection.current()
const agent = controls.agents.current
if (!model || !agent) {
input.notify.missingSelection()
return
}
const variant = controls.model.selection.variant.current()
const retry = input.adapter.state.retry.current()
const retryID =
retry &&
retry.agent === agent &&
retry.providerID === model.provider.id &&
retry.modelID === model.id &&
(retry.variant ?? "default") === (variant ?? "default")
? retry.id
: undefined
return {
id: retryID ?? SessionMessage.ID.create(),
mode,
prompt,
context,
text,
images,
selection: {
agent,
model: { modelID: model.id, providerID: model.provider.id },
variant,
},
delivery: "steer",
}
}
function clearSubmission(input: ComposerSubmitInput, submission: ReturnType<typeof createComposerSubmission>) {
submission.clear()
submission.target().mode.set("normal")
input.setMode("normal")
input.closePopover()
}
function restoreSubmission(
input: ComposerSubmitInput,
submission: ReturnType<typeof createComposerSubmission>,
value: ComposerSubmission,
comments: PromptHistoryComment[],
) {
const restored = submission.restore()
if (!restored) return false
restored.target.set(restored.prompt, promptLength(restored.prompt))
restored.target.mode.set(value.mode)
restored.target.context.replaceComments(
restored.context
.filter((item) => !!item.comment?.trim())
.map((item) => ({
type: "file",
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
commentOrigin: item.commentOrigin,
preview: item.preview,
})),
)
if (value.mode === "normal") {
restored.target.retry.set({
id: value.id,
agent: value.selection.agent,
providerID: value.selection.model.providerID,
modelID: value.selection.model.modelID,
variant: value.selection.variant,
})
}
if (!submission.current(input.adapter.state)) return true
input.comments.restore(comments)
input.setMode(value.mode)
input.closePopover()
requestAnimationFrame(() => {
const editor = input.editor()
if (!editor) return
editor.focus()
setCursorPosition(editor, promptLength(value.prompt))
input.queueScroll()
})
return true
}
async function sendShell(session: ComposerSession, value: ComposerSubmission) {
await session.api.shell({ sessionID: session.id, id: Event.ID.create(), command: value.text })
}
function findCommand(session: ComposerSession, text: string) {
if (!text.startsWith("/")) return
const [name, ...arguments_] = text.split(" ")
const command = name.slice(1)
if (!session.data.location.command.list({ directory: session.directory })?.some((item) => item.name === command))
return
return { command, arguments: arguments_.join(" ") }
}
async function sendCommand(
session: ComposerSession,
value: ComposerSubmission,
command: { command: string; arguments: string },
) {
const request = await buildSubmissionRequest(session, value)
await session.api.command({
sessionID: session.id,
id: value.id,
command: command.command,
arguments: command.arguments,
agent: value.selection.agent,
model: {
id: value.selection.model.modelID,
providerID: value.selection.model.providerID,
variant: value.selection.variant,
},
files: request.files.map((file) => ({ uri: file.uri, name: file.name, mention: file.mention })),
agents: request.agents,
skills: request.skills,
delivery: value.delivery,
})
}
async function sendPrompt(session: ComposerSession, value: ComposerSubmission) {
const request = await buildSubmissionRequest(session, value)
const current = session.current()
if (current?.agent !== value.selection.agent) {
await session.api.switchAgent({ sessionID: session.id, agent: value.selection.agent })
}
if (
current?.model?.providerID !== value.selection.model.providerID ||
current.model.id !== value.selection.model.modelID ||
(current.model.variant ?? "default") !== (value.selection.variant ?? "default")
) {
await session.api.switchModel({
sessionID: session.id,
model: {
id: value.selection.model.modelID,
providerID: value.selection.model.providerID,
variant: value.selection.variant,
},
})
}
const admission = {
id: value.id,
sessionID: session.id,
delivery: value.delivery,
text: request.text,
files: request.files.map((file) => ({ uri: file.uri, name: file.name, mention: file.mention })),
agents: request.agents,
skills: request.skills,
metadata: {
displayText: request.displayText,
comments: request.comments,
agent: value.selection.agent,
model: {
...value.selection.model,
...(value.selection.variant ? { variant: value.selection.variant } : {}),
},
},
}
await session.data.session.prompt(admission).catch(() => session.data.session.prompt(admission))
}
async function buildSubmissionRequest(session: ComposerSession, value: ComposerSubmission) {
const images = await Promise.all(
value.images.map(async (attachment) => ({
...attachment,
dataUrl: await blobDataUrl(attachment.blob, attachment.mime),
})),
)
const request = buildPromptRequest({
prompt: value.prompt,
context: value.context,
images,
text: value.text,
sessionDirectory: session.directory,
})
return request
}
function failSubmission(
input: ComposerSubmitInput,
session: ComposerSession,
kind: "shell" | "command" | "prompt",
error: unknown,
restore: () => boolean,
messageID?: string,
) {
if (messageID && session.admitted(messageID)) return
restore()
input.notify.failed(kind, error)
}
function promptLength(prompt: Prompt) {
return prompt.reduce((length, part) => length + ("content" in part ? part.content.length : 0), 0)
}
-40
View File
@@ -1,40 +0,0 @@
import type { AgentPart, ComposerStore, FileAttachmentPart, ImageAttachmentPart, Prompt, SkillPart } from "./state"
export type ComposerFilePart = FileAttachmentPart
export type ComposerAgentPart = AgentPart
export type ComposerSkillPart = SkillPart
export type ComposerAttachment = ImageAttachmentPart
export type ComposerPrompt = Prompt
export type ComposerComment = ComposerStore["context"]["items"][number]
export type ComposerPersistedState = ComposerStore
export type ComposerHistoryEntry = {
prompt: ComposerPrompt
metadata?: unknown
}
export type ComposerHistory = {
entries: (mode: "normal" | "shell") => ComposerHistoryEntry[]
add: (prompt: ComposerPrompt, mode: "normal" | "shell") => void
capture?: () => unknown
restore?: (metadata: unknown) => void
}
export type ComposerOption = {
id: string
label: string
providerID?: string
}
export type ComposerSuggestion = {
id: string
kind: "agent" | "command" | "file" | "reference" | "resource" | "skill"
label: string
title?: string
trigger?: string
description?: string
path?: string
keybind?: string[]
recent?: boolean
mention?: ComposerFilePart | ComposerAgentPart | ComposerSkillPart
}
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { createPromptState, DEFAULT_PROMPT } from "./prompt-state"
describe("prompt state initialization", () => {
test("initializes prompt text, cursor, and model together", () => {
createRoot((dispose) => {
const model = { providerID: "anthropic", modelID: "claude", variant: "high" }
const prompt = createPromptState({ prompt: "hello", model })
expect(prompt.current()).toEqual([{ type: "text", content: "hello", start: 0, end: 5 }])
expect(prompt.cursor()).toBe(5)
expect(prompt.model.current()).toEqual(model)
expect(prompt.model.current()).not.toBe(model)
dispose()
})
})
test("uses the default prompt without initial values", () => {
createRoot((dispose) => {
const prompt = createPromptState()
expect(prompt.current()).toEqual(DEFAULT_PROMPT)
expect(prompt.cursor()).toBeUndefined()
expect(prompt.model.current()).toBeUndefined()
dispose()
})
})
})
+289
View File
@@ -0,0 +1,289 @@
import { checksum } from "@opencode-ai/util/encode"
import { batch, createMemo, type Accessor } from "solid-js"
import { createStore, type SetStoreFunction } from "solid-js/store"
import type { FileSelection } from "@/context/file"
import { Persist, persisted } from "@/utils/persist"
import { ServerScope } from "@/utils/server-scope"
import type { BlobReference } from "@/utils/draft-store"
import type { Platform } from "@/context/platform"
interface PartBase {
content: string
start: number
end: number
}
type FilePartSourceText = { value: string; start: number; end: number }
type FilePartSource =
| { text: FilePartSourceText; type: "file"; path: string }
| {
text: FilePartSourceText
type: "symbol"
path: string
range: { start: { line: number; character: number }; end: { line: number; character: number } }
name: string
kind: number
}
| { text: FilePartSourceText; type: "resource"; clientName: string; uri: string }
export interface TextPart extends PartBase {
type: "text"
}
export interface FileAttachmentPart extends PartBase {
type: "file"
path: string
selection?: FileSelection
mime?: string
filename?: string
url?: string
source?: FilePartSource
}
export interface AgentPart extends PartBase {
type: "agent"
name: string
}
export interface ImageAttachmentPart {
type: "image"
id: string
filename: string
sourcePath?: string
mime: string
blob: BlobReference
}
export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart
export type Prompt = ContentPart[]
export type PromptModel = {
providerID: string
modelID: string
variant?: string | null
}
export type FileContextItem = {
type: "file"
path: string
selection?: FileSelection
comment?: string
commentID?: string
commentOrigin?: "review" | "file"
preview?: string
}
export type ContextItem = FileContextItem
export type PromptScope = { draftID: string } | { dir: string; id?: string }
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
export type PromptStore = {
prompt: Prompt
cursor?: number
model?: PromptModel
context: {
items: (ContextItem & { key: string })[]
}
}
type InitialPrompt = {
prompt?: string
model?: PromptModel
}
function isSelectionEqual(a?: FileSelection, b?: FileSelection) {
if (!a && !b) return true
if (!a || !b) return false
return (
a.startLine === b.startLine && a.startChar === b.startChar && a.endLine === b.endLine && a.endChar === b.endChar
)
}
function isPartEqual(partA: ContentPart, partB: ContentPart) {
switch (partA.type) {
case "text":
return partB.type === "text" && partA.content === partB.content
case "file":
return (
partB.type === "file" &&
partA.path === partB.path &&
partA.mime === partB.mime &&
partA.filename === partB.filename &&
isSelectionEqual(partA.selection, partB.selection)
)
case "agent":
return partB.type === "agent" && partA.name === partB.name
case "image":
return partB.type === "image" && partA.id === partB.id
}
}
export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
if (promptA.length !== promptB.length) return false
for (let i = 0; i < promptA.length; i++) {
if (!isPartEqual(promptA[i], promptB[i])) return false
}
return true
}
function cloneSelection(selection?: FileSelection) {
if (!selection) return undefined
return { ...selection }
}
function clonePart(part: ContentPart): ContentPart {
if (part.type === "text") return { ...part }
if (part.type === "image") return { ...part }
if (part.type === "agent") return { ...part }
return {
...part,
selection: cloneSelection(part.selection),
}
}
function clonePrompt(prompt: Prompt): Prompt {
return prompt.map(clonePart)
}
function contextItemKey(item: ContextItem) {
if (item.type !== "file") return item.type
const start = item.selection?.startLine
const end = item.selection?.endLine
const key = `${item.type}:${item.path}:${start}:${end}`
if (item.commentID) return `${key}:c=${item.commentID}`
const comment = item.comment?.trim()
if (!comment) return key
const digest = checksum(comment) ?? comment
return `${key}:c=${digest.slice(0, 8)}`
}
export function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
return item.type === "file" && !!item.comment?.trim()
}
function createPromptActions(setStore: SetStoreFunction<PromptStore>) {
return {
set(prompt: Prompt, cursorPosition?: number) {
const next = clonePrompt(prompt)
batch(() => {
setStore("prompt", next)
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
})
},
reset() {
batch(() => {
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
setStore("cursor", 0)
})
},
}
}
function promptTarget(serverScope: ServerScope, scope: PromptScope) {
if ("draftID" in scope) return Persist.prompt(Persist.draft(scope.draftID, "prompt"))
return Persist.prompt({
...Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt"),
...(serverScope === ServerScope.local
? { previousKey: `${scope.dir}/prompt${scope.id ? "/" + scope.id : ""}.v2` }
: {}),
})
}
function promptStore(initial?: InitialPrompt): PromptStore {
const text = initial?.prompt
return {
prompt:
text === undefined ? clonePrompt(DEFAULT_PROMPT) : [{ type: "text", content: text, start: 0, end: text.length }],
cursor: text === undefined ? undefined : text.length,
model: initial?.model ? { ...initial.model } : undefined,
context: {
items: [],
},
}
}
function createPromptStateValue(store: PromptStore, setStore: SetStoreFunction<PromptStore>) {
const actions = createPromptActions(setStore)
const value = {
store: [() => store, setStore] as [Accessor<PromptStore>, SetStoreFunction<PromptStore>],
current: () => store.prompt,
cursor: createMemo(() => store.cursor),
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
model: {
current: () => store.model,
set: (model: PromptModel | undefined) => setStore("model", model),
},
context: {
items: createMemo(() => store.context.items),
add(item: ContextItem) {
const key = contextItemKey(item)
if (store.context.items.find((x) => x.key === key)) return
setStore("context", "items", (items) => [...items, { key, ...item }])
},
remove(key: string) {
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
},
removeComment(path: string, commentID: string) {
setStore("context", "items", (items) =>
items.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
)
},
updateComment(path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) {
setStore("context", "items", (items) =>
items.map((item) => {
if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
const value = { ...item, ...next }
return { ...value, key: contextItemKey(value) }
}),
)
},
replaceComments(items: FileContextItem[]) {
setStore("context", "items", (current) => [
...current.filter((item) => !isCommentItem(item)),
...items.map((item) => ({ ...item, key: contextItemKey(item) })),
])
},
},
set: actions.set,
reset: actions.reset,
capture: () => value,
}
return value
}
function createPersistedPrompt(target: ReturnType<typeof promptTarget>, initial?: InitialPrompt, platform?: Platform) {
const [store, setStore, _, ready] = persisted(target, createStore<PromptStore>(promptStore(initial)), platform)
return { ready, ...createPromptStateValue(store, setStore) }
}
export function createPromptSession(
serverScope: ServerScope,
scope: PromptScope,
initial?: InitialPrompt,
platform?: Platform,
) {
return createPersistedPrompt(promptTarget(serverScope, scope), initial, platform)
}
export function createDraftPromptSession(draftID: string, initial?: InitialPrompt) {
return createPersistedPrompt(Persist.prompt(Persist.draft(draftID, "prompt")), initial)
}
export type PromptSession = ReturnType<typeof createPromptSession>
export function createPromptReady(session: Accessor<PromptSession>) {
return Object.defineProperty(() => session().ready(), "promise", {
get: () => session().ready.promise,
}) as (() => boolean) & { readonly promise: Promise<unknown> | undefined }
}
export function createPromptState(initial?: InitialPrompt) {
const [store, setStore] = createStore<PromptStore>(promptStore(initial))
const ready = Object.assign(() => true, { promise: Promise.resolve(true) })
return {
ready,
...createPromptStateValue(store, setStore),
}
}
@@ -3,29 +3,30 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useParams, useSearchParams } from "@solidjs/router"
import { createMemo, createResource, createRoot, getOwner, onCleanup } from "solid-js"
import { requireServerKey } from "@/utils/session-route"
import { ServerConnection } from "@/context/servers"
import { useServerSDK } from "@/context/server-sdk"
import { useWorkspaceLocation } from "@/context/location"
import { useTabs, type Tab } from "@/context/tabs"
import { ServerConnection } from "./servers"
import { useServerSDK } from "./server-sdk"
import { useWorkspaceLocation } from "./location"
import { useTabs, type Tab } from "./tabs"
import type { ServerScope } from "@/utils/server-scope"
import {
createComposerReady,
createComposerState,
createPromptReady,
createPromptSession,
type ContextItem,
type FileContextItem,
type Prompt,
type PromptModel,
type PromptScope,
type ComposerState,
} from "./state"
type PromptSession,
} from "./prompt-state"
export {
createComposerReady,
createComposerState,
createMemoryComposerState,
createPromptReady,
createPromptSession,
createPromptState,
DEFAULT_PROMPT,
isCommentItem,
} from "./state"
isPromptEqual,
} from "./prompt-state"
export type {
AgentPart,
ContentPart,
@@ -35,11 +36,11 @@ export type {
ImageAttachmentPart,
Prompt,
PromptModel,
ComposerStore,
PromptStore,
PromptScope,
ComposerState,
PromptSession,
TextPart,
} from "./state"
} from "./prompt-state"
const WORKSPACE_KEY = "__workspace__"
const MAX_PROMPT_SESSIONS = 20
@@ -58,19 +59,19 @@ function scopeKey(scope: PromptScope) {
return `${scope.dir}:${scope.id ?? WORKSPACE_KEY}`
}
type ComposerCacheEntry = {
value: ComposerState
type PromptCacheEntry = {
value: PromptSession
dispose: VoidFunction
}
export const createTabComposerState = (
export const createTabPromptState = (
tabs: ReturnType<typeof useTabs>,
tab: Tab,
...args: Parameters<typeof createComposerState>
) => tabs.state(tab, "prompt", () => createComposerState(...args))
...args: Parameters<typeof createPromptSession>
) => tabs.state(tab, "prompt", () => createPromptSession(...args))
export const { use: useComposerState, provider: ComposerPersistenceProvider } = createSimpleContext({
name: "ComposerState",
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
name: "Prompt",
gate: false,
init: () => {
const params = useParams<{ serverKey?: string; id?: string }>()
@@ -78,7 +79,7 @@ export const { use: useComposerState, provider: ComposerPersistenceProvider } =
const [search] = useSearchParams<{ draftId?: string }>()
const serverSDK = useServerSDK()
const tabs = useTabs()
const cache = new Map<string, ComposerCacheEntry>()
const cache = new Map<string, PromptCacheEntry>()
const disposeAll = () => {
for (const entry of cache.values()) entry.dispose()
@@ -104,7 +105,7 @@ export const { use: useComposerState, provider: ComposerPersistenceProvider } =
search.draftId ? { draftID: search.draftId } : { dir: base64Encode(sdk().directory), id: params.id }
const load = (scope: PromptScope, target?: { server?: ServerConnection.Key; scope: ServerScope }) => {
const current = selectPromptTab(tabs.store, scope, target?.server ?? serverKey())
if (current) return createTabComposerState(tabs, current, target?.scope ?? serverSDK.scope, scope)
if (current) return createTabPromptState(tabs, current, target?.scope ?? serverSDK.scope, scope)
const key = target ? `${target.scope}:${scopeKey(scope)}` : scopeKey(scope)
const existing = cache.get(key)
@@ -116,7 +117,7 @@ export const { use: useComposerState, provider: ComposerPersistenceProvider } =
const entry = createRoot(
(dispose) => ({
value: createComposerState(target?.scope ?? serverSDK.scope, scope),
value: createPromptSession(target?.scope ?? serverSDK.scope, scope),
dispose,
}),
owner,
@@ -130,7 +131,7 @@ export const { use: useComposerState, provider: ComposerPersistenceProvider } =
const session = createMemo(() => load(scope()))
const pick = (scope?: PromptScope, target?: { server?: ServerConnection.Key; scope: ServerScope }) =>
scope ? load(scope, target) : session()
const ready = createComposerReady(session)
const ready = createPromptReady(session)
const withSuspense = <T,>(cb: () => T): (() => T) =>
createResource(
@@ -149,6 +150,7 @@ export const { use: useComposerState, provider: ComposerPersistenceProvider } =
pick(scope, target).capture(),
current: withSuspense(() => session().current()),
cursor: withSuspense(() => session().cursor()),
dirty: withSuspense(() => session().dirty()),
model: {
current: withSuspense(() => session().model.current()),
set: (model: PromptModel | undefined) => session().model.set(model),
+14
View File
@@ -26,6 +26,7 @@ export interface Settings {
general: {
autoSave: boolean
releaseNotes: boolean
followup: "queue" | "steer"
showFileTree: boolean
showNavigation: boolean
showSearch: boolean
@@ -111,6 +112,7 @@ const defaultSettings: Settings = {
general: {
autoSave: true,
releaseNotes: true,
followup: "steer",
showFileTree: false,
showNavigation: false,
showSearch: false,
@@ -174,6 +176,11 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans))
})
createEffect(() => {
if (store.general?.followup !== "queue") return
setStore("general", "followup", "steer")
})
return {
ready,
get current() {
@@ -188,6 +195,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setReleaseNotes(value: boolean) {
setStore("general", "releaseNotes", value)
},
followup: withFallback(
() => (store.general?.followup === "queue" ? "steer" : store.general?.followup),
defaultSettings.general.followup,
),
setFollowup(value: "queue" | "steer") {
setStore("general", "followup", value === "queue" ? "steer" : value)
},
showFileTree,
setShowFileTree(value: boolean) {
setStore("general", "showFileTree", value)
+2 -2
View File
@@ -11,7 +11,7 @@ import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
import { sessionHref } from "@/utils/session-route"
import { createTabMemory } from "./tab-memory"
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
import { createDraftComposerState, type PromptModel } from "@/composer/state"
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
import { migrateTabs } from "./tab-migration"
import { useCurrentRoute } from "./layout"
@@ -210,7 +210,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
async newDraft(draft: Omit<DraftTab, "type" | "draftID">, prompt?: string, model?: PromptModel) {
const draftID = uuid()
const tab = { type: "draft" as const, draftID, ...draft }
memory.ensure(tabKey(tab), "prompt", () => createDraftComposerState(draftID, { prompt, model }))
memory.ensure(tabKey(tab), "prompt", () => createDraftPromptSession(draftID, { prompt, model }))
await startTransition(() => {
setStore(
produce((tabs) => {
+35 -1
View File
@@ -174,6 +174,10 @@ export const dict = {
"command.session.compact.description": "የአውድ መጠንን ለመቀነስ ክፍለ-ጊዜውን ያጠቃልሉት",
"command.session.fork": "ከመልዕክት አዲስ ቅርንጫፍ ፍጠር",
"command.session.fork.description": "ከቀደመው መልእክት አዲስ ክፍለ ጊዜ ፍጠር",
"command.session.share": "አጋራ ክፍለ ጊዜ",
"command.session.share.description": "ይህን ክፍለ ጊዜ ያጋሩ እና URLን ወደ ቅንጥብ ሰሌዳ ይቅዱ",
"command.session.unshare": "ክፍለ-ጊዜን አታጋራ",
"command.session.unshare.description": "ይህን ክፍለ ጊዜ ማጋራት አቁም",
"command.session.export": "ክፍለ ጊዜን ወደ ውጭ ላክ",
"command.session.export.description": "ሙሉውን የክፍለ ጊዜ ግልባጭ እንደ JSON",
"palette.search.placeholder": "ፋይሎችን፣ ትዕዛዞችን እና ክፍለ-ጊዜዎችን ይፈልጉ",
@@ -581,6 +585,11 @@ export const dict = {
"toast.file.listFailed.title": "ፋይሎችን መዘርዘር አልተሳካም",
"toast.context.noLineSelection.title": "የመስመር ምርጫ የለም",
"toast.context.noLineSelection.description": "በመጀመሪያ በፋይል ትር ውስጥ የመስመር ክልልን ይምረጡ።",
"toast.session.share.copyFailed.title": "URLን ወደ ቅንጥብ ሰሌዳ መቅዳት አልተሳካም",
"toast.session.share.success.title": "ክፍል የተጋራ",
"toast.session.share.success.description": "አጋራ URL ወደ ቅንጥብ ሰሌዳ ተቀድቷል!",
"toast.session.share.failed.title": "ክፍለ ጊዜን ማጋራት አልተሳካም",
"toast.session.share.failed.description": "ክፍለ-ጊዜውን በማጋራት ላይ ስህተት ተፈጥሯል",
"toast.session.unshare.success.title": "ክፍል ያልተጋራ",
"toast.session.unshare.success.description": "ክፍለ ጊዜው በተሳካ ሁኔታ አልተጋራም!",
"toast.session.unshare.failed.title": "ክፍለ ጊዜን አለማጋራት",
@@ -708,6 +717,17 @@ export const dict = {
"session.question.restore": "ጥያቄን ወደነበረበት መልስ",
"session.question.pending.one": "{{count}} በመጠባበቅ ላይ ያለ ጥያቄ",
"session.question.pending.other": "{{count}} በመጠባበቅ ላይ ያሉ ጥያቄዎች",
"session.followupDock.summary.one": "{{count}}የተሰለፈ መልእክት",
"session.followupDock.summary.other": "{{count}}የተሰለፉ መልዕክቶች",
"session.followupDock.sendNow": "አሁን ላክ",
"session.followupDock.edit": "አርትዕ",
"session.followupDock.collapse": "የተሰለፉ መልዕክቶችን ሰብስብ",
"session.followupDock.expand": "የተሰለፉ መልዕክቶችን ዘርጋ",
"session.revertDock.summary.one": "{{count}}የተመለሰ መልዕክት",
"session.revertDock.summary.other": "{{count}}የተመለሱ መልዕክቶች",
"session.revertDock.collapse": "የተመለሱ መልዕክቶችን ሰብስብ",
"session.revertDock.expand": "የተጠቀለሉ መልዕክቶችን ዘርጋ",
"session.revertDock.restore": "መልዕክት እነበረበት መልስ",
"session.new.title": "ማንኛውም ነገር ገንባ",
"session.new.project.new": "አዲስ ፕሮጀክት",
"session.new.project.search": "የፍለጋ ፕሮጀክቶች",
@@ -754,7 +774,17 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "ፕለጊኖች",
"status.popover.action.manageServers": "አገልጋዮችን አስተዳድር",
"common.copied": "የተገለበጠ",
"session.share.popover.title": "በድር ላይ አትም",
"session.share.popover.description.shared": "ይህ ክፍለ ጊዜ በድር ላይ ይፋዊ ነው። አገናኙ ላለው ለማንኛውም ሰው ተደራሽ ነው።",
"session.share.popover.description.unshared": "ክፍለጊዜውን በይፋ በድሩ ላይ አጋራ። አገናኙ ላለው ለማንኛውም ሰው ተደራሽ ይሆናል።",
"session.share.action.share": "አጋራ",
"session.share.action.publish": "አትም",
"session.share.action.publishing": "በህትመት ላይ...",
"session.share.action.unpublish": "አትታተም",
"session.share.action.unpublishing": "ያለመታተም...",
"session.share.action.view": "እይታ",
"session.share.copy.copied": "የተገለበጠ",
"session.share.copy.copyLink": "መገልበጥ አገናኝ",
"lsp.tooltip.none": "ምንም LSP አገልጋዮች",
"lsp.label.connected": "{{count}}LSP",
"prompt.loading": "ፕሮምፕትን በመጫን ላይ...",
@@ -879,6 +909,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "በተርሚናል ውስጥ ጥቅም ላይ የዋለውን ቅርጸ-ቁምፊ አብጅ",
"settings.general.row.uiFont.title": "የUI ቅርጸ ቁምፊ",
"settings.general.row.uiFont.description": "በመገናኛው ሁሉ ጥቅም ላይ የዋለውን ቅርጸ-ቁምፊ አብጅ",
"settings.general.row.followup.title": "መከታተያ ባህሪ",
"settings.general.row.followup.description": "ክትትል የሚጠይቅ ከሆነ ወዲያውኑ ይመራ እንደሆነ ይምረጡ ወይም በሰልፍ ይጠብቁ",
"settings.general.row.followup.option.queue": "ወረፋ",
"settings.general.row.followup.option.steer": "መሪ",
"settings.general.row.showFileTree.title": "ፋይል ዛፍ",
"settings.general.row.showFileTree.description": "የፋይል ዛፍ ፓነልን በክፍሎች ውስጥ አሳይ",
"settings.general.row.showNavigation.title": "የአሰሳ መቆጣጠሪያዎች",
+43 -1
View File
@@ -180,6 +180,10 @@ export const dict = {
"command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق",
"command.session.fork": "تشعب من الرسالة",
"command.session.fork.description": "إنشاء جلسة جديدة من رسالة سابقة",
"command.session.share": "مشاركة الجلسة",
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
"command.session.unshare": "إلغاء مشاركة الجلسة",
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
"command.session.export": "تصدير الجلسة",
"command.session.export.description": "تصدير النص الكامل للجلسة بصيغة JSON",
@@ -585,6 +589,11 @@ export const dict = {
"toast.file.listFailed.title": "فشل سرد الملفات",
"toast.context.noLineSelection.title": "لا يوجد تحديد للأسطر",
"toast.context.noLineSelection.description": "حدد نطاق أسطر في تبويب ملف أولاً.",
"toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة",
"toast.session.share.success.title": "تمت مشاركة الجلسة",
"toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!",
"toast.session.share.failed.title": "فشل مشاركة الجلسة",
"toast.session.share.failed.description": "حدث خطأ أثناء مشاركة الجلسة",
"toast.session.unshare.success.title": "تم إلغاء مشاركة الجلسة",
"toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!",
"toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة",
@@ -703,6 +712,25 @@ export const dict = {
"session.question.pending.few": "{{count}} أسئلة معلقة",
"session.question.pending.many": "{{count}} سؤالًا معلقًا",
"session.question.pending.other": "الأسئلة المعلقة: {{count}}",
"session.followupDock.summary.one": "{{count}} رسالة في قائمة الانتظار",
"session.followupDock.summary.zero": "عدد الرسائل في قائمة الانتظار: {{count}}",
"session.followupDock.summary.two": "عدد الرسائل في قائمة الانتظار: {{count}}",
"session.followupDock.summary.few": "{{count}} رسائل في قائمة الانتظار",
"session.followupDock.summary.many": "{{count}} رسالةً في قائمة الانتظار",
"session.followupDock.summary.other": "{{count}} رسائل في قائمة الانتظار",
"session.followupDock.sendNow": "إرسال الآن",
"session.followupDock.edit": "تحرير",
"session.followupDock.collapse": "طي الرسائل المنتظرة",
"session.followupDock.expand": "توسيع الرسائل المنتظرة",
"session.revertDock.summary.one": "{{count}} رسالة تم التراجع عنها",
"session.revertDock.summary.zero": "عدد الرسائل التي تم التراجع عنها: {{count}}",
"session.revertDock.summary.two": "عدد الرسائل التي تم التراجع عنها: {{count}}",
"session.revertDock.summary.few": "{{count}} رسائل تم التراجع عنها",
"session.revertDock.summary.many": "{{count}} رسالةً تم التراجع عنها",
"session.revertDock.summary.other": "{{count}} رسائل تم التراجع عنها",
"session.revertDock.collapse": "طي الرسائل التي تم التراجع عنها",
"session.revertDock.expand": "توسيع الرسائل التي تم التراجع عنها",
"session.revertDock.restore": "استعادة الرسالة",
"session.new.title": "ابنِ أي شيء",
"session.new.project.new": "مشروع جديد",
"session.new.project.search": "البحث عن المشاريع",
@@ -730,7 +758,17 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "الإضافات",
"status.popover.action.manageServers": "إدارة الخوادم",
"common.copied": "تم النسخ",
"session.share.popover.title": "نشر على الويب",
"session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.",
"session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.",
"session.share.action.share": "مشاركة",
"session.share.action.publish": "نشر",
"session.share.action.publishing": "جارٍ النشر...",
"session.share.action.unpublish": "إلغاء النشر",
"session.share.action.unpublishing": "جارٍ إلغاء النشر...",
"session.share.action.view": "عرض",
"session.share.copy.copied": "تم النسخ",
"session.share.copy.copyLink": "نسخ الرابط",
"lsp.tooltip.none": "لا توجد خوادم LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "جارٍ تحميل الموجه...",
@@ -808,6 +846,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "خصّص الخط المستخدم في الطرفية",
"settings.general.row.uiFont.title": "خط الواجهة",
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
"settings.general.row.followup.title": "سلوك المتابعة",
"settings.general.row.followup.description": "اختر ما إذا كانت طلبات المتابعة توجه فورًا أو تنتظر في قائمة انتظار",
"settings.general.row.followup.option.queue": "قائمة انتظار",
"settings.general.row.followup.option.steer": "توجيه",
"settings.general.row.showFileTree.title": "شجرة الملفات",
"settings.general.row.showFileTree.description": "إظهار لوحة شجرة الملفات في الجلسات",
"settings.general.row.showNavigation.title": "عناصر التحكم في التنقل",
+37 -1
View File
@@ -176,6 +176,10 @@ export const dict = {
"command.session.compact.description": "Kontekst həcmini azaltmaq üçün sessiyanı xülasə et",
"command.session.fork": "Mesajdan fork et",
"command.session.fork.description": "Əvvəlki mesajdan yeni sessiya yarat",
"command.session.share": "Sessiyanı paylaş",
"command.session.share.description": "Bu sessiyanı paylaş və URL-ni buferə kopyala",
"command.session.unshare": "Sessiyanın paylaşımını dayandır",
"command.session.unshare.description": "Bu sessiyanın paylaşımını dayandır",
"command.session.export": "Sessiyanı ixrac et",
"command.session.export.description": "Sessiyanın tam transkriptini JSON formatında ixrac et",
@@ -595,6 +599,11 @@ export const dict = {
"toast.file.listFailed.title": "Fayllar siyahılana bilmədi",
"toast.context.noLineSelection.title": "Sətir seçimi yoxdur",
"toast.context.noLineSelection.description": "Əvvəlcə fayl tabında sətir aralığı seçin.",
"toast.session.share.copyFailed.title": "URL buferə kopyalana bilmədi",
"toast.session.share.success.title": "Sessiya paylaşıldı",
"toast.session.share.success.description": "Paylaşma URL-si buferə kopyalandı!",
"toast.session.share.failed.title": "Sessiya paylaşıla bilmədi",
"toast.session.share.failed.description": "Sessiyanı paylaşarkən xəta baş verdi",
"toast.session.unshare.success.title": "Sessiyanın paylaşımı dayandırıldı",
"toast.session.unshare.success.description": "Sessiyanın paylaşımı uğurla dayandırıldı!",
"toast.session.unshare.failed.title": "Sessiyanın paylaşımı dayandırıla bilmədi",
@@ -729,6 +738,17 @@ export const dict = {
"session.question.restore": "Sualı bərpa edin",
"session.question.pending.one": "{{count}} cavab gözləyən sual",
"session.question.pending.other": "{{count}} cavab gözləyən sual",
"session.followupDock.summary.one": "{{count}} növbəyə qoyulmuş mesaj",
"session.followupDock.summary.other": "{{count}} növbəyə qoyulmuş mesajlar",
"session.followupDock.sendNow": "İndi göndər",
"session.followupDock.edit": "Redaktə et",
"session.followupDock.collapse": "Növbəyə qoyulmuş mesajları yığcamlaşdırın",
"session.followupDock.expand": "Növbəyə qoyulmuş mesajları genişləndirin",
"session.revertDock.summary.one": "{{count}} geri alınmış mesaj",
"session.revertDock.summary.other": "{{count}} geri alınmış mesaj",
"session.revertDock.collapse": "Geri alınmış mesajları yığcamlaşdırın",
"session.revertDock.expand": "Geri qaytarılmış mesajları genişləndirin",
"session.revertDock.restore": "Mesajı bərpa edin",
"session.new.title": "İstədiyinizi qurun",
"session.new.project.new": "Yeni layihə",
"session.new.project.search": "Layihələri axtarın",
@@ -775,7 +795,18 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plaginlər",
"status.popover.action.manageServers": "Serverləri idarə et",
"common.copied": "Kopyalandı",
"session.share.popover.title": "Vebdə dərc et",
"session.share.popover.description.shared": "Bu sessiya vebdə açıqdır. Linkə sahib olan hər kəs daxil ola bilər.",
"session.share.popover.description.unshared":
"Sessiyanı vebdə açıq paylaşın. Linkə sahib olan hər kəs daxil ola biləcək.",
"session.share.action.share": "Paylaş",
"session.share.action.publish": "Dərc et",
"session.share.action.publishing": "Dərc edilir...",
"session.share.action.unpublish": "Dərcdən çıxar",
"session.share.action.unpublishing": "Dərcdən çıxarılır...",
"session.share.action.view": "Bax",
"session.share.copy.copied": "Kopyalandı",
"session.share.copy.copyLink": "Linki kopyala",
"lsp.tooltip.none": "LSP server yoxdur",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Prompt yüklənir...",
@@ -905,6 +936,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Terminalda istifadə olunan şrifti fərdiləşdirin",
"settings.general.row.uiFont.title": "İnterfeys şrifti",
"settings.general.row.uiFont.description": "Bütün interfeysdə istifadə olunan şrifti fərdiləşdirin",
"settings.general.row.followup.title": "Sonrakı sorğuların davranışı",
"settings.general.row.followup.description":
"Sonrakı sorğuların dərhal yönləndirilməsini və ya növbədə gözləməsini seçin",
"settings.general.row.followup.option.queue": "Növbə",
"settings.general.row.followup.option.steer": "Yönləndir",
"settings.general.row.showFileTree.title": "Fayl ağacı",
"settings.general.row.showFileTree.description": "Seanslarda fayl ağacı panelini göstərin",
"settings.general.row.showNavigation.title": "Naviqasiya nəzarətləri",
+37 -1
View File
@@ -177,6 +177,10 @@ export const dict = {
"command.session.compact.description": "Обобщете сесията, за да намалите размера на контекста",
"command.session.fork": "Разклонение от съобщение",
"command.session.fork.description": "Създайте нова сесия от предишно съобщение",
"command.session.share": "Споделяне на сесия",
"command.session.share.description": "Споделете тази сесия и копирайте URL в клипборда",
"command.session.unshare": "Прекратяване на споделянето на сесията",
"command.session.unshare.description": "Спрете да споделяте тази сесия",
"command.session.export": "Експортиране на сесия",
"command.session.export.description": "Експортирайте пълния препис на сесията като JSON",
"palette.search.placeholder": "Търсене на файлове, команди и сесии",
@@ -593,6 +597,11 @@ export const dict = {
"toast.file.listFailed.title": "Неуспешно изброяване на файлове",
"toast.context.noLineSelection.title": "Няма избор на линия",
"toast.context.noLineSelection.description": "Първо изберете диапазон от редове в раздел на файл.",
"toast.session.share.copyFailed.title": "Неуспешно копиране на URL в клипборда",
"toast.session.share.success.title": "Сесията е споделена",
"toast.session.share.success.description": "Споделяне на URL копирано в клипборда!",
"toast.session.share.failed.title": "Неуспешно споделяне на сесията",
"toast.session.share.failed.description": "Възникна грешка при споделяне на сесията",
"toast.session.unshare.success.title": "Сесията е прекратена",
"toast.session.unshare.success.description": "Сесията бе прекратена успешно!",
"toast.session.unshare.failed.title": "Прекратяването на споделянето на сесията не бе успешно",
@@ -725,6 +734,17 @@ export const dict = {
"session.question.restore": "Възстановете въпроса",
"session.question.pending.one": "{{count}} чакащ въпрос",
"session.question.pending.other": "{{count}} висящи въпроси",
"session.followupDock.summary.one": "{{count}} съобщение в опашка",
"session.followupDock.summary.other": "{{count}} съобщения в опашка",
"session.followupDock.sendNow": "Изпратете сега",
"session.followupDock.edit": "Редактиране",
"session.followupDock.collapse": "Свиване на съобщенията в опашката",
"session.followupDock.expand": "Разгъване на съобщенията в опашката",
"session.revertDock.summary.one": "{{count}} върнато съобщение",
"session.revertDock.summary.other": "{{count}} отменени съобщения",
"session.revertDock.collapse": "Свиване на върнатите съобщения",
"session.revertDock.expand": "Разгъване на върнатите съобщения",
"session.revertDock.restore": "Възстановяване на съобщението",
"session.new.title": "Изградете каквото и да било",
"session.new.project.new": "Нов проект",
"session.new.project.search": "Търсене на проекти",
@@ -771,7 +791,18 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Плъгини",
"status.popover.action.manageServers": "Управление на сървъри",
"common.copied": "Копирано",
"session.share.popover.title": "Публикувайте в мрежата",
"session.share.popover.description.shared": "Тази сесия е публична в мрежата. Достъпен е за всеки с връзката.",
"session.share.popover.description.unshared":
"Споделете сесията публично в мрежата. Тя ще бъде достъпна за всеки с връзката.",
"session.share.action.share": "Споделете",
"session.share.action.publish": "Публикувай",
"session.share.action.publishing": "Публикуване...",
"session.share.action.unpublish": "Отмяна на публикуването",
"session.share.action.unpublishing": "Отменя се публикуването...",
"session.share.action.view": "Преглед",
"session.share.copy.copied": "Копирано",
"session.share.copy.copyLink": "Копиране на връзката",
"lsp.tooltip.none": "Няма LSP сървъри",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Подканата се зарежда...",
@@ -901,6 +932,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Персонализирайте шрифта, използван в терминала",
"settings.general.row.uiFont.title": "UI шрифт",
"settings.general.row.uiFont.description": "Персонализирайте шрифта, използван в целия интерфейс",
"settings.general.row.followup.title": "Последващо поведение",
"settings.general.row.followup.description":
"Изберете дали последващите подкани да се управляват незабавно или да чакат на опашка",
"settings.general.row.followup.option.queue": "Опашка",
"settings.general.row.followup.option.steer": "Насочвайте",
"settings.general.row.showFileTree.title": "Файлово дърво",
"settings.general.row.showFileTree.description": "Показване на панела на файловото дърво в сесии",
"settings.general.row.showNavigation.title": "Контроли за навигация",
+36 -1
View File
@@ -175,6 +175,10 @@ export const dict: Record<string, string> = {
"command.session.compact.description": "প্রসঙ্গ আকার কমাতে সেশনের সারসংক্ষেপ করুন",
"command.session.fork": "বার্তা থেকে ফর্ক",
"command.session.fork.description": "একটি পূর্ববর্তী বার্তা থেকে একটি নতুন সেশন তৈরি করুন",
"command.session.share": "সেশন শেয়ার করুন",
"command.session.share.description": "এই সেশনটি শেয়ার করুন এবং ক্লিপবোর্ডে URL অনুলিপি করুন",
"command.session.unshare": "সেশন শেয়ার করা",
"command.session.unshare.description": "এই সেশন শেয়ার করা বন্ধ করুন",
"command.session.export": "রপ্তানি সেশন",
"command.session.export.description": "JSON হিসাবে সম্পূর্ণ সেশন ট্রান্সক্রিপ্ট রপ্তানি করুন",
"palette.search.placeholder": "অনুসন্ধান ফাইল, কমান্ড, এবং সেশন",
@@ -588,6 +592,11 @@ export const dict: Record<string, string> = {
"toast.file.listFailed.title": "ফাইল তালিকা করতে ব্যর্থ হয়েছে",
"toast.context.noLineSelection.title": "কোন লাইন নির্বাচন নেই",
"toast.context.noLineSelection.description": "প্রথমে একটি ফাইল ট্যাবে একটি লাইন পরিসর নির্বাচন করুন।",
"toast.session.share.copyFailed.title": "ক্লিপবোর্ডে URL অনুলিপি করতে ব্যর্থ হয়েছে৷",
"toast.session.share.success.title": "সেশন শেয়ার করা হয়েছে",
"toast.session.share.success.description": "শেয়ার করুন URL ক্লিপবোর্ডে কপি করা হয়েছে!",
"toast.session.share.failed.title": "সেশন শেয়ার করতে ব্যর্থ হয়েছে",
"toast.session.share.failed.description": "সেশন শেয়ার করার সময় একটি ত্রুটি ঘটেছে৷",
"toast.session.unshare.success.title": "সেশন শেয়ার করা বাদ দেওয়া হয়েছে",
"toast.session.unshare.success.description": "সেশন সফলভাবে মুক্ত করা হয়েছে!",
"toast.session.unshare.failed.title": "সেশন শেয়ার মুক্ত করতে ব্যর্থ হয়েছে",
@@ -719,6 +728,17 @@ export const dict: Record<string, string> = {
"session.question.restore": "প্রশ্ন পুনরুদ্ধার করুন",
"session.question.pending.one": "{{count}} মুলতুবি প্রশ্ন",
"session.question.pending.other": "{{count}} মুলতুবি প্রশ্ন",
"session.followupDock.summary.one": "{{count}} সারিবদ্ধ বার্তা",
"session.followupDock.summary.other": "{{count}} সারিবদ্ধ বার্তা",
"session.followupDock.sendNow": "এখন পাঠান",
"session.followupDock.edit": "সম্পাদনা করুন",
"session.followupDock.collapse": "সারিবদ্ধ বার্তাগুলি সঙ্কুচিত করুন৷",
"session.followupDock.expand": "সারিবদ্ধ বার্তাগুলি প্রসারিত করুন",
"session.revertDock.summary.one": "{{count}} রোল ব্যাক বার্তা৷",
"session.revertDock.summary.other": "{{count}} রোল ব্যাক বার্তা",
"session.revertDock.collapse": "রোল ব্যাক বার্তাগুলিকে আড়াল করুন৷",
"session.revertDock.expand": "রোল ব্যাক বার্তা প্রসারিত করুন",
"session.revertDock.restore": "বার্তা পুনরুদ্ধার করুন",
"session.new.title": "যে কোনো কিছু তৈরি করুন",
"session.new.project.new": "নতুন প্রকল্প",
"session.new.project.search": "অনুসন্ধান প্রকল্প",
@@ -765,7 +785,18 @@ export const dict: Record<string, string> = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "প্লাগইন",
"status.popover.action.manageServers": "সার্ভার পরিচালনা করুন",
"common.copied": "কপি করা হয়েছে",
"session.share.popover.title": "ওয়েবে প্রকাশ করুন",
"session.share.popover.description.shared": "এই সেশনটি ওয়েবে সর্বজনীন। এটি লিঙ্ক সহ যে কেউ অ্যাক্সেসযোগ্য।",
"session.share.popover.description.unshared":
"ওয়েবে সর্বজনীনভাবে সেশন শেয়ার করুন। এটি লিঙ্ক সহ যে কেউ অ্যাক্সেসযোগ্য হবে।",
"session.share.action.share": "শেয়ার করুন",
"session.share.action.publish": "প্রকাশ করুন",
"session.share.action.publishing": "প্রকাশ করা হচ্ছে...",
"session.share.action.unpublish": "অপ্রকাশিত করুন",
"session.share.action.unpublishing": "অপ্রকাশিত হচ্ছে...",
"session.share.action.view": "দেখুন",
"session.share.copy.copied": "কপি করা হয়েছে",
"session.share.copy.copyLink": "লিঙ্ক কপি করুন",
"lsp.tooltip.none": "কোনো LSP সার্ভার নেই",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "প্রম্পট লোড হচ্ছে...",
@@ -893,6 +924,10 @@ export const dict: Record<string, string> = {
"settings.general.row.terminalFont.description": "টার্মিনালে ব্যবহৃত ফন্টটি কাস্টমাইজ করুন",
"settings.general.row.uiFont.title": "UI ফন্ট",
"settings.general.row.uiFont.description": "ইন্টারফেস জুড়ে ব্যবহৃত ফন্ট কাস্টমাইজ করুন",
"settings.general.row.followup.title": "ফলো-আপ আচরণ",
"settings.general.row.followup.description": "ফলো-আপ প্রম্পট অবিলম্বে বাছা বা একটি সারিতে অপেক্ষা করুন চয়ন করুন",
"settings.general.row.followup.option.queue": "সারি",
"settings.general.row.followup.option.steer": "বাহা",
"settings.general.row.showFileTree.title": "ফাইল গাছ",
"settings.general.row.showFileTree.description": "সেশনে ফাইল ট্রি প্যানেল দেখান",
"settings.general.row.showNavigation.title": "নেভিগেশন নিয়ন্ত্রণ",
+40 -1
View File
@@ -182,6 +182,10 @@ export const dict = {
"command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto",
"command.session.fork": "Bifurcar da mensagem",
"command.session.fork.description": "Criar uma nova sessão a partir de uma mensagem anterior",
"command.session.share": "Compartilhar sessão",
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
"command.session.unshare": "Parar de compartilhar sessão",
"command.session.unshare.description": "Parar de compartilhar esta sessão",
"command.session.export": "Exportar sessão",
"command.session.export.description": "Exportar a transcrição completa da sessão como JSON",
@@ -589,6 +593,11 @@ export const dict = {
"toast.file.listFailed.title": "Falha ao listar arquivos",
"toast.context.noLineSelection.title": "Nenhuma seleção de linhas",
"toast.context.noLineSelection.description": "Selecione primeiro um intervalo de linhas em uma aba de arquivo.",
"toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência",
"toast.session.share.success.title": "Sessão compartilhada",
"toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!",
"toast.session.share.failed.title": "Falha ao compartilhar sessão",
"toast.session.share.failed.description": "Ocorreu um erro ao compartilhar a sessão",
"toast.session.unshare.success.title": "Sessão não compartilhada",
"toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!",
"toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão",
@@ -708,6 +717,19 @@ export const dict = {
"session.question.pending.one": "{{count}} pergunta pendente",
"session.question.pending.many": "{{count}} de perguntas pendentes",
"session.question.pending.other": "{{count}} perguntas pendentes",
"session.followupDock.summary.one": "{{count}} mensagem na fila",
"session.followupDock.summary.many": "{{count}} de mensagens na fila",
"session.followupDock.summary.other": "{{count}} mensagens na fila",
"session.followupDock.sendNow": "Enviar agora",
"session.followupDock.edit": "Editar",
"session.followupDock.collapse": "Recolher mensagens na fila",
"session.followupDock.expand": "Expandir mensagens na fila",
"session.revertDock.summary.one": "{{count}} mensagem revertida",
"session.revertDock.summary.many": "{{count}} de mensagens revertidas",
"session.revertDock.summary.other": "{{count}} mensagens revertidas",
"session.revertDock.collapse": "Recolher mensagens revertidas",
"session.revertDock.expand": "Expandir mensagens revertidas",
"session.revertDock.restore": "Restaurar mensagem",
"session.new.title": "Crie qualquer coisa",
"session.new.project.new": "Novo projeto",
"session.new.project.search": "Buscar projetos",
@@ -735,7 +757,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Gerenciar servidores",
"common.copied": "Copiado",
"session.share.popover.title": "Publicar na web",
"session.share.popover.description.shared":
"Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.",
"session.share.popover.description.unshared":
"Compartilhar sessão publicamente na web. Estará acessível para qualquer pessoa com o link.",
"session.share.action.share": "Compartilhar",
"session.share.action.publish": "Publicar",
"session.share.action.publishing": "Publicando...",
"session.share.action.unpublish": "Cancelar publicação",
"session.share.action.unpublishing": "Cancelando publicação...",
"session.share.action.view": "Ver",
"session.share.copy.copied": "Copiado",
"session.share.copy.copyLink": "Copiar link",
"lsp.tooltip.none": "Nenhum servidor LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Carregando prompt...",
@@ -815,6 +849,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Personalize a fonte usada no terminal",
"settings.general.row.uiFont.title": "Fonte da interface",
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
"settings.general.row.followup.title": "Comportamento de acompanhamento",
"settings.general.row.followup.description":
"Escolha se os prompts de acompanhamento orientam imediatamente ou esperam na fila",
"settings.general.row.followup.option.queue": "Fila",
"settings.general.row.followup.option.steer": "Orientar",
"settings.general.row.showFileTree.title": "Árvore de arquivos",
"settings.general.row.showFileTree.description": "Mostrar o painel da árvore de arquivos nas sessões",
"settings.general.row.showNavigation.title": "Controles de navegação",
+37 -1
View File
@@ -188,6 +188,10 @@ export const dict = {
"command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst",
"command.session.fork": "Fork iz poruke",
"command.session.fork.description": "Kreiraj novu sesiju iz prethodne poruke",
"command.session.share": "Podijeli sesiju",
"command.session.share.description": "Podijeli ovu sesiju i kopiraj URL u međuspremnik",
"command.session.unshare": "Ukini dijeljenje sesije",
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
"command.session.export": "Izvezi sesiju",
"command.session.export.description": "Izvezi cijeli zapis sesije u JSON formatu",
@@ -631,6 +635,11 @@ export const dict = {
"toast.context.noLineSelection.title": "Nema odabranih linija",
"toast.context.noLineSelection.description": "Prvo odaberi raspon linija u kartici datoteke.",
"toast.session.share.copyFailed.title": "Neuspjelo kopiranje URL-a u međuspremnik",
"toast.session.share.success.title": "Sesija podijeljena",
"toast.session.share.success.description": "URL za dijeljenje je kopiran u međuspremnik!",
"toast.session.share.failed.title": "Neuspjelo dijeljenje sesije",
"toast.session.share.failed.description": "Došlo je do greške prilikom dijeljenja sesije",
"toast.session.unshare.success.title": "Dijeljenje sesije ukinuto",
"toast.session.unshare.success.description": "Dijeljenje sesije je uspješno ukinuto!",
@@ -763,6 +772,19 @@ export const dict = {
"session.question.pending.one": "{{count}} pitanje na čekanju",
"session.question.pending.few": "{{count}} pitanja na čekanju",
"session.question.pending.other": "{{count}} pitanja na čekanju",
"session.followupDock.summary.one": "{{count}} poruka na čekanju",
"session.followupDock.summary.few": "{{count}} poruke na čekanju",
"session.followupDock.summary.other": "{{count}} poruka na čekanju",
"session.followupDock.sendNow": "Pošalji sada",
"session.followupDock.edit": "Uredi",
"session.followupDock.collapse": "Sažmi poruke na čekanju",
"session.followupDock.expand": "Proširi poruke na čekanju",
"session.revertDock.summary.one": "{{count}} vraćena poruka",
"session.revertDock.summary.few": "{{count}} vraćene poruke",
"session.revertDock.summary.other": "{{count}} vraćenih poruka",
"session.revertDock.collapse": "Sažmi vraćene poruke",
"session.revertDock.expand": "Proširi vraćene poruke",
"session.revertDock.restore": "Vrati poruku",
"session.new.title": "Napravi bilo šta",
"session.new.project.new": "Novi projekat",
@@ -794,7 +816,17 @@ export const dict = {
"status.popover.tab.plugins": "Plugini",
"status.popover.action.manageServers": "Upravljaj serverima",
"common.copied": "Kopirano",
"session.share.popover.title": "Objavi na webu",
"session.share.popover.description.shared": "Ova sesija je javna na webu. Dostupna je svima koji imaju link.",
"session.share.popover.description.unshared": "Podijeli sesiju javno na webu. Biće dostupna svima koji imaju link.",
"session.share.action.share": "Podijeli",
"session.share.action.publish": "Objavi",
"session.share.action.publishing": "Objavljivanje...",
"session.share.action.unpublish": "Poništi objavu",
"session.share.action.unpublishing": "Poništavanje objave...",
"session.share.action.view": "Prikaži",
"session.share.copy.copied": "Kopirano",
"session.share.copy.copyLink": "Kopiraj link",
"lsp.tooltip.none": "Nema LSP servera",
"lsp.label.connected": "{{count}} LSP",
@@ -881,6 +913,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Prilagodi font koji se koristi u terminalu",
"settings.general.row.uiFont.title": "UI font",
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
"settings.general.row.followup.description": "Odaberi da li upiti nadovezivanja usmjeravaju odmah ili čekaju u redu",
"settings.general.row.followup.option.queue": "Red čekanja",
"settings.general.row.followup.option.steer": "Usmjeri",
"settings.general.row.showFileTree.title": "Stablo datoteka",
"settings.general.row.showFileTree.description": "Prikaži stablo datoteka u sesijama",
"settings.general.row.showNavigation.title": "Kontrole navigacije",
+40 -1
View File
@@ -176,6 +176,10 @@ export const dict = {
"command.session.compact.description": "Resumeix la sessió per reduir la mida del context",
"command.session.fork": "Bifurcació del missatge",
"command.session.fork.description": "Crea una sessió nova a partir d'un missatge anterior",
"command.session.share": "Compartir sessió",
"command.session.share.description": "Comparteix aquesta sessió i copia el URL al porta-retalls",
"command.session.unshare": "Deixa de compartir la sessió",
"command.session.unshare.description": "Deixa de compartir aquesta sessió",
"command.session.export": "Sessió d'exportació",
"command.session.export.description": "Exporta la transcripció completa de la sessió com a JSON",
"palette.search.placeholder": "Cerca fitxers, ordres i sessions",
@@ -591,6 +595,11 @@ export const dict = {
"toast.file.listFailed.title": "No s'han pogut llistar els fitxers",
"toast.context.noLineSelection.title": "Sense selecció de línia",
"toast.context.noLineSelection.description": "Seleccioneu primer un interval de línies en una pestanya de fitxer.",
"toast.session.share.copyFailed.title": "No s'ha pogut copiar URL al porta-retalls",
"toast.session.share.success.title": "Sessió compartida",
"toast.session.share.success.description": "Comparteix URL copiat al porta-retalls!",
"toast.session.share.failed.title": "No s'ha pogut compartir la sessió",
"toast.session.share.failed.description": "S'ha produït un error en compartir la sessió",
"toast.session.unshare.success.title": "S'ha deixat de compartir la sessió",
"toast.session.unshare.success.description": "S'ha deixat de compartir la sessió correctament.",
"toast.session.unshare.failed.title": "No s'ha pogut deixar de compartir la sessió",
@@ -725,6 +734,19 @@ export const dict = {
"session.question.pending.one": "{{count}} pregunta pendent",
"session.question.pending.other": "{{count}} preguntes pendents",
"session.question.pending.many": "{{count}} preguntes pendents",
"session.followupDock.summary.one": "{{count}} missatge a la cua",
"session.followupDock.summary.other": "{{count}} missatges a la cua",
"session.followupDock.summary.many": "{{count}} missatges a la cua",
"session.followupDock.sendNow": "Envia ara",
"session.followupDock.edit": "Edita",
"session.followupDock.collapse": "Replega els missatges a la cua",
"session.followupDock.expand": "Amplieu els missatges a la cua",
"session.revertDock.summary.one": "{{count}} missatge revertit",
"session.revertDock.summary.other": "{{count}} missatges revertits",
"session.revertDock.summary.many": "{{count}} missatges revertits",
"session.revertDock.collapse": "Replega els missatges desfets",
"session.revertDock.expand": "Amplieu els missatges revertits",
"session.revertDock.restore": "Restaura el missatge",
"session.new.title": "Construeix qualsevol cosa",
"session.new.project.new": "Nou projecte",
"session.new.project.search": "Cerca projectes",
@@ -771,7 +793,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Connectors",
"status.popover.action.manageServers": "Gestionar servidors",
"common.copied": "Copiat",
"session.share.popover.title": "Publicar a la web",
"session.share.popover.description.shared":
"Aquesta sessió és pública a la web. És accessible per a qualsevol persona que tingui l'enllaç.",
"session.share.popover.description.unshared":
"Comparteix la sessió públicament al web. Serà accessible per a qualsevol persona que tingui l'enllaç.",
"session.share.action.share": "Comparteix",
"session.share.action.publish": "Publicar",
"session.share.action.publishing": "S'està publicant...",
"session.share.action.unpublish": "Despublicar",
"session.share.action.unpublishing": "S'està anul·lant la publicació...",
"session.share.action.view": "Veure",
"session.share.copy.copied": "Copiat",
"session.share.copy.copyLink": "Copia l'enllaç",
"lsp.tooltip.none": "No hi ha LSP servidors",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "S'està carregant el missatge...",
@@ -900,6 +934,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Personalitzeu el tipus de lletra utilitzat al terminal",
"settings.general.row.uiFont.title": "Tipus de lletra de la interfície",
"settings.general.row.uiFont.description": "Personalitzeu el tipus de lletra utilitzat a tota la interfície",
"settings.general.row.followup.title": "Comportament de seguiment",
"settings.general.row.followup.description":
"Trieu si les indicacions de seguiment es dirigeixen immediatament o esperen en una cua",
"settings.general.row.followup.option.queue": "Cua",
"settings.general.row.followup.option.steer": "Dirigeix",
"settings.general.row.showFileTree.title": "Arbre de fitxers",
"settings.general.row.showFileTree.description": "Mostra el tauler de l'arbre de fitxers a les sessions",
"settings.general.row.showNavigation.title": "Controls de navegació",
+39 -1
View File
@@ -174,6 +174,10 @@ export const dict = {
"command.session.compact.description": "Shrňte relaci, abyste snížili velikost kontextu",
"command.session.fork": "Vytvořit větev ze zprávy",
"command.session.fork.description": "Vytvořte novou relaci z předchozí zprávy",
"command.session.share": "Sdílejte relaci",
"command.session.share.description": "Sdílejte tuto relaci a zkopírujte URL do schránky",
"command.session.unshare": "Zrušit sdílení relace",
"command.session.unshare.description": "Přestat sdílet tuto relaci",
"command.session.export": "Export relace",
"command.session.export.description": "Exportovat celý přepis relace jako JSON",
"palette.search.placeholder": "Prohledávejte soubory, příkazy a relace",
@@ -589,6 +593,11 @@ export const dict = {
"toast.file.listFailed.title": "Seznam souborů se nezdařil",
"toast.context.noLineSelection.title": "Žádný výběr řádku",
"toast.context.noLineSelection.description": "Nejprve vyberte rozsah řádků na kartě souboru.",
"toast.session.share.copyFailed.title": "Zkopírování URL do schránky se nezdařilo",
"toast.session.share.success.title": "Relace sdílena",
"toast.session.share.success.description": "Sdílet URL zkopírováno do schránky!",
"toast.session.share.failed.title": "Sdílení relace se nezdařilo",
"toast.session.share.failed.description": "Při sdílení relace došlo k chybě",
"toast.session.unshare.success.title": "Relace byla zrušena",
"toast.session.unshare.success.description": "Sdílení relace bylo úspěšně zrušeno!",
"toast.session.unshare.failed.title": "Zrušení sdílení relace se nezdařilo",
@@ -721,6 +730,21 @@ export const dict = {
"session.question.pending.other": "{{count}} nevyřízené otázky",
"session.question.pending.few": "{{count}} nevyřízené otázky",
"session.question.pending.many": "{{count}} nevyřízených otázek",
"session.followupDock.summary.one": "{{count}} zpráva ve frontě",
"session.followupDock.summary.other": "{{count}} zpráv ve frontě",
"session.followupDock.summary.few": "{{count}} zprávy ve frontě",
"session.followupDock.summary.many": "{{count}} zprávy ve frontě",
"session.followupDock.sendNow": "Poslat nyní",
"session.followupDock.edit": "Upravit",
"session.followupDock.collapse": "Sbalit zprávy ve frontě",
"session.followupDock.expand": "Rozbalte zprávy ve frontě",
"session.revertDock.summary.one": "{{count}} vrácená zpráva",
"session.revertDock.summary.other": "{{count}} vrátilo zpět zprávy",
"session.revertDock.summary.few": "{{count}} vrácené zprávy",
"session.revertDock.summary.many": "{{count}} vrácené zprávy",
"session.revertDock.collapse": "Sbalit vrácené zprávy",
"session.revertDock.expand": "Rozbalte vrácené zprávy",
"session.revertDock.restore": "Obnovit zprávu",
"session.new.title": "Postavte cokoliv",
"session.new.project.new": "Nový projekt",
"session.new.project.search": "Hledat projekty",
@@ -767,7 +791,17 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Pluginy",
"status.popover.action.manageServers": "Správa serverů",
"common.copied": "Zkopírováno",
"session.share.popover.title": "Publikovat na webu",
"session.share.popover.description.shared": "Tato relace je veřejná na webu. Je přístupný komukoli s odkazem.",
"session.share.popover.description.unshared": "Sdílejte relaci veřejně na webu. Bude přístupný komukoli s odkazem.",
"session.share.action.share": "Sdílejte",
"session.share.action.publish": "Publikovat",
"session.share.action.publishing": "Publikování...",
"session.share.action.unpublish": "Zrušit publikování",
"session.share.action.unpublishing": "Rušení publikování...",
"session.share.action.view": "Zobrazit",
"session.share.copy.copied": "Zkopírováno",
"session.share.copy.copyLink": "Zkopírujte odkaz",
"lsp.tooltip.none": "Žádné LSP servery",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Načítání výzvy...",
@@ -898,6 +932,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Přizpůsobte písmo použité v terminálu",
"settings.general.row.uiFont.title": "UI Písmo",
"settings.general.row.uiFont.description": "Přizpůsobte písmo používané v celém rozhraní",
"settings.general.row.followup.title": "Následné chování",
"settings.general.row.followup.description": "Zvolte, zda se mají následné výzvy řídit okamžitě nebo čekat ve frontě",
"settings.general.row.followup.option.queue": "Fronta",
"settings.general.row.followup.option.steer": "Řídit",
"settings.general.row.showFileTree.title": "Strom souborů",
"settings.general.row.showFileTree.description": "Zobrazit panel stromu souborů v relacích",
"settings.general.row.showNavigation.title": "Ovládací prvky navigace",
+37 -1
View File
@@ -87,6 +87,10 @@ export const dict = {
"command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen",
"command.session.fork": "Forgren fra besked",
"command.session.fork.description": "Opret en ny session fra en tidligere besked",
"command.session.share": "Del session",
"command.session.share.description": "Del denne session og kopier URL'en til udklipsholderen",
"command.session.unshare": "Stop deling af session",
"command.session.unshare.description": "Stop med at dele denne session",
"command.session.export": "Eksportér session",
"command.session.export.description": "Eksportér hele sessionsudskriften som JSON",
@@ -527,6 +531,11 @@ export const dict = {
"toast.file.listFailed.title": "Kunne ikke liste filer",
"toast.context.noLineSelection.title": "Ingen linjevalg",
"toast.context.noLineSelection.description": "Vælg først et linjeinterval i en filfane.",
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder",
"toast.session.share.success.title": "Session delt",
"toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!",
"toast.session.share.failed.title": "Kunne ikke dele session",
"toast.session.share.failed.description": "Der opstod en fejl under deling af sessionen",
"toast.session.unshare.success.title": "Deling af session stoppet",
"toast.session.unshare.success.description": "Deling af session blev stoppet!",
@@ -656,6 +665,17 @@ export const dict = {
"session.question.restore": "Gendan spørgsmål",
"session.question.pending.one": "{{count}} afventende spørgsmål",
"session.question.pending.other": "{{count}} afventende spørgsmål",
"session.followupDock.summary.one": "{{count}} besked i kø",
"session.followupDock.summary.other": "{{count}} beskeder i kø",
"session.followupDock.sendNow": "Send nu",
"session.followupDock.edit": "Rediger",
"session.followupDock.collapse": "Skjul beskeder i kø",
"session.followupDock.expand": "Udvid beskeder i kø",
"session.revertDock.summary.one": "{{count}} tilbagerullet besked",
"session.revertDock.summary.other": "{{count}} tilbagerullede beskeder",
"session.revertDock.collapse": "Skjul tilbagerullede beskeder",
"session.revertDock.expand": "Udvid tilbagerullede beskeder",
"session.revertDock.restore": "Gendan besked",
"session.new.title": "Byg hvad som helst",
"session.new.project.new": "Nyt projekt",
@@ -687,7 +707,19 @@ export const dict = {
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Administrer servere",
"common.copied": "Kopieret",
"session.share.popover.title": "Udgiv på nettet",
"session.share.popover.description.shared":
"Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.",
"session.share.popover.description.unshared":
"Del session offentligt på nettet. Den vil være tilgængelig for alle med linket.",
"session.share.action.share": "Del",
"session.share.action.publish": "Udgiv",
"session.share.action.publishing": "Udgiver...",
"session.share.action.unpublish": "Afpublicer",
"session.share.action.unpublishing": "Afpublicerer...",
"session.share.action.view": "Vis",
"session.share.copy.copied": "Kopieret",
"session.share.copy.copyLink": "Kopier link",
"lsp.tooltip.none": "Ingen LSP-servere",
"lsp.label.connected": "{{count}} LSP",
@@ -774,6 +806,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Tilpas den skrifttype, der bruges i terminalen",
"settings.general.row.uiFont.title": "UI-skrifttype",
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugergrænsefladen",
"settings.general.row.followup.title": "Opfølgningsadfærd",
"settings.general.row.followup.description": "Vælg om opfølgende forespørgsler skal styre straks eller vente i kø",
"settings.general.row.followup.option.queue": "Kø",
"settings.general.row.followup.option.steer": "Styr",
"settings.general.row.showFileTree.title": "Filtræ",
"settings.general.row.showFileTree.description": "Vis filtræspanelet i sessioner",
"settings.general.row.showNavigation.title": "Navigationsknapper",
+38 -1
View File
@@ -85,6 +85,10 @@ export const dict = {
"command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren",
"command.session.fork": "Von Nachricht abzweigen",
"command.session.fork.description": "Neue Sitzung aus einer früheren Nachricht erstellen",
"command.session.share": "Sitzung teilen",
"command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren",
"command.session.unshare": "Teilen der Sitzung aufheben",
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
"command.session.export": "Sitzung exportieren",
"command.session.export.description": "Das vollständige Transkript der Sitzung als JSON exportieren",
@@ -495,6 +499,11 @@ export const dict = {
"toast.file.listFailed.title": "Dateien konnten nicht aufgelistet werden",
"toast.context.noLineSelection.title": "Keine Zeilenauswahl",
"toast.context.noLineSelection.description": "Wählen Sie zuerst einen Zeilenbereich in einem Datei-Tab aus.",
"toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden",
"toast.session.share.success.title": "Sitzung geteilt",
"toast.session.share.success.description": "URL zum Teilen in die Zwischenablage kopiert!",
"toast.session.share.failed.title": "Sitzung konnte nicht geteilt werden",
"toast.session.share.failed.description": "Beim Teilen der Sitzung ist ein Fehler aufgetreten",
"toast.session.unshare.success.title": "Teilen der Sitzung aufgehoben",
"toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!",
"toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen",
@@ -615,6 +624,17 @@ export const dict = {
"session.question.restore": "Frage wiederherstellen",
"session.question.pending.one": "{{count}} ausstehende Frage",
"session.question.pending.other": "{{count}} ausstehende Fragen",
"session.followupDock.summary.one": "{{count}} Nachricht in der Warteschlange",
"session.followupDock.summary.other": "{{count}} Nachrichten in der Warteschlange",
"session.followupDock.sendNow": "Jetzt senden",
"session.followupDock.edit": "Bearbeiten",
"session.followupDock.collapse": "Warteschlange einklappen",
"session.followupDock.expand": "Warteschlange ausklappen",
"session.revertDock.summary.one": "{{count}} zurückgesetzte Nachricht",
"session.revertDock.summary.other": "{{count}} zurückgesetzte Nachrichten",
"session.revertDock.collapse": "Zurückgesetzte Nachrichten einklappen",
"session.revertDock.expand": "Zurückgesetzte Nachrichten ausklappen",
"session.revertDock.restore": "Nachricht wiederherstellen",
"session.new.title": "Alles entwickeln",
"session.new.project.new": "Neues Projekt",
"session.new.project.search": "Projekte durchsuchen",
@@ -642,7 +662,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Server verwalten",
"common.copied": "Kopiert",
"session.share.popover.title": "Im Web veröffentlichen",
"session.share.popover.description.shared":
"Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.",
"session.share.popover.description.unshared":
"Sitzung öffentlich im Web teilen. Sie wird für jeden mit dem Link zugänglich sein.",
"session.share.action.share": "Teilen",
"session.share.action.publish": "Veröffentlichen",
"session.share.action.publishing": "Wird veröffentlicht…",
"session.share.action.unpublish": "Veröffentlichung aufheben",
"session.share.action.unpublishing": "Veröffentlichung wird aufgehoben…",
"session.share.action.view": "Ansehen",
"session.share.copy.copied": "Kopiert",
"session.share.copy.copyLink": "Link kopieren",
"lsp.tooltip.none": "Keine LSP-Server",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Prompt wird geladen…",
@@ -724,6 +756,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Die im Terminal verwendete Schriftart anpassen",
"settings.general.row.uiFont.title": "UI-Schriftart",
"settings.general.row.uiFont.description": "Die in der gesamten Benutzeroberfläche verwendete Schriftart anpassen",
"settings.general.row.followup.title": "Verhalten bei Folgeeingaben",
"settings.general.row.followup.description":
"Wählen Sie, ob Folgeeingaben die laufende Sitzung sofort steuern oder in einer Warteschlange warten",
"settings.general.row.followup.option.queue": "Warteschlange",
"settings.general.row.followup.option.steer": "Steuern",
"settings.general.row.showFileTree.title": "Dateibaum",
"settings.general.row.showFileTree.description": "Dateibaum in Sitzungen anzeigen",
"settings.general.row.showNavigation.title": "Navigationssteuerung",
+38 -1
View File
@@ -177,6 +177,10 @@ export const dict = {
"command.session.compact.description": "ކޮންޓެކްސްޓް ސައިޒު ކުޑަކުރުމަށްޓަކައި ސެޝަން ޚުލާޞާކުރުން",
"command.session.fork": "މެސެޖުން ފޯކް",
"command.session.fork.description": "ކުރީގެ މެސެޖަކުން އާ ސެޝަނެއް އުފެއްދުން",
"command.session.share": "ޝެއާ ސެޝަން",
"command.session.share.description": "މި ސެޝަން ހިއްސާކޮށް، URL ކްލިޕްބޯޑަށް ކޮޕީކޮށްލާށެވެ",
"command.session.unshare": "އަންޝެއާ ސެޝަން",
"command.session.unshare.description": "މި ސެޝަން ހިއްސާކުރުން ހުއްޓާލާށެވެ",
"command.session.export": "އެކްސްޕޯޓް ސެޝަން",
"command.session.export.description": "ފުލް ސެޝަން ޓްރާންސްކްރިޕްޓް JSON ގެ ގޮތުގައި އެކްސްޕޯޓްކުރުން",
"palette.search.placeholder": "ފައިލްތަކާއި، ކޮމާންޑްތަކާއި، ސެޝަންތައް ހޯދުން",
@@ -595,6 +599,11 @@ export const dict = {
"toast.file.listFailed.title": "ފައިލްތައް ލިސްޓް ނުކުރެވުނެވެ",
"toast.context.noLineSelection.title": "ލައިން ސެލެކްޝަނެއް ނެތެވެ",
"toast.context.noLineSelection.description": "ފުރަތަމަ ފައިލް ޓެބެއްގައި ލައިން ރޭންޖެއް ހޮވާށެވެ.",
"toast.session.share.copyFailed.title": "URL ކްލިޕްބޯޑަށް ކޮޕީ ނުކުރެވުނެވެ",
"toast.session.share.success.title": "ސެޝަން ޝެއާ ކުރިއެވެ",
"toast.session.share.success.description": "ޝެއާ URL ކްލިޕްބޯޑަށް ކޮޕީކޮށްފައި!",
"toast.session.share.failed.title": "ޝެއާ ސެޝަން ނާކާމިޔާބުވި",
"toast.session.share.failed.description": "ސެޝަން ހިއްސާ ކުރަމުން ދިޔައިރު ގޯހެއް ދިމާވިއެވެ",
"toast.session.unshare.success.title": "ސެޝަން އަންޝެއަރޑް",
"toast.session.unshare.success.description": "ސެޝަން އަންޝެއަރ ކާމިޔާބުކަމާއެކު!",
"toast.session.unshare.failed.title": "ސެޝަން އަންޝެއާ ކުރަން ފެއިލްވެއްޖެ",
@@ -730,6 +739,17 @@ export const dict = {
"session.question.restore": "ސުވާލު އަލުން އާލާކުރުން",
"session.question.pending.one": "{{count}} ކުރިއަށް އޮތް ސުވާލު",
"session.question.pending.other": "{{count}} ކުރިއަށް އޮތް ސުވާލުތައް",
"session.followupDock.summary.one": "{{count}} ކިއު ކޮށްފައިވާ މެސެޖެކެވެ",
"session.followupDock.summary.other": "{{count}} ކިއު ކުރެވިފައިވާ މެސެޖުތައް",
"session.followupDock.sendNow": "މިހާރު ފޮނުއްވާ",
"session.followupDock.edit": "ބަދަލު ގެނައުން",
"session.followupDock.collapse": "ކިއު ޖަހާފައިވާ މެސެޖުތައް ކޮލަޕްސް ކޮށްލާށެވެ",
"session.followupDock.expand": "ކިއު ކުރެވިފައިވާ މެސެޖުތައް ފުޅާކުރުން",
"session.revertDock.summary.one": "{{count}} ރޯލް ބެކް މެސެޖެއް",
"session.revertDock.summary.other": "{{count}} ރޯލްބެކް މެސެޖުތައް",
"session.revertDock.collapse": "ކޮލަޕްސް ރޯލް ބެކް މެސެޖުތަކެވެ",
"session.revertDock.expand": "ރޯލް ބެކް މެސެޖުތައް ފުޅާކުރުން",
"session.revertDock.restore": "މެސެޖު ރިސްޓޯރ ކުރާށެވެ",
"session.new.title": "ކޮންމެ އެއްޗެއް ބިނާކުރާށެވެ",
"session.new.project.new": "އާ މަޝްރޫއެއް",
"session.new.project.search": "ޕްރޮޖެކްޓްތައް ހޯދުން",
@@ -776,7 +796,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP އެވެ",
"status.popover.tab.plugins": "ޕްލަގިންސް އެވެ",
"status.popover.action.manageServers": "ސަރވަރތައް މެނޭޖްކުރުން",
"common.copied": "ކޮޕީކޮށްފައި",
"session.share.popover.title": "ވެބްގައި ޝާއިއުކުރުން",
"session.share.popover.description.shared":
"މި ސެޝަން ވެބްގައި އާންމުކޮށް ކުރިއަށް ގެންދެވޭނެއެވެ. އެއީ ލިންކް އާއި އެކު ކޮންމެ މީހަކަށް ވެސް އެކްސެސް ކުރެވޭނެ އެއްޗެކެވެ.",
"session.share.popover.description.unshared":
"ސެޝަން އާންމުކޮށް ވެބްގައި ހިއްސާކުރުން. އަދި ލިންކް ހުރި ކޮންމެ މީހަކަށްވެސް އެތަނަށް ވަދެވޭނެއެވެ.",
"session.share.action.share": "ޙިއްސާ",
"session.share.action.publish": "ޝާއިއުކުރުން",
"session.share.action.publishing": "ޕަބްލިޝިންގ...",
"session.share.action.unpublish": "އަންޕަބްލިޝް ކުރުން",
"session.share.action.unpublishing": "އަންޕަބްލިޝް ކުރަމުންދާ...",
"session.share.action.view": "މަންޒަރު",
"session.share.copy.copied": "ކޮޕީކޮށްފައި",
"session.share.copy.copyLink": "ކޮޕީ ލިންކް",
"lsp.tooltip.none": "LSP ސަރވަރެއް ނެތެވެ",
"lsp.label.connected": "{{count}} LSP އެވެ",
"prompt.loading": "ލޯޑިންގ ޕްރޮމްޕްޓް...",
@@ -908,6 +940,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "ޓާމިނަލްގައި ބޭނުންކުރާ ފޮންޓް ކަސްޓަމައިޒް ކުރުން",
"settings.general.row.uiFont.title": "UI ފޮންޓް",
"settings.general.row.uiFont.description": "މުޅި އިންޓަރފޭސްގައި ބޭނުންކުރާ ފޮންޓް ކަސްޓަމައިޒް ކުރުން",
"settings.general.row.followup.title": "ފޮލޯއަޕް ސުލޫކު",
"settings.general.row.followup.description":
"ފޮލޯއަޕް ޕްރޮމްޕްޓްސް ވަގުތުން ސްޓިއަރ ކުރުން ނުވަތަ ކިއުއެއްގައި މަޑުކުރުންތޯ ހޮވާށެވެ",
"settings.general.row.followup.option.queue": "ކިއު",
"settings.general.row.followup.option.steer": "ސްޓީއާ އެވެ",
"settings.general.row.showFileTree.title": "ފައިލް ގަހެވެ",
"settings.general.row.showFileTree.description": "ސެޝަންތަކުގައި ފައިލް ޓްރީ ޕެނަލް ދައްކާށެވެ",
"settings.general.row.showNavigation.title": "ނޭވިގޭޝަން ކޮންޓްރޯލްތައް",
+38 -1
View File
@@ -177,6 +177,10 @@ export const dict: Record<string, string> = {
"command.session.compact.description": "སྐབས་དོན་གྱི་ཚད་མར་ཕབ་འབད་ནིའི་དོན་ལུ་ ལཱ་ཡུན་བཅུད་བསྡུས།",
"command.session.fork": "འཕྲིན་དོན་ལས་ཕོརཀ།",
"command.session.fork.description": "ཧེ་མའི་འཕྲིན་དོན་ལས་ ལཱ་ཡུན་གསརཔ་ཅིག་གསར་བསྐྲུན་འབད།",
"command.session.share": "བརྗེ་སོར་གྱི་ལཱ་ཡུན།",
"command.session.share.description": "ལཱ་ཡུན་འདི་རུབ་སྤྱོད་འབད་ཞིནམ་ལས་ URLའདི་འཛིན་པང་ལུ་འདྲ་བཤུས་རྐྱབས།",
"command.session.unshare": "ལཱ་ཡུན་བགོ་བཤའ་རྐྱབ།",
"command.session.unshare.description": "ལཱ་ཡུན་འདི་བརྗེ་སོར་འབད་ནི་འདི་བཀག།",
"command.session.export": "ལཱ་ཡུན་ཕྱིར་འདྲེན་འབད་ནི།",
"command.session.export.description": "ལཱ་ཡུན་ཡིག་བསྒྱུར་ཆ་ཚང་འདི་ JSON སྦེ་ཕྱིར་འདྲེན་འབད།",
"palette.search.placeholder": "ཡིག་སྣོད་དང་བརྡ་བཀོད་ དེ་ལས་ལཱ་ཡུན་ཚུ་འཚོལ་ཞིབ་འབད།",
@@ -595,6 +599,11 @@ export const dict: Record<string, string> = {
"toast.file.listFailed.title": "ཡིག་སྣོད་ཚུ་ཐོ་བཀོད་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།",
"toast.context.noLineSelection.title": "གྲལ་ཐིག་སེལ་འཐུ་མེད།",
"toast.context.noLineSelection.description": "དང་པ་རང་ ཡིག་སྣོད་མཆོང་ལྡེ་ཅིག་ནང་གྲལ་ཐིག་ཁྱབ་ཚད་ཅིག་སེལ་འཐུ་འབད།",
"toast.session.share.copyFailed.title": "URLའདི་འཛིན་པང་ལུ་འདྲ་བཤུས་རྐྱབ་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།",
"toast.session.share.success.title": "ལཱ་ཡུན་བརྗེ་སོར་འབད་ཡོདཔ།",
"toast.session.share.success.description": "བརྗེ་སོར་ URLའཛིན་པང་ལུ་འདྲ་བཤུས་རྐྱབས་ནུག་!",
"toast.session.share.failed.title": "ལཱ་ཡུན་རུབ་སྤྱོད་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།",
"toast.session.share.failed.description": "ལཱ་ཡུན་བརྗེ་སོར་འབད་བའི་སྐབས་ འཛོལ་བ་ཅིག་བྱུང་ཡོདཔ།",
"toast.session.unshare.success.title": "ལཱ་ཡུན་བརྗེ་སོར་མ་འབད་བ།",
"toast.session.unshare.success.description": "ལཱ་ཡུན་འདི་ མཐར་འཁྱོལ་ཅན་སྦེ་ བརྗེ་སོར་མ་འབད་བས།",
"toast.session.unshare.failed.title": "ལཱ་ཡུན་བགོ་བཤའ་རྐྱབ་ནི་འཐུས་ཤོར་འབྱུང་ཡོདཔ།",
@@ -730,6 +739,17 @@ export const dict: Record<string, string> = {
"session.question.restore": "དྲི་བ་སླར་གསོ་འབད།",
"session.question.pending.one": "{{count}} བསྒུག་པའི་དྲི་བ།",
"session.question.pending.other": "{{count}} སྒུག་པའི་དྲི་བ།",
"session.followupDock.summary.one": "{{count}} གྲལ་ཐིག་ཅན་གྱི་འཕྲིན་འཕྲིན།",
"session.followupDock.summary.other": "{{count}} གྲལ་སྒྲིག་ཡོད་པའི་འཕྲིན་འཕྲིན།",
"session.followupDock.sendNow": "ད་ལྟོ་གཏང་།",
"session.followupDock.edit": "ཞུན༌དག",
"session.followupDock.collapse": "གྲལ་ཐིག་ནང་བཙུགས་ཡོད་པའི་འཕྲིན་དོན་ཚུ་མར་བསྡམས།",
"session.followupDock.expand": "གྲལ་ཐིག་ནང་ཡོད་པའི་འཕྲིན་དོན་ཚུ་རྒྱ་བསྐྱེད་འབད།",
"session.revertDock.summary.one": "{{count}} ཕྱིར་ལོག་འཕྲིན་ཡིག།",
"session.revertDock.summary.other": "{{count}} ཕྱིར་ལོག་འབད་ཡོད་པའི་འཕྲིན་ཡིག།",
"session.revertDock.collapse": "ལོག་བཤུད་འབད་ཡོད་པའི་འཕྲིན་དོན་ཚུ་ མར་བསྡམས།",
"session.revertDock.expand": "ཕྱིར་ལོག་འབད་ཡོད་པའི་འཕྲིན་དོན་ཚུ་རྒྱ་བསྐྱེད་འབད།",
"session.revertDock.restore": "འཕྲིན་དོན་སླར་གསོ་འབད།",
"session.new.title": "ག་ཅི་ཡང་བཟོ་བསྐྲུན།",
"session.new.project.new": "ལས་འགུལ་གསརཔ།",
"session.new.project.search": "ལས་གཞི་འཚོལ་ཞིབ།",
@@ -776,7 +796,19 @@ export const dict: Record<string, string> = {
"status.popover.tab.lsp": "LSP།",
"status.popover.tab.plugins": "པ་ལག་ཨིན་ཚུ།",
"status.popover.action.manageServers": "སར་བར་ཚུ་འཛིན་སྐྱོང་འཐབ།",
"common.copied": "འདྲ་བཤུས་འབད་ཡོདཔ།",
"session.share.popover.title": "ཝེབ་ནང་དཔར་བསྐྲུན་འབད།",
"session.share.popover.description.shared":
"ལཱ་ཡུན་འདི་ ཝེབ་ནང་ མི་མང་ཨིན། འབྲེལ་ལམ་ཡོད་མི་ག་ར་གིས་ འཛུལ་སྤྱོད་འབད་ཚུགས།",
"session.share.popover.description.unshared":
"ཡོངས་འབྲེལ་ནང་ལུ་ མི་མང་ལུ་ ལཱ་ཡུན་བརྗེ་སོར་འབད། འབྲེལ་ལམ་ཡོད་མི་ག་ར་གིས་ འཛུལ་སྤྱོད་འབད་ཚུགས།",
"session.share.action.share": "བགོ་བཤའ",
"session.share.action.publish": "དཔར་སྐྲུན།",
"session.share.action.publishing": "དཔར་བསྐྲུན་འབད་དོ།...",
"session.share.action.unpublish": "དཔར་བསྐྲུན་བཤོལ།",
"session.share.action.unpublishing": "དཔར་བསྐྲུན་མ་འབད་བའི་...",
"session.share.action.view": "བསམ༌འཆར",
"session.share.copy.copied": "འདྲ་བཤུས་འབད་ཡོདཔ།",
"session.share.copy.copyLink": "འབྲེལ་མཐུད་འདྲ་བཤུས།",
"lsp.tooltip.none": "LSP སར་བར་ཚུ་མེད།",
"lsp.label.connected": "{{count}} LSP།",
"prompt.loading": "མངོན་གསལ་འབད་དོ།",
@@ -909,6 +941,11 @@ export const dict: Record<string, string> = {
"settings.general.row.terminalFont.description": "ཊར་མི་ནཱལ་ནང་ལག་ལེན་འཐབ་ཡོད་པའི་ཡིག་གཟུགས་སྲོལ་སྒྲིག་འབད།",
"settings.general.row.uiFont.title": "ཡུ་ཨའི་ཡིག་གཟུགས།",
"settings.general.row.uiFont.description": "ངོས་འདྲ་བ་ཆ་མཉམ་ལུ་ལག་ལེན་འཐབ་ཡོད་པའི་ཡིག་གཟུགས་སྲོལ་སྒྲིག་འབད།",
"settings.general.row.followup.title": "རྗེས་སུ་འབྲངས་པའི་སྤྱོད་ལམ།",
"settings.general.row.followup.description":
"རྗེས་འཇུག་འབོད་བརྡ་ཚུ་ དེ་འཕྲོ་ལས་ སྒུལ་ནི་ཨིན་ན་ ཡང་ན་ གྱལ་ནང་བསྒུག་སྡོད་ནི་ཨིན་ན་ གདམ་ཁ་རྐྱབས།",
"settings.general.row.followup.option.queue": "གྱལ",
"settings.general.row.followup.option.steer": "སྒུལ་ཤུགས།",
"settings.general.row.showFileTree.title": "ཡིག་སྣོད་ཤིང་།",
"settings.general.row.showFileTree.description": "ལཱ་ཡུན་ཚུ་ནང་ཡིག་སྣོད་ཤིང་གི་པེ་ནཱལ་སྟོན།",
"settings.general.row.showNavigation.title": "འགྲུལ་བསྐྱོད་ཚད་འཛིན་ཚུ།",
+38 -1
View File
@@ -175,6 +175,10 @@ export const dict = {
"command.session.compact.description": "Συνοψήστε τη συνεδρία για να μειώσετε το μέγεθος του περιβάλλοντος",
"command.session.fork": "Διακλάδωση από μήνυμα",
"command.session.fork.description": "Δημιουργία νέας συνεδρίας από προηγούμενο μήνυμα",
"command.session.share": "Κοινή χρήση συνεδρίας",
"command.session.share.description": "Κοινή χρήση αυτής της συνεδρίας και αντιγράψτε το URL στο πρόχειρο",
"command.session.unshare": "Κατάργηση κοινής χρήσης συνεδρίας",
"command.session.unshare.description": "Διακοπή κοινής χρήσης αυτής της συνεδρίας",
"command.session.export": "Εξαγωγή συνεδρίας",
"command.session.export.description": "Εξαγωγή της πλήρους μεταγραφής της συνεδρίας ως JSON",
"palette.search.placeholder": "Αναζήτηση αρχείων, εντολών και συνεδριών",
@@ -592,6 +596,11 @@ export const dict = {
"toast.file.listFailed.title": "Απέτυχε η λίστα αρχείων",
"toast.context.noLineSelection.title": "Χωρίς επιλογή γραμμής",
"toast.context.noLineSelection.description": "Επιλέξτε ένα εύρος γραμμής σε μια καρτέλα αρχείου πρώτα.",
"toast.session.share.copyFailed.title": "Αποτυχία αντιγραφής του URL στο πρόχειρο",
"toast.session.share.success.title": "Η συνεδρία κοινοποιήθηκε",
"toast.session.share.success.description": "Κοινή χρήση του URL αντιγράφηκε στο πρόχειρο!",
"toast.session.share.failed.title": "Αποτυχία κοινής χρήσης συνεδρίας",
"toast.session.share.failed.description": "Παρουσιάστηκε σφάλμα κατά την κοινή χρήση της συνεδρίας",
"toast.session.unshare.success.title": "Καταργήθηκε η κοινή χρήση της συνεδρίας",
"toast.session.unshare.success.description": "Η κοινή χρήση της συνεδρίας καταργήθηκε με επιτυχία!",
"toast.session.unshare.failed.title": "Αποτυχία κατάργησης κοινής χρήσης συνεδρίας",
@@ -727,6 +736,17 @@ export const dict = {
"session.question.restore": "Ερώτηση επαναφοράς",
"session.question.pending.one": "{{count}} ερώτηση σε εκκρεμότητα",
"session.question.pending.other": "{{count}} ερωτήσεις σε εκκρεμότητα",
"session.followupDock.summary.one": "{{count}} μήνυμα στην ουρά",
"session.followupDock.summary.other": "{{count}} μηνύματα στην ουρά",
"session.followupDock.sendNow": "Αποστολή τώρα",
"session.followupDock.edit": "Επεξεργασία",
"session.followupDock.collapse": "Σύμπτυξη μηνυμάτων στην ουρά",
"session.followupDock.expand": "Ανάπτυξη μηνυμάτων στην ουρά",
"session.revertDock.summary.one": "{{count}} ανανεωμένο μήνυμα",
"session.revertDock.summary.other": "{{count}} επαναλαμβανόμενα μηνύματα",
"session.revertDock.collapse": "Σύμπτυξη επαναλαμβανόμενων μηνυμάτων",
"session.revertDock.expand": "Ανάπτυξη επαναλαμβανόμενων μηνυμάτων",
"session.revertDock.restore": "Επαναφορά μηνύματος",
"session.new.title": "Δημιουργία οτιδήποτε",
"session.new.project.new": "Νέο έργο",
"session.new.project.search": "Αναζήτηση έργων",
@@ -773,7 +793,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Προσθήκες",
"status.popover.action.manageServers": "Διαχείριση διακομιστών",
"common.copied": "Αντιγράφηκε",
"session.share.popover.title": "Δημοσίευση στον Ιστό",
"session.share.popover.description.shared":
"Αυτή η συνεδρία είναι δημόσια στον Ιστό. Είναι προσβάσιμο σε οποιονδήποτε έχει τον σύνδεσμο.",
"session.share.popover.description.unshared":
"Κοινή χρήση συνεδρίας δημόσια στον ιστό. Θα είναι προσβάσιμο σε οποιονδήποτε έχει τον σύνδεσμο.",
"session.share.action.share": "Κοινή χρήση",
"session.share.action.publish": "Δημοσίευση",
"session.share.action.publishing": "Δημοσίευση...",
"session.share.action.unpublish": "Κατάργηση δημοσίευσης",
"session.share.action.unpublishing": "Κατάργηση δημοσίευσης...",
"session.share.action.view": "Προβολή",
"session.share.copy.copied": "Αντιγράφηκε",
"session.share.copy.copyLink": "Αντιγραφή συνδέσμου",
"lsp.tooltip.none": "Δεν υπάρχουν LSP διακομιστές",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Φόρτωση προτροπής...",
@@ -905,6 +937,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Προσαρμογή της γραμματοσειράς που χρησιμοποιείται στο τερματικό",
"settings.general.row.uiFont.title": "Γραμματοσειρά UI",
"settings.general.row.uiFont.description": "Προσαρμογή της γραμματοσειράς που χρησιμοποιείται σε όλη τη διεπαφή",
"settings.general.row.followup.title": "Συμπεριφορά παρακολούθησης",
"settings.general.row.followup.description":
"Επιλέξτε εάν οι επακόλουθες προτροπές κατευθύνονται αμέσως ή περιμένετε σε μια ουρά",
"settings.general.row.followup.option.queue": "Ουρά",
"settings.general.row.followup.option.steer": "Καθοδήγηση",
"settings.general.row.showFileTree.title": "Δέντρο αρχείων",
"settings.general.row.showFileTree.description": "Εμφάνιση του πίνακα δέντρου αρχείων σε περιόδους λειτουργίας",
"settings.general.row.showNavigation.title": "Στοιχεία ελέγχου πλοήγησης",
+37 -1
View File
@@ -91,6 +91,10 @@ export const dict = {
"command.session.compact.description": "Summarize the session to reduce context size",
"command.session.fork": "Fork from message",
"command.session.fork.description": "Create a new session from a previous message",
"command.session.share": "Share session",
"command.session.share.description": "Share this session and copy the URL to clipboard",
"command.session.unshare": "Unshare session",
"command.session.unshare.description": "Stop sharing this session",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
@@ -542,6 +546,11 @@ export const dict = {
"toast.context.noLineSelection.title": "No line selection",
"toast.context.noLineSelection.description": "Select a line range in a file tab first.",
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
"toast.session.share.success.title": "Session shared",
"toast.session.share.success.description": "Share URL copied to clipboard!",
"toast.session.share.failed.title": "Failed to share session",
"toast.session.share.failed.description": "An error occurred while sharing the session",
"toast.session.unshare.success.title": "Session unshared",
"toast.session.unshare.success.description": "Session unshared successfully!",
@@ -698,6 +707,17 @@ export const dict = {
"session.question.restore": "Restore question",
"session.question.pending.one": "{{count}} pending question",
"session.question.pending.other": "{{count}} pending questions",
"session.followupDock.summary.one": "{{count}} queued message",
"session.followupDock.summary.other": "{{count}} queued messages",
"session.followupDock.sendNow": "Send now",
"session.followupDock.edit": "Edit",
"session.followupDock.collapse": "Collapse queued messages",
"session.followupDock.expand": "Expand queued messages",
"session.revertDock.summary.one": "{{count}} rolled back message",
"session.revertDock.summary.other": "{{count}} rolled back messages",
"session.revertDock.collapse": "Collapse rolled back messages",
"session.revertDock.expand": "Expand rolled back messages",
"session.revertDock.restore": "Restore message",
"session.new.title": "Build anything",
"session.new.project.new": "New project",
@@ -748,7 +768,19 @@ export const dict = {
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Manage servers",
"common.copied": "Copied",
"session.share.popover.title": "Publish on web",
"session.share.popover.description.shared":
"This session is public on the web. It is accessible to anyone with the link.",
"session.share.popover.description.unshared":
"Share session publicly on the web. It will be accessible to anyone with the link.",
"session.share.action.share": "Share",
"session.share.action.publish": "Publish",
"session.share.action.publishing": "Publishing...",
"session.share.action.unpublish": "Unpublish",
"session.share.action.unpublishing": "Unpublishing...",
"session.share.action.view": "View",
"session.share.copy.copied": "Copied",
"session.share.copy.copyLink": "Copy link",
"lsp.tooltip.none": "No LSP servers",
"lsp.label.connected": "{{count}} LSP",
@@ -920,6 +952,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
"settings.general.row.uiFont.title": "UI Font",
"settings.general.row.uiFont.description": "Customise the font used throughout the interface",
"settings.general.row.followup.title": "Follow-up behavior",
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
"settings.general.row.followup.option.queue": "Queue",
"settings.general.row.followup.option.steer": "Steer",
"settings.general.row.showFileTree.title": "File tree",
"settings.general.row.showFileTree.description": "Show the file tree panel in sessions",
"settings.general.row.showNavigation.title": "Navigation controls",
+40 -1
View File
@@ -188,6 +188,10 @@ export const dict = {
"command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto",
"command.session.fork": "Bifurcar desde mensaje",
"command.session.fork.description": "Crear una nueva sesión desde un mensaje anterior",
"command.session.share": "Compartir sesión",
"command.session.share.description": "Compartir esta sesión y copiar la URL al portapapeles",
"command.session.unshare": "Dejar de compartir sesión",
"command.session.unshare.description": "Dejar de compartir esta sesión",
"command.session.export": "Exportar sesión",
"command.session.export.description": "Exportar la transcripción completa de la sesión como JSON",
@@ -633,6 +637,11 @@ export const dict = {
"toast.context.noLineSelection.title": "Sin selección de líneas",
"toast.context.noLineSelection.description": "Primero selecciona un rango de líneas en una pestaña de archivo.",
"toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles",
"toast.session.share.success.title": "Sesión compartida",
"toast.session.share.success.description": "Enlace para compartir copiado al portapapeles.",
"toast.session.share.failed.title": "Fallo al compartir sesión",
"toast.session.share.failed.description": "Ocurrió un error al compartir la sesión",
"toast.session.unshare.success.title": "La sesión dejó de compartirse",
"toast.session.unshare.success.description": "La sesión dejó de compartirse correctamente.",
@@ -766,6 +775,19 @@ export const dict = {
"session.question.pending.one": "{{count}} pregunta pendiente",
"session.question.pending.many": "{{count}} de preguntas pendientes",
"session.question.pending.other": "{{count}} preguntas pendientes",
"session.followupDock.summary.one": "{{count}} mensaje en cola",
"session.followupDock.summary.many": "{{count}} de mensajes en cola",
"session.followupDock.summary.other": "{{count}} mensajes en cola",
"session.followupDock.sendNow": "Enviar ahora",
"session.followupDock.edit": "Editar",
"session.followupDock.collapse": "Contraer mensajes en cola",
"session.followupDock.expand": "Expandir mensajes en cola",
"session.revertDock.summary.one": "{{count}} mensaje revertido",
"session.revertDock.summary.many": "{{count}} de mensajes revertidos",
"session.revertDock.summary.other": "{{count}} mensajes revertidos",
"session.revertDock.collapse": "Contraer mensajes revertidos",
"session.revertDock.expand": "Expandir mensajes revertidos",
"session.revertDock.restore": "Restaurar mensaje",
"session.new.title": "Construye lo que quieras",
"session.new.project.new": "Nuevo proyecto",
@@ -797,7 +819,19 @@ export const dict = {
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Gestionar servidores",
"common.copied": "Copiado",
"session.share.popover.title": "Publicar en la web",
"session.share.popover.description.shared":
"Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.",
"session.share.popover.description.unshared":
"Compartir sesión públicamente en la web. Será accesible para cualquiera con el enlace.",
"session.share.action.share": "Compartir",
"session.share.action.publish": "Publicar",
"session.share.action.publishing": "Publicando...",
"session.share.action.unpublish": "Despublicar",
"session.share.action.unpublishing": "Despublicando...",
"session.share.action.view": "Ver",
"session.share.copy.copied": "Copiado",
"session.share.copy.copyLink": "Copiar enlace",
"lsp.tooltip.none": "Sin servidores LSP",
"lsp.label.connected": "{{count}} LSP",
@@ -883,6 +917,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Personaliza la fuente utilizada en el terminal",
"settings.general.row.uiFont.title": "Fuente de la interfaz",
"settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
"settings.general.row.followup.title": "Comportamiento de seguimiento",
"settings.general.row.followup.description":
"Elige si los prompts de seguimiento se dirigen inmediatamente o esperan en una cola",
"settings.general.row.followup.option.queue": "Cola",
"settings.general.row.followup.option.steer": "Dirigir",
"settings.general.row.showFileTree.title": "Árbol de archivos",
"settings.general.row.showFileTree.description": "Mostrar el panel del árbol de archivos en las sesiones",
"settings.general.row.showNavigation.title": "Controles de navegación",
+37 -1
View File
@@ -174,6 +174,10 @@ export const dict = {
"command.session.compact.description": "Konteksti suuruse vähendamiseks tehke seansist kokkuvõte",
"command.session.fork": "Sõnumi kahvel",
"command.session.fork.description": "Looge eelmisest sõnumist uus seanss",
"command.session.share": "Jaga seanssi",
"command.session.share.description": "Jagage seda seanssi ja kopeerige URL lõikelauale",
"command.session.unshare": "Tühista seansi jagamine",
"command.session.unshare.description": "Lõpetage selle seansi jagamine",
"command.session.export": "Ekspordiseanss",
"command.session.export.description": "Ekspordi kogu seansi transkriptsioon kui JSON",
"palette.search.placeholder": "Otsige faile, käske ja seansse",
@@ -586,6 +590,11 @@ export const dict = {
"toast.file.listFailed.title": "Failide loetlemine ebaõnnestus",
"toast.context.noLineSelection.title": "Rea valikut pole",
"toast.context.noLineSelection.description": "Valige faili vahekaardilt esmalt reavahemik.",
"toast.session.share.copyFailed.title": "URL lõikelauale kopeerimine ebaõnnestus",
"toast.session.share.success.title": "Seanss jagatud",
"toast.session.share.success.description": "Jaga URL kopeeriti lõikelauale!",
"toast.session.share.failed.title": "Seansi jagamine ebaõnnestus",
"toast.session.share.failed.description": "Seansi jagamisel ilmnes viga",
"toast.session.unshare.success.title": "Seansi jagamine tühistati",
"toast.session.unshare.success.description": "Seansi jagamise tühistamine õnnestus!",
"toast.session.unshare.failed.title": "Seansi jagamise tühistamine ebaõnnestus",
@@ -716,6 +725,17 @@ export const dict = {
"session.question.restore": "Taasta küsimus",
"session.question.pending.one": "{{count}} ootel küsimus",
"session.question.pending.other": "{{count}} ootel küsimust",
"session.followupDock.summary.one": "{{count}} järjekorda pandud sõnum",
"session.followupDock.summary.other": "{{count}} järjekorras olevat sõnumit",
"session.followupDock.sendNow": "Saada kohe",
"session.followupDock.edit": "Muuda",
"session.followupDock.collapse": "Ahenda järjekorda pandud kirjad",
"session.followupDock.expand": "Laiendage järjekorras olevaid sõnumeid",
"session.revertDock.summary.one": "{{count}} tagasikeeratud sõnum",
"session.revertDock.summary.other": "{{count}} tagasipööratud sõnumit",
"session.revertDock.collapse": "Ahenda tagasi keritud sõnumid",
"session.revertDock.expand": "Laienda tagasipööratud sõnumeid",
"session.revertDock.restore": "Taasta sõnum",
"session.new.title": "Ehitage midagi",
"session.new.project.new": "Uus projekt",
"session.new.project.search": "Otsige projekte",
@@ -762,7 +782,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Pluginad",
"status.popover.action.manageServers": "Hallake servereid",
"common.copied": "Kopeeritud",
"session.share.popover.title": "Avalda veebis",
"session.share.popover.description.shared":
"See seanss on veebis avalik. See on juurdepääsetav kõigile, kellel on link.",
"session.share.popover.description.unshared":
"Jagage seanssi avalikult veebis. See on juurdepääsetav kõigile, kellel on link.",
"session.share.action.share": "Jaga",
"session.share.action.publish": "Avalda",
"session.share.action.publishing": "Avaldamine...",
"session.share.action.unpublish": "Tühista avaldamine",
"session.share.action.unpublishing": "Avaldamise tühistamine...",
"session.share.action.view": "Vaade",
"session.share.copy.copied": "Kopeeritud",
"session.share.copy.copyLink": "Kopeeri link",
"lsp.tooltip.none": "LSP serverit pole",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Viipa laadimine...",
@@ -891,6 +923,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Kohandage terminalis kasutatavat fonti",
"settings.general.row.uiFont.title": "Kasutajaliidese font",
"settings.general.row.uiFont.description": "Kohandage kogu liideses kasutatavat fonti",
"settings.general.row.followup.title": "Järelkäitumine",
"settings.general.row.followup.description": "Valige, kas järelviibad juhivad kohe või oodake järjekorras",
"settings.general.row.followup.option.queue": "Järjekord",
"settings.general.row.followup.option.steer": "Juhtida",
"settings.general.row.showFileTree.title": "Failipuu",
"settings.general.row.showFileTree.description": "Failipuu paneeli kuvamine seanssides",
"settings.general.row.showNavigation.title": "Navigeerimisnupud",
+38 -1
View File
@@ -175,6 +175,10 @@ export const dict = {
"command.session.compact.description": "برای کاهش اندازه زمینه، جلسه را خلاصه کنید",
"command.session.fork": "فورک از پیام",
"command.session.fork.description": "یک جلسه جدید از پیام قبلی ایجاد کنید",
"command.session.share": "جلسه را به اشتراک بگذارید",
"command.session.share.description": "این جلسه را به اشتراک بگذارید و URL را در کلیپ بورد کپی کنید",
"command.session.unshare": "لغو اشتراک‌گذاری جلسه",
"command.session.unshare.description": "اشتراک‌گذاری این جلسه را متوقف کنید",
"command.session.export": "جلسه صادرات",
"command.session.export.description": "رونوشت کامل جلسه را به عنوان JSON صادر کنید",
"palette.search.placeholder": "فایل ها، دستورات و جلسات را جستجو کنید",
@@ -588,6 +592,11 @@ export const dict = {
"toast.file.listFailed.title": "لیست کردن فایل ها انجام نشد",
"toast.context.noLineSelection.title": "بدون انتخاب خط",
"toast.context.noLineSelection.description": "ابتدا یک محدوده خط را در یک برگه فایل انتخاب کنید.",
"toast.session.share.copyFailed.title": "URL در کلیپ بورد کپی نشد",
"toast.session.share.success.title": "جلسه به اشتراک گذاشته شد",
"toast.session.share.success.description": "اشتراک گذاری URL کپی شده در کلیپ بورد!",
"toast.session.share.failed.title": "جلسه اشتراک گذاری نشد",
"toast.session.share.failed.description": "هنگام اشتراک‌گذاری جلسه خطایی روی داد",
"toast.session.unshare.success.title": "جلسه لغو اشتراک‌گذاری شد",
"toast.session.unshare.success.description": "جلسه با موفقیت لغو اشتراک گذاری شد!",
"toast.session.unshare.failed.title": "لغو اشتراک‌گذاری جلسه انجام نشد",
@@ -718,6 +727,17 @@ export const dict = {
"session.question.restore": "بازیابی سوال",
"session.question.pending.one": "سوال معلق {{count}}",
"session.question.pending.other": "{{count}} سوالات معلق",
"session.followupDock.summary.one": "پیام {{count}} در صف",
"session.followupDock.summary.other": "پیغام های در صف {{count}}",
"session.followupDock.sendNow": "اکنون ارسال کنید",
"session.followupDock.edit": "ویرایش کنید",
"session.followupDock.collapse": "کوچک کردن پیام‌های در صف",
"session.followupDock.expand": "پیام های در صف را بزرگ کنید",
"session.revertDock.summary.one": "پیام {{count}} برگشت داده شد",
"session.revertDock.summary.other": "{{count}} پیام‌های برگشتی",
"session.revertDock.collapse": "کوچک کردن پیام‌های برگشتی",
"session.revertDock.expand": "گسترش پیام‌های برگشتی",
"session.revertDock.restore": "بازیابی پیام",
"session.new.title": "هر چیزی بساز",
"session.new.project.new": "پروژه جدید",
"session.new.project.search": "جستجوی پروژه ها",
@@ -764,7 +784,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "پلاگین ها",
"status.popover.action.manageServers": "مدیریت سرورها",
"common.copied": "کپی شد",
"session.share.popover.title": "در وب منتشر کنید",
"session.share.popover.description.shared":
"این جلسه در وب عمومی است. برای هر کسی که پیوند را داشته باشد قابل دسترسی است.",
"session.share.popover.description.unshared":
"جلسه را به صورت عمومی در وب به اشتراک بگذارید. برای هر کسی که پیوند را داشته باشد قابل دسترسی خواهد بود.",
"session.share.action.share": "به اشتراک بگذارید",
"session.share.action.publish": "منتشر کنید",
"session.share.action.publishing": "در حال انتشار...",
"session.share.action.unpublish": "لغو انتشار",
"session.share.action.unpublishing": "در حال لغو انتشار...",
"session.share.action.view": "مشاهده کنید",
"session.share.copy.copied": "کپی شد",
"session.share.copy.copyLink": "لینک را کپی کنید",
"lsp.tooltip.none": "بدون سرور LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "در حال بارگیری درخواست...",
@@ -891,6 +923,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "فونت مورد استفاده در ترمینال را سفارشی کنید",
"settings.general.row.uiFont.title": "قلم UI",
"settings.general.row.uiFont.description": "فونت مورد استفاده در سراسر رابط را سفارشی کنید",
"settings.general.row.followup.title": "رفتار پیگیری",
"settings.general.row.followup.description":
"انتخاب کنید که آیا درخواست پیگیری فوراً هدایت می شود یا در یک صف منتظر می ماند",
"settings.general.row.followup.option.queue": "صف",
"settings.general.row.followup.option.steer": "هدایت کنید",
"settings.general.row.showFileTree.title": "درخت فایل",
"settings.general.row.showFileTree.description": "پانل درخت فایل را در جلسات نمایش دهید",
"settings.general.row.showNavigation.title": "کنترل های ناوبری",
+37 -1
View File
@@ -81,6 +81,10 @@ export const dict = {
"command.session.compact.description": "Tee yhteenveto istunnosta pienentääksesi kontekstin kokoa",
"command.session.fork": "Haarauta viestistä",
"command.session.fork.description": "Luo uusi istunto edellisestä viestistä",
"command.session.share": "Jaa istunto",
"command.session.share.description": "Jaa tämä istunto ja kopioi URL-osoite leikepöydälle",
"command.session.unshare": "Peru istunnon jakaminen",
"command.session.unshare.description": "Lopeta tämän istunnon jakaminen",
"command.session.export": "Vie istunto",
"command.session.export.description": "Vie istunnon koko transkriptio JSON-muodossa",
@@ -499,6 +503,11 @@ export const dict = {
"toast.file.listFailed.title": "Tiedostojen luettelointi epäonnistui",
"toast.context.noLineSelection.title": "Ei rivivalintaa",
"toast.context.noLineSelection.description": "Valitse ensin riviväli tiedostovälilehdeltä.",
"toast.session.share.copyFailed.title": "URL-osoitteen kopioiminen leikepöydälle epäonnistui",
"toast.session.share.success.title": "Istunto jaettu",
"toast.session.share.success.description": "Jakolinkki kopioitu leikepöydälle!",
"toast.session.share.failed.title": "Istunnon jakaminen epäonnistui",
"toast.session.share.failed.description": "Istunnon jakamisessa tapahtui virhe",
"toast.session.unshare.success.title": "Istunnon jako peruttu",
"toast.session.unshare.success.description": "Istunnon jakaminen peruutettu onnistuneesti!",
"toast.session.unshare.failed.title": "Istunnon jakamisen peruuttaminen epäonnistui",
@@ -632,6 +641,17 @@ export const dict = {
"session.question.restore": "Palauta kysymys",
"session.question.pending.one": "{{count}} odottava kysymys",
"session.question.pending.other": "{{count}} odottavaa kysymystä",
"session.followupDock.summary.one": "{{count}} jonossa oleva viesti",
"session.followupDock.summary.other": "{{count}} jonossa olevaa viestiä",
"session.followupDock.sendNow": "Lähetä nyt",
"session.followupDock.edit": "Muokkaa",
"session.followupDock.collapse": "Kutista jonossa olevat viestit",
"session.followupDock.expand": "Laajenna jonossa olevia viestejä",
"session.revertDock.summary.one": "{{count}} palautettu viesti",
"session.revertDock.summary.other": "{{count}} palautettua viestiä",
"session.revertDock.collapse": "Kutista palautetut viestit",
"session.revertDock.expand": "Laajenna palautetut viestit",
"session.revertDock.restore": "Palauta viesti",
"session.new.title": "Rakenna mitä tahansa",
"session.new.project.new": "Uusi projekti",
"session.new.project.search": "Etsi projekteja",
@@ -678,7 +698,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Laajennukset",
"status.popover.action.manageServers": "Hallinnoi palvelimia",
"common.copied": "Kopioitu",
"session.share.popover.title": "Julkaise verkossa",
"session.share.popover.description.shared":
"Tämä istunto on julkinen verkossa. Se on kaikkien linkin saaneiden käytettävissä.",
"session.share.popover.description.unshared":
"Jaa istunto julkisesti verkossa. Se on kaikkien linkin saaneiden käytettävissä.",
"session.share.action.share": "Jaa",
"session.share.action.publish": "Julkaise",
"session.share.action.publishing": "Julkaistaan...",
"session.share.action.unpublish": "Peruuta julkaisu",
"session.share.action.unpublishing": "Peruutetaan julkaisua...",
"session.share.action.view": "Näytä",
"session.share.copy.copied": "Kopioitu",
"session.share.copy.copyLink": "Kopioi linkki",
"lsp.tooltip.none": "Ei LSP-palvelimia",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Ladataan kehotetta...",
@@ -809,6 +841,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Mukauta terminaalissa käytettävää fonttia",
"settings.general.row.uiFont.title": "Käyttöliittymän fontti",
"settings.general.row.uiFont.description": "Mukauta koko käyttöliittymässä käytettyä fonttia",
"settings.general.row.followup.title": "Jatkokehotteiden toiminta",
"settings.general.row.followup.description": "Valitse, ohjaavatko jatkokehotteet heti vai odottavatko ne jonossa",
"settings.general.row.followup.option.queue": "Jonota",
"settings.general.row.followup.option.steer": "Ohjaa heti",
"settings.general.row.showFileTree.title": "Tiedostopuu",
"settings.general.row.showFileTree.description": "Näytä tiedostopuupaneeli istunnoissa",
"settings.general.row.showNavigation.title": "Navigointiohjaimet",
+37 -1
View File
@@ -174,6 +174,10 @@ export const dict = {
"command.session.compact.description": "Samanber setuna fyri at minka um samanhangsstøddina",
"command.session.fork": "Greina frá boðum",
"command.session.fork.description": "Stovna eina nýggja setu frá einum undanfarnum boði",
"command.session.share": "Deil setu",
"command.session.share.description": "Deil hesa setuna og kopiera URL á útklippiborðið",
"command.session.unshare": "Avdeila setu",
"command.session.unshare.description": "Lat vera við at deila hesa setuna",
"command.session.export": "Útflutningsløta",
"command.session.export.description": "Eksportera alt setuuppskriftið sum JSON",
"palette.search.placeholder": "Leita eftir fílum, skipanum og setum",
@@ -588,6 +592,11 @@ export const dict = {
"toast.file.listFailed.title": "Tað eydnaðist ikki at lista fílur",
"toast.context.noLineSelection.title": "Einki linjuval",
"toast.context.noLineSelection.description": "Vel eitt linjuøki í einum fíluflipa fyrst.",
"toast.session.share.copyFailed.title": "Tað eydnaðist ikki at avrita URL til klippiborð",
"toast.session.share.success.title": "Setan deild",
"toast.session.share.success.description": "Deil URL avritað á klippiborð!",
"toast.session.share.failed.title": "Tað eydnaðist ikki at deila setu",
"toast.session.share.failed.description": "Ein feilur hendi, meðan setan varð deild.",
"toast.session.unshare.success.title": "Setan ódeild",
"toast.session.unshare.success.description": "Setan ódeild eydnaðist!",
"toast.session.unshare.failed.title": "Tað eydnaðist ikki at avdeila setuna",
@@ -719,6 +728,17 @@ export const dict = {
"session.question.restore": "Endurnýggja spurningin",
"session.question.pending.one": "{{count}} spurningur í bíðini",
"session.question.pending.other": "{{count}} bíðandi spurningar",
"session.followupDock.summary.one": "{{count}} bíðirøð boð",
"session.followupDock.summary.other": "{{count}} bíðirøð boð",
"session.followupDock.sendNow": "Send nú",
"session.followupDock.edit": "Rætta",
"session.followupDock.collapse": "Samla boð í bíðirøð",
"session.followupDock.expand": "Víðka boð í bíðirøð",
"session.revertDock.summary.one": "{{count}} rullað afturboð",
"session.revertDock.summary.other": "{{count}} rullað boð aftur",
"session.revertDock.collapse": "Kollaps afturrullað boð",
"session.revertDock.expand": "Víðka afturrullað boð",
"session.revertDock.restore": "Endurnýggja boð",
"session.new.title": "Bygg alt",
"session.new.project.new": "Nýggj verkætlan",
"session.new.project.search": "Leita verkætlanir",
@@ -765,7 +785,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Tilskot",
"status.popover.action.manageServers": "Umsita ambætarar",
"common.copied": "Avritað",
"session.share.popover.title": "Útgeva á netinum",
"session.share.popover.description.shared":
"Henda setan er almenn á netinum. Tað er atkomiligt fyri øll, sum hava leinkjuna.",
"session.share.popover.description.unshared":
"Deil setu alment á netinum. Tað verður atkomiligt fyri øll, sum hava leinkjuna.",
"session.share.action.share": "Deil",
"session.share.action.publish": "Útgeva",
"session.share.action.publishing": "Útgáva...",
"session.share.action.unpublish": "Fráútgeva",
"session.share.action.unpublishing": "Avútgáva...",
"session.share.action.view": "Vís",
"session.share.copy.copied": "Avritað",
"session.share.copy.copyLink": "Avrita leinkju",
"lsp.tooltip.none": "Ongar LSP ambætarar",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Heinta boð...",
@@ -893,6 +925,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Tillaga skriftslagið, sum verður brúkt í terminalinum",
"settings.general.row.uiFont.title": "UI Skrift",
"settings.general.row.uiFont.description": "Tillaga skriftslagið, sum verður brúkt í øllum nýtsluflatanum",
"settings.general.row.followup.title": "Eftirfylgjandi atferð",
"settings.general.row.followup.description": "Vel um eftirfylgjandi boðini stýra beinanvegin ella bíða í bíðirøð",
"settings.general.row.followup.option.queue": "Bíðirøð",
"settings.general.row.followup.option.steer": "Stýr",
"settings.general.row.showFileTree.title": "Fílutræ",
"settings.general.row.showFileTree.description": "Vís fílutræpanelið í setum",
"settings.general.row.showNavigation.title": "Navigatiónsstýringar",
+40 -1
View File
@@ -182,6 +182,10 @@ export const dict = {
"command.session.compact.description": "Résumer la session pour réduire la taille du contexte",
"command.session.fork": "Bifurquer à partir du message",
"command.session.fork.description": "Créer une nouvelle session à partir d'un message précédent",
"command.session.share": "Partager la session",
"command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers",
"command.session.unshare": "Ne plus partager la session",
"command.session.unshare.description": "Arrêter de partager cette session",
"command.session.export": "Exporter la session",
"command.session.export.description": "Exporter la transcription intégrale de la session au format JSON",
@@ -594,6 +598,11 @@ export const dict = {
"toast.file.listFailed.title": "Échec de la liste des fichiers",
"toast.context.noLineSelection.title": "Aucune sélection de lignes",
"toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.",
"toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers",
"toast.session.share.success.title": "Session partagée",
"toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !",
"toast.session.share.failed.title": "Échec du partage de la session",
"toast.session.share.failed.description": "Une erreur s'est produite lors du partage de la session",
"toast.session.unshare.success.title": "Partage de la session désactivé",
"toast.session.unshare.success.description": "Le partage de la session a bien été désactivé.",
"toast.session.unshare.failed.title": "Échec de la désactivation du partage",
@@ -717,6 +726,19 @@ export const dict = {
"session.question.pending.one": "{{count}} question en attente",
"session.question.pending.many": "{{count}} de questions en attente",
"session.question.pending.other": "{{count}} questions en attente",
"session.followupDock.summary.one": "{{count}} message en file d'attente",
"session.followupDock.summary.many": "{{count}} de messages en file d'attente",
"session.followupDock.summary.other": "{{count}} messages en file d'attente",
"session.followupDock.sendNow": "Envoyer maintenant",
"session.followupDock.edit": "Modifier",
"session.followupDock.collapse": "Réduire les messages en file d'attente",
"session.followupDock.expand": "Développer les messages en file d'attente",
"session.revertDock.summary.one": "{{count}} message annulé",
"session.revertDock.summary.many": "{{count}} de messages annulés",
"session.revertDock.summary.other": "{{count}} messages annulés",
"session.revertDock.collapse": "Réduire les messages annulés",
"session.revertDock.expand": "Développer les messages annulés",
"session.revertDock.restore": "Restaurer le message",
"session.new.title": "Créez ce que vous voulez",
"session.new.project.new": "Nouveau projet",
"session.new.project.search": "Rechercher des projets",
@@ -744,7 +766,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Gérer les serveurs",
"common.copied": "Copié",
"session.share.popover.title": "Publier sur le Web",
"session.share.popover.description.shared":
"Cette session est publique sur le Web. Elle est accessible à toute personne disposant du lien.",
"session.share.popover.description.unshared":
"Rendre la session publique sur le Web. Elle sera accessible à toute personne disposant du lien.",
"session.share.action.share": "Partager",
"session.share.action.publish": "Publier",
"session.share.action.publishing": "Publication...",
"session.share.action.unpublish": "Dépublier",
"session.share.action.unpublishing": "Dépublication...",
"session.share.action.view": "Voir",
"session.share.copy.copied": "Copié",
"session.share.copy.copyLink": "Copier le lien",
"lsp.tooltip.none": "Aucun serveur LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Chargement de l'invite...",
@@ -819,6 +853,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Personnalisez la police utilisée dans le terminal",
"settings.general.row.uiFont.title": "Police de l'interface",
"settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
"settings.general.row.followup.title": "Comportement des messages de suivi",
"settings.general.row.followup.description":
"Choisissez si les messages de suivi orientent immédiatement l'agent ou sont placés dans une file d'attente",
"settings.general.row.followup.option.queue": "File d'attente",
"settings.general.row.followup.option.steer": "Orienter",
"settings.general.row.showFileTree.title": "Arborescence des fichiers",
"settings.general.row.showFileTree.description": "Afficher le panneau d'arborescence des fichiers dans les sessions",
"settings.general.row.showNavigation.title": "Commandes de navigation",
+37 -1
View File
@@ -175,6 +175,10 @@ export const dict = {
"command.session.compact.description": "סכם את ההפעלה כדי להקטין את גודל ההקשר",
"command.session.fork": "יצירת הפעלה מהודעה",
"command.session.fork.description": "צור הפעלה חדשה מהודעה קודמת",
"command.session.share": "שתף הפעלה",
"command.session.share.description": "שתף את ההפעלה הזו והעתק את ה-URL ללוח",
"command.session.unshare": "בטל שיתוף הפעלה",
"command.session.unshare.description": "הפסק לשתף את ההפעלה הזו",
"command.session.export": "ייצוא ההפעלה",
"command.session.export.description": "ייצא את תמליל ההפעלה המלא כ-JSON",
"palette.search.placeholder": "חפש קבצים, פקודות והפעלות",
@@ -585,6 +589,11 @@ export const dict = {
"toast.file.listFailed.title": "רישום הקבצים נכשל",
"toast.context.noLineSelection.title": "אין בחירת שורה",
"toast.context.noLineSelection.description": "בחר תחילה טווח שורות בכרטיסיית קובץ.",
"toast.session.share.copyFailed.title": "ההעתקה של URL ללוח נכשלה",
"toast.session.share.success.title": "ההפעלה שותפה",
"toast.session.share.success.description": "הקישור לשיתוף הועתק ללוח!",
"toast.session.share.failed.title": "שיתוף ההפעלה נכשל",
"toast.session.share.failed.description": "אירעה שגיאה במהלך שיתוף ההפעלה",
"toast.session.unshare.success.title": "שיתוף ההפעלה בוטל",
"toast.session.unshare.success.description": "שיתוף ההפעלה בוטל בהצלחה!",
"toast.session.unshare.failed.title": "ביטול השיתוף של ההפעלה נכשל",
@@ -724,6 +733,17 @@ export const dict = {
"session.question.restore": "שחזר שאלה",
"session.question.pending.one": "{{count}} שאלה ממתינה",
"session.question.pending.other": "{{count}} שאלות ממתינות",
"session.followupDock.summary.one": "{{count}} הודעה בתור",
"session.followupDock.summary.other": "{{count}} הודעות בתור",
"session.followupDock.sendNow": "שלח עכשיו",
"session.followupDock.edit": "ערוך",
"session.followupDock.collapse": "כווץ הודעות בתור",
"session.followupDock.expand": "הרחב הודעות בתור",
"session.revertDock.summary.one": "{{count}} הודעה בוטלה",
"session.revertDock.summary.other": "{{count}} הודעות בוטלו",
"session.revertDock.collapse": "כווץ הודעות שהוחזרו לאחור",
"session.revertDock.expand": "הרחב הודעות שהוחזרו לאחור",
"session.revertDock.restore": "שחזר הודעה",
"session.new.title": "בנה כל דבר",
"session.new.project.new": "פרויקט חדש",
"session.new.project.search": "חיפוש פרויקטים",
@@ -770,7 +790,17 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "תוספים",
"status.popover.action.manageServers": "ניהול שרתים",
"common.copied": "הועתק",
"session.share.popover.title": "פרסם באינטרנט",
"session.share.popover.description.shared": "ההפעלה מפורסמת באינטרנט וזמינה לכל מי שיש לו את הקישור.",
"session.share.popover.description.unshared": "פרסום ההפעלה באינטרנט כך שתהיה זמינה לכל מי שיש לו את הקישור.",
"session.share.action.share": "שתף",
"session.share.action.publish": "פרסם",
"session.share.action.publishing": "מפרסם...",
"session.share.action.unpublish": "בטל את הפרסום",
"session.share.action.unpublishing": "מבטל את הפרסום...",
"session.share.action.view": "הצג",
"session.share.copy.copied": "הועתק",
"session.share.copy.copyLink": "העתק קישור",
"lsp.tooltip.none": "אין שרתי LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "טוען פרומפט...",
@@ -927,6 +957,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "התאם אישית את הגופן המשמש במסוף",
"settings.general.row.uiFont.title": "גופן ממשק משתמש",
"settings.general.row.uiFont.description": "התאם אישית את הגופן המשמש בכל הממשק",
"settings.general.row.followup.title": "התנהגות פרומפטים עוקבים",
"settings.general.row.followup.description": "בחירה אם פרומפטים עוקבים יכוונו מיד את הסוכן או ימתינו בתור",
"settings.general.row.followup.option.queue": "תור",
"settings.general.row.followup.option.steer": "כוון מיד",
"settings.general.row.showFileTree.title": "עץ קבצים",
"settings.general.row.showFileTree.description": "הצג את חלונית עץ הקבצים בהפעלות",
"settings.general.row.showNavigation.title": "בקרות ניווט",
@@ -1165,6 +1199,8 @@ export const dict = {
"workspace.reset.archived.many": "{{count}} הפעלות יועברו לארכיון.",
"workspace.reset.note": "פעולה זו תאפס את סביבת העבודה כך שתתאים לענף ברירת המחדל.",
"session.question.pending.two": "{{count}} שאלות ממתינות",
"session.followupDock.summary.two": "{{count}} הודעות בתור",
"session.revertDock.summary.two": "{{count}} הודעות בוטלו",
"settings.workspaces.count.two": "{{count}} סביבות עבודה",
"settings.workspaces.sessions.two": "{{count}} הפעלות ב-{{project}}",
"session.background.shell.two": "{{count}} פקודות מעטפת",
+38 -1
View File
@@ -181,6 +181,10 @@ export const dict = {
"command.session.compact.description": "कॉन्टेक्स्ट आकार को कम करने के लिए सेशन को सारांशित करें",
"command.session.fork": "संदेश से फ़ोर्क",
"command.session.fork.description": "पिछले संदेश से एक नया सेशन बनाएं",
"command.session.share": "सेशन साझा करें",
"command.session.share.description": "इस सेशन को साझा करें और URL को क्लिपबोर्ड पर कॉपी करें",
"command.session.unshare": "सेशन साझा करना बंद करें",
"command.session.unshare.description": "इस सेशन को साझा करना बंद करें",
"command.session.export": "सेशन निर्यात करें",
"command.session.export.description": "सेशन की पूरी ट्रांसक्रिप्ट को JSON के रूप में निर्यात करें",
@@ -598,6 +602,11 @@ export const dict = {
"toast.file.listFailed.title": "फ़ाइलें सूचीबद्ध करने में विफल",
"toast.context.noLineSelection.title": "कोई पंक्ति चयन नहीं",
"toast.context.noLineSelection.description": "पहले फ़ाइल टैब में एक पंक्ति श्रेणी का चयन करें।",
"toast.session.share.copyFailed.title": "URL को क्लिपबोर्ड पर कॉपी करने में विफल",
"toast.session.share.success.title": "सेशन साझा किया गया",
"toast.session.share.success.description": "साझा URL क्लिपबोर्ड पर कॉपी किया गया!",
"toast.session.share.failed.title": "सेशन साझा करने में विफल",
"toast.session.share.failed.description": "सेशन साझा करते समय एक त्रुटि उत्पन्न हुई",
"toast.session.unshare.success.title": "सेशन अनशेयर किया गया",
"toast.session.unshare.success.description": "सेशन सफलतापूर्वक अनशेयर किया गया!",
"toast.session.unshare.failed.title": "सेशन को अनशेयर करने में विफल",
@@ -730,6 +739,17 @@ export const dict = {
"session.question.restore": "प्रश्न पुनर्स्थापित करें",
"session.question.pending.one": "{{count}} लंबित प्रश्न",
"session.question.pending.other": "{{count}} लंबित प्रश्न",
"session.followupDock.summary.one": "कतार में {{count}} संदेश",
"session.followupDock.summary.other": "कतार में {{count}} संदेश",
"session.followupDock.sendNow": "अब भेजें",
"session.followupDock.edit": "संपादित करें",
"session.followupDock.collapse": "कतार के संदेश संकुचित करें",
"session.followupDock.expand": "कतार के संदेश विस्तृत करें",
"session.revertDock.summary.one": "{{count}} संदेश वापस लिया गया",
"session.revertDock.summary.other": "{{count}} संदेश वापस लिए गए",
"session.revertDock.collapse": "रोलबैक संदेशों को संक्षिप्त करें",
"session.revertDock.expand": "वापस लाये गए संदेशों का विस्तार करें",
"session.revertDock.restore": "संदेश पुनर्स्थापित करें",
"session.new.title": "कुछ भी बनाएँ",
"session.new.project.new": "नया प्रोजेक्ट",
"session.new.project.search": "प्रोजेक्ट खोजें",
@@ -776,7 +796,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "प्लग-इन",
"status.popover.action.manageServers": "सर्वर प्रबंधित करें",
"common.copied": "कॉपी किया गया",
"session.share.popover.title": "वेब पर प्रकाशित करें",
"session.share.popover.description.shared":
"यह सेशन वेब पर सार्वजनिक है। यह लिंक वाले किसी भी व्यक्ति के लिए सुलभ है।",
"session.share.popover.description.unshared":
"सेशन को वेब पर सार्वजनिक रूप से साझा करें। यह लिंक वाले किसी भी व्यक्ति के लिए सुलभ होगा।",
"session.share.action.share": "साझा करें",
"session.share.action.publish": "प्रकाशित करें",
"session.share.action.publishing": "प्रकाशित किया जा रहा है...",
"session.share.action.unpublish": "प्रकाशन हटाएँ",
"session.share.action.unpublishing": "प्रकाशन हटाया जा रहा है...",
"session.share.action.view": "देखें",
"session.share.copy.copied": "कॉपी किया गया",
"session.share.copy.copyLink": "लिंक कॉपी करें",
"lsp.tooltip.none": "कोई LSP सर्वर नहीं",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "प्रॉम्प्ट लोड हो रहा है...",
@@ -903,6 +935,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "टर्मिनल में प्रयुक्त फ़ॉन्ट को अनुकूलित करें",
"settings.general.row.uiFont.title": "UI फ़ॉन्ट",
"settings.general.row.uiFont.description": "पूरे इंटरफ़ेस में उपयोग किए गए फ़ॉन्ट को कस्टमाइज़ करें",
"settings.general.row.followup.title": "अनुवर्ती व्यवहार",
"settings.general.row.followup.description":
"चुनें कि क्या अनुवर्ती प्रॉम्प्ट तुरंत आगे बढ़ेगा या कतार में प्रतीक्षा करेगा",
"settings.general.row.followup.option.queue": "कतार",
"settings.general.row.followup.option.steer": "तुरंत निर्देश दें",
"settings.general.row.showFileTree.title": "फ़ाइल वृक्ष",
"settings.general.row.showFileTree.description": "सेशन में फ़ाइल ट्री पैनल दिखाएँ",
"settings.general.row.showNavigation.title": "नेविगेशन नियंत्रण",
+38 -1
View File
@@ -178,6 +178,10 @@ export const dict = {
"command.session.compact.description": "Sažmite sesiju kako biste smanjili veličinu konteksta",
"command.session.fork": "Odvoji od poruke",
"command.session.fork.description": "Stvorite novu sesiju iz prethodne poruke",
"command.session.share": "Podijeli sesiju",
"command.session.share.description": "Podijelite ovu sesiju i kopirajte URL u međuspremnik",
"command.session.unshare": "Poništi dijeljenje sesije",
"command.session.unshare.description": "Prestani dijeliti ovu sesiju",
"command.session.export": "Izvezi sesiju",
"command.session.export.description": "Izvezite puni transkript sesije kao JSON",
"palette.search.placeholder": "Pretraživanje datoteka, naredbi i sesija",
@@ -594,6 +598,11 @@ export const dict = {
"toast.file.listFailed.title": "Popis datoteka nije uspio",
"toast.context.noLineSelection.title": "Bez odabira linije",
"toast.context.noLineSelection.description": "Najprije odaberite raspon redaka na kartici datoteke.",
"toast.session.share.copyFailed.title": "Kopiranje URL u međuspremnik nije uspjelo",
"toast.session.share.success.title": "Sesija podijeljena",
"toast.session.share.success.description": "Podijeli URL kopirano u međuspremnik!",
"toast.session.share.failed.title": "Dijeljenje sesije nije uspjelo",
"toast.session.share.failed.description": "Došlo je do pogreške prilikom dijeljenja sesije",
"toast.session.unshare.success.title": "Dijeljenje sesije je prekinuto",
"toast.session.unshare.success.description": "Dijeljenje sesije uspješno je prekinuto.",
"toast.session.unshare.failed.title": "Poništavanje dijeljenja sesije nije uspjelo",
@@ -727,6 +736,19 @@ export const dict = {
"session.question.pending.one": "{{count}} pitanje na čekanju",
"session.question.pending.other": "{{count}} pitanja na čekanju",
"session.question.pending.few": "{{count}} pitanja na čekanju",
"session.followupDock.summary.one": "{{count}} poruka u čekanju",
"session.followupDock.summary.other": "{{count}} poruke u redu čekanja",
"session.followupDock.summary.few": "{{count}} poruke u redu čekanja",
"session.followupDock.sendNow": "Pošalji sada",
"session.followupDock.edit": "Uredi",
"session.followupDock.collapse": "Sažmi poruke u redu čekanja",
"session.followupDock.expand": "Proširi poruke u čekanju",
"session.revertDock.summary.one": "{{count}} vratio poruku",
"session.revertDock.summary.other": "{{count}} je vratio poruke",
"session.revertDock.summary.few": "{{count}} vraćene poruke",
"session.revertDock.collapse": "Sažmi vraćene poruke",
"session.revertDock.expand": "Proširi vraćene poruke",
"session.revertDock.restore": "Vrati poruku",
"session.new.title": "Gradite bilo što",
"session.new.project.new": "Novi projekt",
"session.new.project.search": "Pretražite projekte",
@@ -773,7 +795,17 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Dodaci",
"status.popover.action.manageServers": "Upravljanje poslužiteljima",
"common.copied": "Kopirano",
"session.share.popover.title": "Objavi na webu",
"session.share.popover.description.shared": "Ova je sesija javna na webu. Dostupan je svima s vezom.",
"session.share.popover.description.unshared": "Podijelite sesiju javno na webu. Bit će dostupna svima s vezom.",
"session.share.action.share": "Udio",
"session.share.action.publish": "Objaviti",
"session.share.action.publishing": "Objavljivanje...",
"session.share.action.unpublish": "Poništi objavu",
"session.share.action.unpublishing": "Poništavanje objave...",
"session.share.action.view": "Prikaz",
"session.share.copy.copied": "Kopirano",
"session.share.copy.copyLink": "Kopiraj vezu",
"lsp.tooltip.none": "Nema LSP poslužitelja",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Učitavanje upita...",
@@ -903,6 +935,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Prilagodite font koji se koristi u terminalu",
"settings.general.row.uiFont.title": "UI font",
"settings.general.row.uiFont.description": "Prilagodite font koji se koristi u cijelom sučelju",
"settings.general.row.followup.title": "Praćenje ponašanja",
"settings.general.row.followup.description":
"Odaberite hoće li se naknadni upiti usmjeravati odmah ili čekati u redu",
"settings.general.row.followup.option.queue": "Red",
"settings.general.row.followup.option.steer": "Upravljati",
"settings.general.row.showFileTree.title": "Stablo datoteka",
"settings.general.row.showFileTree.description": "Prikaži ploču stabla datoteka u sesijama",
"settings.general.row.showNavigation.title": "Navigacijske kontrole",
+37 -1
View File
@@ -178,6 +178,10 @@ export const dict = {
"command.session.compact.description": "A kontextus méretének csökkentése érdekében foglalja össze a munkamenetet",
"command.session.fork": "Elágazás az üzenettől",
"command.session.fork.description": "Hozzon létre új munkamenetet egy korábbi üzenetből",
"command.session.share": "Munkamenet megosztása",
"command.session.share.description": "Ossza meg ezt a munkamenetet, és másolja a URL fájlt a vágólapra",
"command.session.unshare": "Munkamenet megosztásának megszüntetése",
"command.session.unshare.description": "Állítsa le a munkamenet megosztását",
"command.session.export": "Exportálási munkamenet",
"command.session.export.description": "Exportálja a teljes munkamenet-átiratot JSON formátumban",
"palette.search.placeholder": "Fájlok, parancsok és munkamenetek keresése",
@@ -593,6 +597,11 @@ export const dict = {
"toast.file.listFailed.title": "Nem sikerült listázni a fájlokat",
"toast.context.noLineSelection.title": "Nincs vonalválasztás",
"toast.context.noLineSelection.description": "Először válasszon ki egy sortartományt egy fájl lapon.",
"toast.session.share.copyFailed.title": "Nem sikerült a URL vágólapra másolása",
"toast.session.share.success.title": "Munkamenet megosztva",
"toast.session.share.success.description": "Megosztás URL vágólapra másolva!",
"toast.session.share.failed.title": "Nem sikerült megosztani a munkamenetet",
"toast.session.share.failed.description": "Hiba történt a munkamenet megosztása közben",
"toast.session.unshare.success.title": "Munkamenet megosztása visszavonva",
"toast.session.unshare.success.description": "A munkamenet megosztása sikeresen visszavonva!",
"toast.session.unshare.failed.title": "Nem sikerült megszüntetni a munkamenet megosztását",
@@ -726,6 +735,17 @@ export const dict = {
"session.question.restore": "Kérdés visszaállítása",
"session.question.pending.one": "{{count}} függőben lévő kérdés",
"session.question.pending.other": "{{count}} függőben lévő kérdések",
"session.followupDock.summary.one": "{{count}} sorba állított üzenet",
"session.followupDock.summary.other": "{{count}} sorba állított üzenetek",
"session.followupDock.sendNow": "Küldje el most",
"session.followupDock.edit": "Szerkesztés",
"session.followupDock.collapse": "A sorba állított üzenetek összecsukása",
"session.followupDock.expand": "A sorban álló üzenetek kibontása",
"session.revertDock.summary.one": "{{count}} visszavont üzenet",
"session.revertDock.summary.other": "{{count}} visszavont üzenetek",
"session.revertDock.collapse": "A visszagörgetett üzenetek összecsukása",
"session.revertDock.expand": "A visszagörgetett üzenetek kibontása",
"session.revertDock.restore": "Üzenet visszaállítása",
"session.new.title": "Építsen bármit",
"session.new.project.new": "Új projekt",
"session.new.project.search": "Projektek keresése",
@@ -772,7 +792,18 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Beépülő modulok",
"status.popover.action.manageServers": "Szerverek kezelése",
"common.copied": "Másolva",
"session.share.popover.title": "Közzététel a weben",
"session.share.popover.description.shared": "Ez a munkamenet nyilvános a weben. A link birtokában bárki hozzáférhet.",
"session.share.popover.description.unshared":
"Ossza meg a munkamenetet nyilvánosan az interneten. A link birtokában bárki számára elérhető lesz.",
"session.share.action.share": "Részesedés",
"session.share.action.publish": "Közzététel",
"session.share.action.publishing": "Kiadás...",
"session.share.action.unpublish": "Közzététel visszavonása",
"session.share.action.unpublishing": "Közzététel visszavonása...",
"session.share.action.view": "Nézet",
"session.share.copy.copied": "Másolva",
"session.share.copy.copyLink": "Link másolása",
"lsp.tooltip.none": "Nincsenek LSP szerverek",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Prompt betöltése...",
@@ -904,6 +935,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Testreszabhatja a terminálban használt betűtípust",
"settings.general.row.uiFont.title": "UI betűtípus",
"settings.general.row.uiFont.description": "Testreszabhatja a felületen használt betűtípust",
"settings.general.row.followup.title": "Nyomon követési viselkedés",
"settings.general.row.followup.description":
"Válassza ki, hogy a nyomon követési felszólítások azonnal irányítsák, vagy várakozzanak a sorban",
"settings.general.row.followup.option.queue": "Sor",
"settings.general.row.followup.option.steer": "Tinó",
"settings.general.row.showFileTree.title": "Fájlfa",
"settings.general.row.showFileTree.description": "A fájlfa panel megjelenítése a munkamenetekben",
"settings.general.row.showNavigation.title": "Navigációs vezérlők",
+37 -1
View File
@@ -176,6 +176,10 @@ export const dict = {
"command.session.compact.description": "Ամփոփեք նիստը՝ համատեքստի չափը նվազեցնելու համար",
"command.session.fork": "Ստեղծել ճյուղ հաղորդագրությունից",
"command.session.fork.description": "Ստեղծել նոր նիստ նախորդ հաղորդագրությունից",
"command.session.share": "Կիսվել նիստ",
"command.session.share.description": "Կիսվել այս նիստով և պատճենել URL-ը սեղմատախտակին",
"command.session.unshare": "Դադարեցնել նիստի համօգտագործումը",
"command.session.unshare.description": "Դադարեցնել այս նիստի համօգտագործումը",
"command.session.export": "Արտահանման նիստ",
"command.session.export.description": "Արտահանել ամբողջ նիստի տառադարձումը որպես JSON",
"palette.search.placeholder": "Որոնել ֆայլեր, հրամաններ և նիստեր",
@@ -592,6 +596,11 @@ export const dict = {
"toast.file.listFailed.title": "Չհաջողվեց ցուցակագրել ֆայլերը",
"toast.context.noLineSelection.title": "Տող չկա",
"toast.context.noLineSelection.description": "Ընտրեք տողերի տիրույթը ֆայլի ներդիրում:",
"toast.session.share.copyFailed.title": "Չհաջողվեց պատճենել URL-ը սեղմատախտակում",
"toast.session.share.success.title": "Նիստը համօգտագործված",
"toast.session.share.success.description": "Կիսվել URL-ը պատճենված է սեղմատախտակում:",
"toast.session.share.failed.title": "Չհաջողվեց կիսել նիստը",
"toast.session.share.failed.description": "Սխալ է տեղի ունեցել նիստը կիսելիս",
"toast.session.unshare.success.title": "Նիստի համօգտագործումը դադարեցված է",
"toast.session.unshare.success.description": "Նիստի համօգտագործումը հաջողությամբ դադարեցվեց",
"toast.session.unshare.failed.title": "Չհաջողվեց դադարեցնել նիստի համօգտագործումը",
@@ -724,6 +733,17 @@ export const dict = {
"session.question.restore": "Վերականգնել հարցը",
"session.question.pending.one": "{{count}} առկախ հարց",
"session.question.pending.other": "{{count}} առկախ հարցեր",
"session.followupDock.summary.one": "{{count}} հերթագրված հաղորդագրություն",
"session.followupDock.summary.other": "{{count}} հերթագրված հաղորդագրություններ",
"session.followupDock.sendNow": "Ուղարկել հիմա",
"session.followupDock.edit": "Խմբագրել",
"session.followupDock.collapse": "Ծալել հերթագրված հաղորդագրությունները",
"session.followupDock.expand": "Ընդարձակել հերթագրված հաղորդագրությունները",
"session.revertDock.summary.one": "{{count}} վերադարձված հաղորդագրություն",
"session.revertDock.summary.other": "{{count}} վերադարձված հաղորդագրություններ",
"session.revertDock.collapse": "Ծալել վերադարձված հաղորդագրությունները",
"session.revertDock.expand": "Ընդլայնել վերադարձված հաղորդագրությունները",
"session.revertDock.restore": "Վերականգնել հաղորդագրությունը",
"session.new.title": "Կառուցել որեւէ բան",
"session.new.project.new": "Նոր նախագիծ",
"session.new.project.search": "Որոնել նախագծեր",
@@ -770,7 +790,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Ընդլայնումներ",
"status.popover.action.manageServers": "Կառավարել սերվերները",
"common.copied": "Պատճենված",
"session.share.popover.title": "Հրապարակել համացանցում",
"session.share.popover.description.shared":
"Այս նիստը հրապարակային է համացանցում։ Այն հասանելի է բոլորին, ովքեր ունեն հղումը:",
"session.share.popover.description.unshared":
"Հանրայնորեն համօգտագործեք նիստը համացանցում։ Այն հասանելի կլինի բոլորին, ովքեր ունեն հղումը:",
"session.share.action.share": "Կիսվել",
"session.share.action.publish": "Հրապարակել",
"session.share.action.publishing": "Հրատարակում...",
"session.share.action.unpublish": "Չհրապարակել",
"session.share.action.unpublishing": "Չհրապարակվում է...",
"session.share.action.view": "Դիտել",
"session.share.copy.copied": "Պատճենված",
"session.share.copy.copyLink": "Պատճենել հղումը",
"lsp.tooltip.none": "Չկան LSP սերվեր",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Բեռնվում է հուշում...",
@@ -901,6 +933,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Անհատականացրեք տերմինալում օգտագործվող տառատեսակը",
"settings.general.row.uiFont.title": "UI տառատեսակը",
"settings.general.row.uiFont.description": "Անհատականացրեք ինտերֆեյսի ընթացքում օգտագործվող տառատեսակը",
"settings.general.row.followup.title": "Հետագա պահվածք",
"settings.general.row.followup.description": "Ընտրեք՝ հետևելու հուշումներն անմիջապես կուղղվեն, թե սպասեք հերթում",
"settings.general.row.followup.option.queue": "Հերթ",
"settings.general.row.followup.option.steer": "Ղեկավար",
"settings.general.row.showFileTree.title": "Ֆայլի ծառ",
"settings.general.row.showFileTree.description": "Ցույց տալ ֆայլի ծառի վահանակը նիստերում",
"settings.general.row.showNavigation.title": "Նավարկության կառավարում",
+37 -1
View File
@@ -188,6 +188,10 @@ export const dict = {
"command.session.compact.description": "Ringkas sesi untuk mengurangi ukuran konteks",
"command.session.fork": "Fork dari pesan",
"command.session.fork.description": "Buat sesi baru dari pesan sebelumnya",
"command.session.share": "Bagikan sesi",
"command.session.share.description": "Bagikan sesi ini dan salin URL ke papan klip",
"command.session.unshare": "Hentikan berbagi",
"command.session.unshare.description": "Hentikan berbagi sesi ini",
"command.session.export": "Ekspor sesi",
"command.session.export.description": "Ekspor transkrip sesi lengkap sebagai JSON",
@@ -642,6 +646,11 @@ export const dict = {
"toast.context.noLineSelection.title": "Tidak ada pilihan baris",
"toast.context.noLineSelection.description": "Pilih rentang baris di tab berkas terlebih dahulu.",
"toast.session.share.copyFailed.title": "Gagal menyalin URL ke papan klip",
"toast.session.share.success.title": "Sesi dibagikan",
"toast.session.share.success.description": "URL berbagi disalin ke papan klip!",
"toast.session.share.failed.title": "Gagal membagikan sesi",
"toast.session.share.failed.description": "Terjadi kesalahan saat membagikan sesi",
"toast.session.unshare.success.title": "Berbagi sesi dihentikan",
"toast.session.unshare.success.description": "Berbagi sesi berhasil dihentikan!",
@@ -788,6 +797,17 @@ export const dict = {
"session.question.restore": "Pulihkan pertanyaan",
"session.question.pending.one": "{{count}} pertanyaan tertunda",
"session.question.pending.other": "{{count}} pertanyaan tertunda",
"session.followupDock.summary.one": "{{count}} pesan dalam antrean",
"session.followupDock.summary.other": "{{count}} pesan dalam antrean",
"session.followupDock.sendNow": "Kirim sekarang",
"session.followupDock.edit": "Sunting",
"session.followupDock.collapse": "Ciutkan pesan dalam antrean",
"session.followupDock.expand": "Bentangkan pesan dalam antrean",
"session.revertDock.summary.one": "{{count}} pesan diurungkan",
"session.revertDock.summary.other": "{{count}} pesan diurungkan",
"session.revertDock.collapse": "Ciutkan pesan yang diurungkan",
"session.revertDock.expand": "Bentangkan pesan yang diurungkan",
"session.revertDock.restore": "Pulihkan pesan",
"session.new.title": "Buat apa saja",
"session.new.project.new": "Proyek baru",
@@ -838,7 +858,18 @@ export const dict = {
"status.popover.tab.plugins": "Plugin",
"status.popover.action.manageServers": "Kelola server",
"common.copied": "Tersalin",
"session.share.popover.title": "Publikasikan di web",
"session.share.popover.description.shared": "Sesi ini publik di web. Siapa pun dengan tautan dapat mengaksesnya.",
"session.share.popover.description.unshared":
"Bagikan sesi secara publik di web. Siapa pun dengan tautan dapat mengaksesnya.",
"session.share.action.share": "Bagikan",
"session.share.action.publish": "Publikasikan",
"session.share.action.publishing": "Mempublikasikan...",
"session.share.action.unpublish": "Batalkan publikasi",
"session.share.action.unpublishing": "Membatalkan publikasi...",
"session.share.action.view": "Lihat",
"session.share.copy.copied": "Tersalin",
"session.share.copy.copyLink": "Salin tautan",
"lsp.tooltip.none": "Tidak ada server LSP",
"lsp.label.connected": "{{count}} LSP",
@@ -976,6 +1007,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "Sesuaikan font yang digunakan di terminal",
"settings.general.row.uiFont.title": "Font UI",
"settings.general.row.uiFont.description": "Sesuaikan font yang digunakan di seluruh antarmuka",
"settings.general.row.followup.title": "Perilaku lanjutan",
"settings.general.row.followup.description":
"Pilih apakah prompt lanjutan segera mengarahkan atau menunggu dalam antrean",
"settings.general.row.followup.option.queue": "Antrean",
"settings.general.row.followup.option.steer": "Arahkan",
"settings.general.row.showFileTree.title": "Pohon berkas",
"settings.general.row.showFileTree.description": "Tampilkan panel pohon berkas di sesi",
"settings.general.row.showNavigation.title": "Kontrol navigasi",
+37 -1
View File
@@ -178,6 +178,10 @@ export const dict = {
"command.session.compact.description": "Draga setuna saman til að minnka samhengi",
"command.session.fork": "Kvíslast frá skilaboðum",
"command.session.fork.description": "Stofna nýja setu út frá fyrri skilaboðum",
"command.session.share": "Deila setu",
"command.session.share.description": "Deila þessari setu og afrita slóðina á klippispjaldið",
"command.session.unshare": "Hætta að deila setu",
"command.session.unshare.description": "Hætta að deila þessari setu",
"command.session.export": "Flytja út setu",
"command.session.export.description": "Flytja allt setuafritið út sem JSON",
"palette.search.placeholder": "Leitaðu að skrám, skipunum og fundum",
@@ -593,6 +597,11 @@ export const dict = {
"toast.file.listFailed.title": "Mistókst að skrá skrár",
"toast.context.noLineSelection.title": "Ekkert línuval",
"toast.context.noLineSelection.description": "Veldu línusvið í skráarflipa fyrst.",
"toast.session.share.copyFailed.title": "Mistókst að afrita URL á klemmuspjald",
"toast.session.share.success.title": "Fundi deilt",
"toast.session.share.success.description": "Deildu URL afritað á klemmuspjald!",
"toast.session.share.failed.title": "Ekki tókst að deila setu",
"toast.session.share.failed.description": "Villa kom upp þegar lotunni var deilt",
"toast.session.unshare.success.title": "Ekki deilt lotu",
"toast.session.unshare.success.description": "Lokað var við deilingu lotunnar!",
"toast.session.unshare.failed.title": "Mistókst að hætta við deilingu lotu",
@@ -723,6 +732,17 @@ export const dict = {
"session.question.restore": "Endurheimta spurningu",
"session.question.pending.one": "{{count}} spurning í bið",
"session.question.pending.other": "{{count}} spurningar í bið",
"session.followupDock.summary.one": "{{count}} skilaboð í biðröð",
"session.followupDock.summary.other": "{{count}} skilaboð í biðröð",
"session.followupDock.sendNow": "Sendu núna",
"session.followupDock.edit": "Breyta",
"session.followupDock.collapse": "Draga saman skilaboð í biðröð",
"session.followupDock.expand": "Stækkaðu skilaboð í biðröð",
"session.revertDock.summary.one": "{{count}} afturkallað skilaboð",
"session.revertDock.summary.other": "{{count}} afturkallað skilaboð",
"session.revertDock.collapse": "Dragðu saman skilaboð sem eru afturkölluð",
"session.revertDock.expand": "Stækka afturkölluð skilaboð",
"session.revertDock.restore": "Endurheimta skilaboð",
"session.new.title": "Byggja hvað sem er",
"session.new.project.new": "Nýtt verkefni",
"session.new.project.search": "Leita að verkefnum",
@@ -769,7 +789,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Viðbætur",
"status.popover.action.manageServers": "Stjórna netþjónum",
"common.copied": "Afritað",
"session.share.popover.title": "Birta á vefnum",
"session.share.popover.description.shared":
"Þessi seta er opinber á vefnum. Allir sem hafa tengilinn geta opnað hana.",
"session.share.popover.description.unshared":
"Deildu setunni opinberlega á vefnum. Allir sem hafa tengilinn geta opnað hana.",
"session.share.action.share": "Deila",
"session.share.action.publish": "Birta",
"session.share.action.publishing": "Birtir...",
"session.share.action.unpublish": "Afbirta",
"session.share.action.unpublishing": "Hættir að birta...",
"session.share.action.view": "Skoða",
"session.share.copy.copied": "Afritað",
"session.share.copy.copyLink": "Afritaðu tengil",
"lsp.tooltip.none": "Engir LSP netþjónar",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Hleður tilkynningu...",
@@ -897,6 +929,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Sérsníddu leturgerðina sem notuð er í flugstöðinni",
"settings.general.row.uiFont.title": "UI leturgerð",
"settings.general.row.uiFont.description": "Sérsníddu leturgerðina sem notuð er í öllu viðmótinu",
"settings.general.row.followup.title": "Eftirfylgnihegðun",
"settings.general.row.followup.description": "Veldu hvort eftirfylgnibeiðnir stýra strax eða bíða í biðröð",
"settings.general.row.followup.option.queue": "Biðröð",
"settings.general.row.followup.option.steer": "Stýra",
"settings.general.row.showFileTree.title": "Skráartré",
"settings.general.row.showFileTree.description": "Sýndu skráartréspjaldið í lotum",
"settings.general.row.showNavigation.title": "Leiðsögustýringar",
+39 -1
View File
@@ -82,6 +82,10 @@ export const dict = {
"command.session.compact.description": "Riepiloga la sessione per ridurre le dimensioni del contesto",
"command.session.fork": "Crea sessione dal messaggio",
"command.session.fork.description": "Crea una nuova sessione da un messaggio precedente",
"command.session.share": "Condividi sessione",
"command.session.share.description": "Condividi questa sessione e copia l'URL negli appunti",
"command.session.unshare": "Annulla condivisione sessione",
"command.session.unshare.description": "Interrompi la condivisione di questa sessione",
"command.session.export": "Esporta sessione",
"command.session.export.description": "Esporta la trascrizione completa della sessione in formato JSON",
@@ -502,6 +506,11 @@ export const dict = {
"toast.file.listFailed.title": "Impossibile elencare i file",
"toast.context.noLineSelection.title": "Nessuna riga selezionata",
"toast.context.noLineSelection.description": "Selezionare prima un intervallo di righe nella scheda di un file.",
"toast.session.share.copyFailed.title": "Impossibile copiare l'URL negli appunti",
"toast.session.share.success.title": "Sessione condivisa",
"toast.session.share.success.description": "URL di condivisione copiato negli appunti",
"toast.session.share.failed.title": "Impossibile condividere la sessione",
"toast.session.share.failed.description": "Si è verificato un errore durante la condivisione della sessione",
"toast.session.unshare.success.title": "Sessione non condivisa",
"toast.session.unshare.success.description": "Condivisione della sessione annullata!",
"toast.session.unshare.failed.title": "Impossibile annullare la condivisione della sessione",
@@ -636,6 +645,19 @@ export const dict = {
"session.question.pending.one": "{{count}} domanda in sospeso",
"session.question.pending.many": "{{count}} di domande in sospeso",
"session.question.pending.other": "{{count}} domande in sospeso",
"session.followupDock.summary.one": "{{count}} messaggio in coda",
"session.followupDock.summary.many": "{{count}} di messaggi in coda",
"session.followupDock.summary.other": "{{count}} messaggi in coda",
"session.followupDock.sendNow": "Invia ora",
"session.followupDock.edit": "Modifica",
"session.followupDock.collapse": "Comprimi i messaggi in coda",
"session.followupDock.expand": "Espandi i messaggi in coda",
"session.revertDock.summary.one": "{{count}} messaggio annullato",
"session.revertDock.summary.many": "{{count}} di messaggi annullati",
"session.revertDock.summary.other": "{{count}} messaggi annullati",
"session.revertDock.collapse": "Comprimi i messaggi annullati",
"session.revertDock.expand": "Espandi i messaggi annullati",
"session.revertDock.restore": "Ripristina il messaggio",
"session.new.title": "Crea qualsiasi cosa",
"session.new.project.new": "Nuovo progetto",
"session.new.project.search": "Cerca progetti",
@@ -682,7 +704,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugin",
"status.popover.action.manageServers": "Gestisci server",
"common.copied": "Copiato",
"session.share.popover.title": "Pubblica sul web",
"session.share.popover.description.shared":
"Questa sessione è pubblica sul web. È accessibile a chiunque abbia il collegamento.",
"session.share.popover.description.unshared":
"Condividi la sessione pubblicamente sul web. Sarà accessibile a chiunque abbia il collegamento.",
"session.share.action.share": "Condividi",
"session.share.action.publish": "Pubblica",
"session.share.action.publishing": "Pubblicazione...",
"session.share.action.unpublish": "Annulla pubblicazione",
"session.share.action.unpublishing": "Annullamento della pubblicazione...",
"session.share.action.view": "Visualizza",
"session.share.copy.copied": "Copiato",
"session.share.copy.copyLink": "Copia collegamento",
"lsp.tooltip.none": "Nessun server LSP",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "Caricamento del prompt...",
@@ -812,6 +846,10 @@ export const dict = {
"settings.general.row.terminalFont.description": "Personalizza il carattere utilizzato nel terminale",
"settings.general.row.uiFont.title": "Carattere dell'interfaccia utente",
"settings.general.row.uiFont.description": "Personalizza il carattere utilizzato nell'interfaccia",
"settings.general.row.followup.title": "Gestione dei prompt successivi",
"settings.general.row.followup.description": "Scegli se i prompt successivi intervengono subito o attendono in coda",
"settings.general.row.followup.option.queue": "Coda",
"settings.general.row.followup.option.steer": "Intervieni subito",
"settings.general.row.showFileTree.title": "Albero dei file",
"settings.general.row.showFileTree.description": "Mostra il pannello dell'albero dei file nelle sessioni",
"settings.general.row.showNavigation.title": "Controlli di navigazione",
+38 -1
View File
@@ -180,6 +180,10 @@ export const dict = {
"command.session.compact.description": "セッションを要約してコンテキストサイズを削減",
"command.session.fork": "メッセージからフォーク",
"command.session.fork.description": "以前のメッセージから新しいセッションを作成",
"command.session.share": "セッションを共有",
"command.session.share.description": "このセッションを共有しURLをクリップボードにコピー",
"command.session.unshare": "セッションの共有を停止",
"command.session.unshare.description": "このセッションの共有を停止",
"command.session.export": "セッションをエクスポート",
"command.session.export.description": "セッションの全記録を JSON としてエクスポート",
@@ -587,6 +591,11 @@ export const dict = {
"toast.file.listFailed.title": "ファイル一覧の取得に失敗しました",
"toast.context.noLineSelection.title": "行が選択されていません",
"toast.context.noLineSelection.description": "まずファイルタブで行範囲を選択してください。",
"toast.session.share.copyFailed.title": "URLのコピーに失敗しました",
"toast.session.share.success.title": "セッションを共有しました",
"toast.session.share.success.description": "共有URLをクリップボードにコピーしました!",
"toast.session.share.failed.title": "セッションの共有に失敗しました",
"toast.session.share.failed.description": "セッションの共有中にエラーが発生しました",
"toast.session.unshare.success.title": "セッションの共有を解除しました",
"toast.session.unshare.success.description": "セッションの共有解除に成功しました!",
"toast.session.unshare.failed.title": "セッションの共有解除に失敗しました",
@@ -702,6 +711,17 @@ export const dict = {
"session.question.restore": "質問を復元",
"session.question.pending.one": "{{count}}件の保留中の質問",
"session.question.pending.other": "{{count}}件の保留中の質問",
"session.followupDock.summary.one": "{{count}} 件のメッセージが待機中",
"session.followupDock.summary.other": "{{count}} 件のメッセージが待機中",
"session.followupDock.sendNow": "今すぐ送信",
"session.followupDock.edit": "編集",
"session.followupDock.collapse": "待機中のメッセージを折りたたむ",
"session.followupDock.expand": "待機中のメッセージを展開",
"session.revertDock.summary.one": "{{count}} 件のロールバックされたメッセージ",
"session.revertDock.summary.other": "{{count}} 件のロールバックされたメッセージ",
"session.revertDock.collapse": "ロールバックされたメッセージを折りたたむ",
"session.revertDock.expand": "ロールバックされたメッセージを展開",
"session.revertDock.restore": "メッセージを復元",
"session.new.title": "何でも作る",
"session.new.project.new": "新しいプロジェクト",
"session.new.project.search": "プロジェクトを検索",
@@ -729,7 +749,19 @@ export const dict = {
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "プラグイン",
"status.popover.action.manageServers": "サーバーを管理",
"common.copied": "コピーしました",
"session.share.popover.title": "ウェブで公開",
"session.share.popover.description.shared":
"このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。",
"session.share.popover.description.unshared":
"セッションをウェブで公開します。リンクを知っている人なら誰でもアクセスできるようになります。",
"session.share.action.share": "共有",
"session.share.action.publish": "公開",
"session.share.action.publishing": "公開中...",
"session.share.action.unpublish": "非公開にする",
"session.share.action.unpublishing": "非公開にしています...",
"session.share.action.view": "表示",
"session.share.copy.copied": "コピーしました",
"session.share.copy.copyLink": "リンクをコピー",
"lsp.tooltip.none": "LSPサーバーなし",
"lsp.label.connected": "{{count}} LSP",
"prompt.loading": "プロンプトを読み込み中...",
@@ -802,6 +834,11 @@ export const dict = {
"settings.general.row.terminalFont.description": "ターミナルで使用するフォントをカスタマイズ",
"settings.general.row.uiFont.title": "UIフォント",
"settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
"settings.general.row.followup.title": "フォローアップの動作",
"settings.general.row.followup.description":
"フォローアッププロンプトを即座に実行するか、キューで待機させるかを選択します",
"settings.general.row.followup.option.queue": "キューに追加",
"settings.general.row.followup.option.steer": "即座に実行 (Steer)",
"settings.general.row.showFileTree.title": "ファイルツリー",
"settings.general.row.showFileTree.description": "セッションにファイルツリーパネルを表示します",
"settings.general.row.showNavigation.title": "ナビゲーションコントロール",

Some files were not shown because too many files have changed in this diff Show More