mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-03 15:36:22 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e0750282a | ||
|
|
ea71b0400b | ||
|
|
d0f23fa230 | ||
|
|
d5887fc5c4 | ||
|
|
52964dda33 | ||
|
|
b5d423546b | ||
|
|
a79594c72d | ||
|
|
fba7aaaabb | ||
|
|
c5d6150fe9 | ||
|
|
71cece1a99 | ||
|
|
60ab72d884 | ||
|
|
cde1fc4c6f | ||
|
|
c026023822 | ||
|
|
952387176e | ||
|
|
0b980a1ac0 | ||
|
|
a73f76b368 | ||
|
|
23b6318fd6 | ||
|
|
6f6fc10847 |
@@ -358,6 +358,7 @@
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -605,6 +606,21 @@
|
||||
"solid-js",
|
||||
],
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode-ai/plugin-browser",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/posts": {
|
||||
"name": "@opencode-ai/posts",
|
||||
"dependencies": {
|
||||
@@ -674,6 +690,7 @@
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/httpapi-codegen": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -2144,6 +2161,8 @@
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||
|
||||
"@opencode-ai/plugin-browser": ["@opencode-ai/plugin-browser@workspace:packages/plugin-browser"],
|
||||
|
||||
"@opencode-ai/posts": ["@opencode-ai/posts@workspace:packages/posts"],
|
||||
|
||||
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
||||
|
||||
@@ -423,10 +423,14 @@ export interface ParserState {
|
||||
readonly name: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<string>
|
||||
// Item ids are response-scoped identities. Keep completed ids tombstoned so
|
||||
// reconnect replay cannot reopen fragments already emitted downstream.
|
||||
readonly completedTools: ReadonlySet<string>
|
||||
readonly hasFunctionCall: boolean
|
||||
readonly lifecycle: Lifecycle.State
|
||||
readonly outputItems: Readonly<Record<number, string>>
|
||||
readonly message: { readonly id: string; readonly phase: MessagePhase | null | undefined } | undefined
|
||||
readonly completedMessages: ReadonlySet<string>
|
||||
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>
|
||||
}
|
||||
|
||||
@@ -1038,12 +1042,16 @@ const onOutputItemAdded = (state: ParserState, event: NormalizedEvent): StepResu
|
||||
const item = event.item
|
||||
if (!item) return [state, NO_EVENTS]
|
||||
if (item.type === "message") {
|
||||
if (state.completedMessages.has(item.id)) return [state, NO_EVENTS]
|
||||
const phase = messagePhase(item.phase)
|
||||
const completedMessages = new Set(state.completedMessages)
|
||||
if (state.message !== undefined && state.message.id !== item.id) completedMessages.add(state.message.id)
|
||||
// A new message closes earlier messages, including ones that never streamed.
|
||||
const events: LLMEvent[] = []
|
||||
const lifecycle = [...state.lifecycle.text]
|
||||
.filter((id) => id !== item.id)
|
||||
.reduce((lifecycle, id) => {
|
||||
completedMessages.add(id)
|
||||
const openPhase = state.message?.id === id ? state.message.phase : undefined
|
||||
return Lifecycle.textEnd(
|
||||
lifecycle,
|
||||
@@ -1056,6 +1064,7 @@ const onOutputItemAdded = (state: ParserState, event: NormalizedEvent): StepResu
|
||||
{
|
||||
...state,
|
||||
lifecycle,
|
||||
completedMessages,
|
||||
message: {
|
||||
id: item.id,
|
||||
phase: phase === undefined && state.message?.id === item.id ? state.message.phase : phase,
|
||||
@@ -1085,7 +1094,7 @@ const onOutputItemAdded = (state: ParserState, event: NormalizedEvent): StepResu
|
||||
]
|
||||
}
|
||||
if (item.type !== "function_call" || !item.call_id) return [state, NO_EVENTS]
|
||||
if (state.tools[item.id] !== undefined) return [state, NO_EVENTS]
|
||||
if (state.tools[item.id] !== undefined || state.completedTools.has(item.id)) return [state, NO_EVENTS]
|
||||
const metadata = providerMetadata(state, { itemId: item.id })
|
||||
const events: LLMEvent[] = []
|
||||
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
||||
@@ -1189,9 +1198,14 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
}
|
||||
|
||||
if (item.type === "message") {
|
||||
const active = state.message?.id === item.id
|
||||
if (state.completedMessages.has(item.id)) return [state, NO_EVENTS] satisfies StepResult
|
||||
const completedMessages = new Set(state.completedMessages)
|
||||
completedMessages.add(item.id)
|
||||
if (state.message !== undefined && state.message.id !== item.id)
|
||||
return [{ ...state, completedMessages }, NO_EVENTS] satisfies StepResult
|
||||
const message = state.message
|
||||
const itemPhase = messagePhase(item.phase)
|
||||
const phase = itemPhase === undefined && active ? state.message?.phase : itemPhase
|
||||
const phase = itemPhase === undefined ? message?.phase : itemPhase
|
||||
const parts: ReadonlyArray<unknown> = Array.isArray(item.content) ? item.content : []
|
||||
const content: string[] = []
|
||||
for (const part of parts) {
|
||||
@@ -1207,7 +1221,8 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
{
|
||||
...state,
|
||||
lifecycle: Lifecycle.textEnd(lifecycle, events, item.id, metadata, text),
|
||||
message: active ? undefined : state.message,
|
||||
completedMessages,
|
||||
message: undefined,
|
||||
},
|
||||
events,
|
||||
] satisfies StepResult
|
||||
@@ -1215,6 +1230,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
|
||||
if (item.type === "function_call") {
|
||||
if (!item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult
|
||||
if (state.completedTools.has(item.id)) return [state, NO_EVENTS] satisfies StepResult
|
||||
const metadata = providerMetadata(state, { itemId: item.id })
|
||||
const registered = state.tools[item.id] !== undefined
|
||||
const tools = registered
|
||||
@@ -1241,6 +1257,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) ||
|
||||
state.hasFunctionCall,
|
||||
tools: result.tools,
|
||||
completedTools: new Set([...state.completedTools, item.id]),
|
||||
},
|
||||
events,
|
||||
] satisfies StepResult
|
||||
@@ -1501,9 +1518,11 @@ export const initial = (request: LLMRequest, adapter: ProviderAdapter = BASE_ADA
|
||||
providerMetadataKey: metadataKey(request.model),
|
||||
hasFunctionCall: false,
|
||||
tools: ToolStream.empty<string>(),
|
||||
completedTools: new Set<string>(),
|
||||
lifecycle: Lifecycle.initial(),
|
||||
outputItems: {},
|
||||
message: undefined,
|
||||
completedMessages: new Set<string>(),
|
||||
reasoningItems: {},
|
||||
})
|
||||
|
||||
|
||||
@@ -6,13 +6,11 @@ import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/
|
||||
import {
|
||||
InvalidProviderOutputError,
|
||||
InvalidRequestError,
|
||||
UnsupportedOperationError,
|
||||
AIError,
|
||||
HttpContext,
|
||||
type ContentPart,
|
||||
type LLMRequest,
|
||||
type MediaPart,
|
||||
type ProviderID,
|
||||
type TextPart,
|
||||
type ToolResultPart,
|
||||
} from "../schema/index.js"
|
||||
@@ -256,29 +254,6 @@ export const invalidRequest = (message: string, cause?: unknown) =>
|
||||
reason: new InvalidRequestError({ message, cause }),
|
||||
})
|
||||
|
||||
/**
|
||||
* Canonical constructor for operations the selected route does not implement.
|
||||
* Prefer this over `invalidRequest` when the failure is a missing route
|
||||
* capability rather than a malformed caller input, so consumers can branch on
|
||||
* `reason._tag` plus `reason.operation` instead of matching message text.
|
||||
*/
|
||||
export const unsupportedOperation = (input: {
|
||||
readonly operation: string
|
||||
readonly message: string
|
||||
readonly provider?: ProviderID
|
||||
readonly route?: string
|
||||
readonly cause?: unknown
|
||||
}) =>
|
||||
new AIError({
|
||||
reason: new UnsupportedOperationError({
|
||||
operation: input.operation,
|
||||
message: input.message,
|
||||
provider: input.provider,
|
||||
route: input.route,
|
||||
cause: input.cause,
|
||||
}),
|
||||
})
|
||||
|
||||
export const imageResponse = Effect.fn("ProviderShared.imageResponse")(function* (
|
||||
route: string,
|
||||
name: string,
|
||||
|
||||
@@ -46,12 +46,9 @@ const adapter = {
|
||||
const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(XAIResponsesBody))
|
||||
const fromRequest = Effect.fn("XAIResponses.fromRequest")(function* (request: LLMRequest) {
|
||||
if (request.providerOptions?.contextManagement !== undefined)
|
||||
return yield* ProviderShared.unsupportedOperation({
|
||||
operation: "in-band-compaction",
|
||||
provider: request.model.provider,
|
||||
route: request.model.route.id,
|
||||
message: "xAI requires explicit compaction through LLMClient.compact; automatic context management is not supported",
|
||||
})
|
||||
return yield* ProviderShared.invalidRequest(
|
||||
"xAI requires explicit compaction through LLMClient.compact; automatic context management is not supported",
|
||||
)
|
||||
return yield* decodeBody(yield* OpenResponses.fromRequestWithAdapter(request, adapter))
|
||||
})
|
||||
|
||||
|
||||
@@ -585,12 +585,9 @@ export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer
|
||||
Effect.suspend(() => {
|
||||
const operation = request.model.route.compact
|
||||
if (!operation)
|
||||
return ProviderShared.unsupportedOperation({
|
||||
operation: "compact",
|
||||
provider: request.model.provider,
|
||||
route: request.model.route.id,
|
||||
message: `${request.model.provider}/${request.model.route.id} does not support explicit compaction`,
|
||||
})
|
||||
return ProviderShared.invalidRequest(
|
||||
`${request.model.provider}/${request.model.route.id} does not support explicit compaction`,
|
||||
)
|
||||
return operation(prepareRequest(request), executor, options)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -35,21 +35,6 @@ export class InvalidRequestError extends Schema.TaggedError<InvalidRequestError>
|
||||
},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* A caller-requested operation the selected route does not implement, such as
|
||||
* explicit compaction on a route without a compact endpoint. Detected locally
|
||||
* before any network I/O, so unlike transport or provider-output failures it
|
||||
* never carries HTTP context from a provider round-trip.
|
||||
*/
|
||||
export class UnsupportedOperationError extends Schema.TaggedError<UnsupportedOperationError>(
|
||||
"AI.Error.UnsupportedOperation",
|
||||
)("UnsupportedOperation", {
|
||||
...ReasonFields,
|
||||
operation: Schema.String,
|
||||
provider: Schema.optional(ProviderID),
|
||||
route: Schema.optional(RouteID),
|
||||
}) {}
|
||||
|
||||
export class NoRouteError extends Schema.TaggedError<NoRouteError>("AI.Error.NoRoute")("NoRoute", {
|
||||
...ReasonFields,
|
||||
route: RouteID,
|
||||
@@ -122,7 +107,6 @@ export class UnknownProviderError extends Schema.TaggedError<UnknownProviderErro
|
||||
|
||||
export const AIErrorReason = Schema.Union([
|
||||
InvalidRequestError,
|
||||
UnsupportedOperationError,
|
||||
NoRouteError,
|
||||
AuthenticationError,
|
||||
RateLimitError,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Schema } from "effect"
|
||||
import { CompactionPart, CompactionResponse, LLMEvent, LLMResponse, Message, ProviderID } from "../src/schema/index.js"
|
||||
import { LLM, LLMClient, LLMRequest, LanguageModel } from "../src/index.js"
|
||||
import { OpenAI, Anthropic } from "../src/providers.js"
|
||||
import { testEffect } from "./lib/effect.js"
|
||||
import { fixedResponse } from "./lib/http.js"
|
||||
|
||||
test("runtime capability checks follow model and route updates", () => {
|
||||
const supported = OpenAI.configure({ apiKey: "test" }).responses("fixture")
|
||||
@@ -77,25 +75,3 @@ test("tagged content and event guards accept both checkpoint representations", (
|
||||
expect(Schema.decodeSync(codec)(Schema.encodeSync(codec)(message))).toEqual(message)
|
||||
}
|
||||
})
|
||||
|
||||
testEffect(fixedResponse("")).effect(
|
||||
"explicit compaction on a route without a compact endpoint fails with UnsupportedOperation",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: Anthropic.configure({ apiKey: "test" }).model("fixture"),
|
||||
prompt: "hello",
|
||||
})
|
||||
expect(LLMClient.canCompact(request)).toBe(false)
|
||||
const error = yield* LLMClient.compact(
|
||||
request as unknown as Parameters<typeof LLMClient.compact>[0],
|
||||
).pipe(Effect.flip)
|
||||
expect(error.reason._tag).toBe("UnsupportedOperation")
|
||||
expect(error.message).toContain("does not support explicit compaction")
|
||||
if (error.reason._tag === "UnsupportedOperation") {
|
||||
expect(error.reason.operation).toBe("compact")
|
||||
expect(error.reason.provider).toBe("anthropic")
|
||||
expect(error.reason.route).toBe("anthropic-messages")
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -130,22 +130,16 @@ for (const model of [
|
||||
}),
|
||||
],
|
||||
})
|
||||
for (const [candidate, tag] of [
|
||||
[
|
||||
LLMRequest.update(request, {
|
||||
tools: [
|
||||
{ name: "unsupported", description: "Generation only", inputSchema: {}, native: { unsupported: {} } },
|
||||
],
|
||||
}),
|
||||
"InvalidRequest",
|
||||
],
|
||||
[
|
||||
LLMRequest.update(request, { providerOptions: { contextManagement: "invalid-generation-option" } }),
|
||||
model.provider === "xai" ? "UnsupportedOperation" : "InvalidRequest",
|
||||
],
|
||||
] as const) {
|
||||
for (const candidate of [
|
||||
LLMRequest.update(request, {
|
||||
tools: [
|
||||
{ name: "unsupported", description: "Generation only", inputSchema: {}, native: { unsupported: {} } },
|
||||
],
|
||||
}),
|
||||
LLMRequest.update(request, { providerOptions: { contextManagement: "invalid-generation-option" } }),
|
||||
]) {
|
||||
const error = yield* LLMClient.generate(candidate).pipe(Effect.flip)
|
||||
expect(error.reason._tag).toBe(tag)
|
||||
expect(error.reason._tag).toBe("InvalidRequest")
|
||||
const response = yield* LLMClient.compact(candidate)
|
||||
expect(response.replacement[0]?.content[0]?.type).toBe("compaction")
|
||||
}
|
||||
@@ -367,9 +361,8 @@ testEffect(fixedResponse("must not execute")).effect("xAI rejects automatic comp
|
||||
{ providerOptions: { contextManagement: [{ type: "compaction" }] } },
|
||||
)
|
||||
const error = yield* LLMClient.generate(request).pipe(Effect.flip)
|
||||
expect(error.reason._tag).toBe("UnsupportedOperation")
|
||||
expect(error.reason._tag).toBe("InvalidRequest")
|
||||
expect(error.message).toContain("LLMClient.compact")
|
||||
if (error.reason._tag === "UnsupportedOperation") expect(error.reason.operation).toBe("in-band-compaction")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -435,9 +428,7 @@ for (const model of [
|
||||
Effect.gen(function* () {
|
||||
// @ts-expect-error Untyped callers must still receive the runtime capability error.
|
||||
const error = yield* LLMClient.compact(LLM.request({ model, prompt: "hello" })).pipe(Effect.flip)
|
||||
expect(error.reason._tag).toBe("UnsupportedOperation")
|
||||
expect(error.message).toContain("does not support explicit compaction")
|
||||
if (error.reason._tag === "UnsupportedOperation") expect(error.reason.operation).toBe("compact")
|
||||
expect(error.reason._tag).toBe("InvalidRequest")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,6 +82,32 @@ describe("Open Responses completed item text", () => {
|
||||
expect(response.events.filter(LLMEvent.is.textStart)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("assembles a done-only message once across replayed item events", () =>
|
||||
Effect.gen(function* () {
|
||||
const item = {
|
||||
type: "message",
|
||||
id: "msg_1",
|
||||
content: [{ type: "output_text", text: "Recovered" }],
|
||||
}
|
||||
const response = yield* generate(
|
||||
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Ignored after resume" },
|
||||
{ type: "response.output_item.done", item },
|
||||
{ type: "response.output_item.added", item },
|
||||
{ type: "response.output_item.done", item },
|
||||
completed,
|
||||
)
|
||||
expect(response.text).toBe("Recovered")
|
||||
expect(response.message.content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: "Recovered",
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_1" } },
|
||||
},
|
||||
])
|
||||
expect(response.events.filter(LLMEvent.is.textEnd)).toHaveLength(1)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe("Open Responses completed item reasoning", () => {
|
||||
|
||||
@@ -217,7 +217,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves non-empty done-only message content", () =>
|
||||
it.effect("preserves non-empty done-only message content without replaying duplicates", () =>
|
||||
Effect.gen(function* () {
|
||||
const text = {
|
||||
type: "message",
|
||||
@@ -230,11 +230,17 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
content: [{ type: "refusal", refusal: "Done-only refusal." }],
|
||||
}
|
||||
const events = yield* collect(
|
||||
{ type: "response.output_item.done", item: text },
|
||||
{ type: "response.output_item.done", item: text },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "message", id: "msg_empty", content: [{ type: "output_text", text: "" }] },
|
||||
},
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "message", id: "msg_empty", content: [{ type: "output_text", text: "Late" }] },
|
||||
},
|
||||
{ type: "response.output_item.done", item: refusal },
|
||||
{ type: "response.output_item.done", item: refusal },
|
||||
completed,
|
||||
)
|
||||
@@ -266,6 +272,63 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("treats a repeated message lifecycle as replay", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* collect(
|
||||
{ type: "response.output_item.added", item: { type: "message", id: "msg_1", phase: "commentary" } },
|
||||
{ type: "response.output_text.delta", item_id: "msg_1", delta: "First" },
|
||||
{ type: "response.output_item.done", item: { type: "message", id: "msg_1" } },
|
||||
{ type: "response.output_item.added", item: { type: "message", id: "msg_1" } },
|
||||
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Second" },
|
||||
{ type: "response.output_item.done", item: { type: "message", id: "msg_1" } },
|
||||
completed,
|
||||
)
|
||||
expect(events.filter(LLMEvent.is.textEnd)).toEqual([
|
||||
{
|
||||
type: "text-end",
|
||||
id: "msg_1",
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "commentary" } },
|
||||
},
|
||||
])
|
||||
expect(events.filter(LLMEvent.is.textDelta).map((event) => event.text)).toEqual(["First"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("ignores a stale done-only message while another message is active", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* collect(
|
||||
{ type: "response.output_item.added", item: { type: "message", id: "msg_1", phase: "commentary" } },
|
||||
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Draft" },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "message", id: "msg_2", content: [{ type: "output_text", text: "Recovered" }] },
|
||||
},
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "message", id: "msg_1", content: [{ type: "output_text", text: "Final" }] },
|
||||
},
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "message", id: "msg_2", content: [{ type: "output_text", text: "Late" }] },
|
||||
},
|
||||
completed,
|
||||
)
|
||||
expect(events.filter((event) => event.type.startsWith("text-"))).toEqual([
|
||||
{
|
||||
type: "text-start",
|
||||
id: "msg_1",
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "commentary" } },
|
||||
},
|
||||
{ type: "text-delta", id: "msg_1", text: "Draft" },
|
||||
{
|
||||
type: "text-end",
|
||||
id: "msg_1",
|
||||
text: "Final",
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "commentary" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
// Captured from Bedrock Mantle (openai.gpt-oss-120b): the terminal function_call
|
||||
// items rename `id` to `item_id` and carry a stray `output_index`.
|
||||
it.effect("recovers a terminal function_call id from its output slot", () =>
|
||||
@@ -356,7 +419,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("opens and closes a done-only tool", () =>
|
||||
it.effect("opens and closes a done-only tool once", () =>
|
||||
Effect.gen(function* () {
|
||||
const item = {
|
||||
type: "function_call",
|
||||
@@ -365,7 +428,12 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
name: "lookup",
|
||||
arguments: '{"query":"weather"}',
|
||||
}
|
||||
const events = yield* collect({ type: "response.output_item.done", item }, completed)
|
||||
const events = yield* collect(
|
||||
{ type: "response.output_item.done", item },
|
||||
{ type: "response.output_item.done", item },
|
||||
{ type: "response.output_item.added", item },
|
||||
completed,
|
||||
)
|
||||
const providerMetadata = { "openai-compatible": { itemId: "fc_1" } }
|
||||
expect(events.filter((event) => event.type.startsWith("tool-"))).toEqual([
|
||||
{ type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata },
|
||||
|
||||
@@ -2850,7 +2850,7 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("ignores duplicate item start events", () =>
|
||||
it.effect("ignores duplicate item boundary events", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
@@ -2881,6 +2881,21 @@ describe("OpenAI Responses route", () => {
|
||||
arguments: '{"query":"weather"}',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: {
|
||||
type: "function_call",
|
||||
id: "fc_1",
|
||||
call_id: "call_1",
|
||||
name: "lookup",
|
||||
arguments: '{"query":"weather"}',
|
||||
},
|
||||
},
|
||||
// A completed item that is re-added stays closed.
|
||||
{
|
||||
type: "response.output_item.added",
|
||||
item: { type: "function_call", id: "fc_1", call_id: "call_1", name: "lookup", arguments: "" },
|
||||
},
|
||||
{ type: "response.completed", response: { id: "resp_1" } },
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
ToolResultValue,
|
||||
TransportError,
|
||||
UnknownProviderError,
|
||||
UnsupportedOperationError,
|
||||
Usage,
|
||||
} from "../src/schema/index.js"
|
||||
import { ProviderShared } from "../src/protocols/shared.js"
|
||||
@@ -277,12 +276,6 @@ test("AI errors serialize diagnostics only on their typed reason", () => {
|
||||
test("AI error reasons are tagged Errors with required messages", () => {
|
||||
const reasons = [
|
||||
new InvalidRequestError({ message: "Invalid request" }),
|
||||
new UnsupportedOperationError({
|
||||
message: "Unsupported operation",
|
||||
operation: "compact",
|
||||
provider: model.provider,
|
||||
route: "fake-route",
|
||||
}),
|
||||
new NoRouteError({
|
||||
message: "No route",
|
||||
route: RouteID.make("missing"),
|
||||
@@ -300,7 +293,6 @@ test("AI error reasons are tagged Errors with required messages", () => {
|
||||
]
|
||||
expect(reasons.map((reason) => reason._tag)).toEqual([
|
||||
"InvalidRequest",
|
||||
"UnsupportedOperation",
|
||||
"NoRoute",
|
||||
"Authentication",
|
||||
"RateLimit",
|
||||
|
||||
@@ -78,13 +78,9 @@ for (const theme of ["light", "dark"] as const) {
|
||||
await expectToken(
|
||||
message,
|
||||
"background-color",
|
||||
scenario.accent ? "--v2-background-bg-accent" : theme === "light" ? "--v2-blue-100" : "--v2-blue-1200",
|
||||
)
|
||||
await expectToken(
|
||||
message,
|
||||
"color",
|
||||
scenario.accent ? "--v2-text-text-contrast" : theme === "light" ? "--v2-blue-700" : "--v2-blue-300",
|
||||
scenario.accent ? "--v2-background-bg-accent" : "--v2-state-bg-info",
|
||||
)
|
||||
await expectToken(message, "color", scenario.accent ? "--v2-text-text-contrast" : "--v2-text-text-accent")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ export function Composer(props: { class?: string; model: ComposerModel; borderUn
|
||||
modelControlsVisible={!props.model.model.loading}
|
||||
attachKeybind={command.keybindParts("file.attach")}
|
||||
attachShortcut={command.keybind("file.attach")}
|
||||
alternateKeybind={[formatKeybind("mod", language.t), "↵"]}
|
||||
exitShellKeybind={[formatKeybind("esc", language.t)]}
|
||||
alternateKeybind={[formatKeybind("mod", language.t), formatKeybind("enter", language.t)]}
|
||||
modelControl={
|
||||
<ComposerModelControl
|
||||
loading={props.model.model.loading}
|
||||
|
||||
@@ -6,7 +6,3 @@
|
||||
[data-color-scheme="dark"] [data-component="new-session"] [data-component="composer"] {
|
||||
background: var(--v2-background-bg-layer-01);
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] [data-component="composer-suggestions"] [data-active] {
|
||||
background: var(--v2-alpha-light-10);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export type ComposerEditorProps = {
|
||||
attachKeybind?: string[]
|
||||
attachShortcut?: string
|
||||
alternateKeybind?: string[]
|
||||
exitShellKeybind?: string[]
|
||||
}
|
||||
|
||||
export function ComposerEditor(props: ComposerEditorProps) {
|
||||
@@ -282,24 +281,6 @@ export function ComposerEditor(props: ComposerEditorProps) {
|
||||
keybind={props.alternateKeybind ?? ["Mod", "Enter"]}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={state.mode === "shell"}>
|
||||
<Button
|
||||
data-action="composer-exit-shell"
|
||||
type="button"
|
||||
variant="ghost-faint"
|
||||
size="small"
|
||||
class="me-3 gap-1.5 px-1.5"
|
||||
onClick={() => {
|
||||
props.controller.dispatch({ type: "mode.normal" })
|
||||
props.controller.restoreFocus()
|
||||
}}
|
||||
>
|
||||
{i18n.t("ui.promptInput.exitShell")}
|
||||
<span class="hidden sm:block">
|
||||
<Keybind keys={props.exitShellKeybind ?? ["ESC"]} variant="neutral" />
|
||||
</span>
|
||||
</Button>
|
||||
</Show>
|
||||
<ComposerEditorSubmitButton
|
||||
mode={state.mode}
|
||||
stopping={view.submit.stopping()}
|
||||
@@ -692,7 +673,6 @@ export function ComposerEditorPopover(props: {
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-component="composer-suggestions"
|
||||
class="absolute inset-x-0 -top-2 z-40 flex max-h-80 -translate-y-full flex-col overflow-auto rounded-xl bg-v2-background-bg-base p-2 shadow-[var(--v2-elevation-raised)] no-scrollbar"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
@@ -721,7 +701,6 @@ export function ComposerEditorPopover(props: {
|
||||
<button
|
||||
type="button"
|
||||
data-suggestion-id={item.id}
|
||||
data-active={props.activeID === item.id ? "" : undefined}
|
||||
class="flex w-full items-center gap-2 rounded-md px-2 py-1 text-start hover:bg-v2-overlay-simple-overlay-hover"
|
||||
classList={{ "bg-v2-overlay-simple-overlay-hover": props.activeID === item.id }}
|
||||
onPointerMove={() => props.onActiveChange(item)}
|
||||
@@ -770,9 +749,9 @@ function ComposerEditorAlternateDelivery(props: { controller: ComposerEditorMode
|
||||
ref={setButton}
|
||||
data-action="composer-alternate-delivery"
|
||||
type="button"
|
||||
variant="ghost-faint"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
class="me-3 gap-1.5 px-1.5 ![font-weight:530] duration-150 motion-reduce:animate-none"
|
||||
class="me-3 gap-1.5 px-1.5 text-v2-text-text-muted ![font-weight:530] duration-150 motion-reduce:animate-none"
|
||||
classList={{
|
||||
"animate-in fade-in": presence.animate() && presence.show(),
|
||||
"animate-out fade-out fill-mode-forwards": presence.animate() && !presence.show(),
|
||||
|
||||
@@ -3,8 +3,7 @@ import { type HomeProjectSelection, useLayout } from "@/shell/state/layout"
|
||||
import { ServerConnection, useServers } from "@/runtime/server/registry"
|
||||
import { useTabs } from "@/shell/tabs/tabs"
|
||||
import { toggleHomeProjectSelection } from "@/shell/layout/helpers"
|
||||
import { createEffect, createMemo, startTransition } from "solid-js"
|
||||
import type { SessionInfo } from "@opencode-ai/client/promise"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
|
||||
export function createHomeController() {
|
||||
const layout = useLayout()
|
||||
@@ -46,18 +45,6 @@ export function createHomeController() {
|
||||
void tabs.newDraft({ server: ServerConnection.key(conn), directory })
|
||||
}
|
||||
|
||||
function openProjectSession(conn: ServerConnection.Any, directory: string, session: SessionInfo) {
|
||||
const ctx = global.ensureServerCtx(conn)
|
||||
void ctx.data.session.message.sync(session.id).catch(() => undefined)
|
||||
void startTransition(() => {
|
||||
const tab = tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id })
|
||||
tabs.select(tab)
|
||||
ctx.data.session.remember(session)
|
||||
ctx.projects.open(directory)
|
||||
ctx.projects.touch(directory)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
selection: {
|
||||
value: selection,
|
||||
@@ -118,7 +105,6 @@ export function createHomeController() {
|
||||
openProjectNewSession(conn, project.worktree)
|
||||
},
|
||||
openProjectNewSession,
|
||||
openProjectSession,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Schema } from "effect"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import type { HomeController } from "../model"
|
||||
import { useGlobal } from "@/runtime/server/runtime"
|
||||
import { SessionTransfer } from "@opencode-ai/schema/session-transfer"
|
||||
|
||||
export const HomeServersSchema = Schema.Struct({
|
||||
collapsed: Persistence.record(Persistence.fallback(Schema.Boolean, () => false)),
|
||||
@@ -80,35 +79,6 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
select: home.project.select,
|
||||
add: home.project.add,
|
||||
openNewSession: home.project.openProjectNewSession,
|
||||
canImportSession: !!platform.openAttachmentPickerDialog,
|
||||
importSession: (conn: ServerConnection.Any, project: LocalProject) => {
|
||||
if (!platform.openAttachmentPickerDialog) return
|
||||
void platform
|
||||
.openAttachmentPickerDialog(
|
||||
{
|
||||
title: language.t("command.session.import"),
|
||||
accept: ["application/json"],
|
||||
extensions: ["json"],
|
||||
},
|
||||
async (file) => {
|
||||
const data = await Schema.decodeUnknownPromise(Schema.fromJsonString(SessionTransfer.Data))(
|
||||
await file.text(),
|
||||
)
|
||||
const api = home.server.context(conn).sdk.api.session
|
||||
const imported = await api.import({
|
||||
...Schema.encodeSync(SessionTransfer.Data)(data),
|
||||
location: { directory: project.worktree },
|
||||
} as Parameters<typeof api.import>[0])
|
||||
home.project.openProjectSession(conn, project.worktree, imported)
|
||||
},
|
||||
)
|
||||
.catch((cause: unknown) => {
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
description: errorMessage(cause, language.t("common.requestFailed")),
|
||||
})
|
||||
})
|
||||
},
|
||||
edit: (conn: ServerConnection.Any, project: LocalProject) => {
|
||||
void import("@/settings/workspaces/project-dialog").then(({ DialogEditProject }) => {
|
||||
void dialog.show(() => <DialogEditProject server={conn} project={project} />)
|
||||
|
||||
@@ -37,8 +37,6 @@ export function HomeProjects(props: {
|
||||
onSelectProject={props.projects.project.select}
|
||||
onAddProjects={props.projects.project.add}
|
||||
onOpenProjectNewSession={props.projects.project.openNewSession}
|
||||
canImportSession={props.projects.project.canImportSession}
|
||||
onImportSession={props.projects.project.importSession}
|
||||
onEditProject={props.projects.project.edit}
|
||||
onRevealProject={props.projects.project.reveal}
|
||||
onClearNotifications={props.projects.project.clearNotifications}
|
||||
|
||||
@@ -57,8 +57,6 @@ export type HomeProjectsViewProps = {
|
||||
onSelectProject: (server: ServerConnection.Any, directory: string) => void
|
||||
onAddProjects: (server: ServerConnection.Any, directories: string[]) => void
|
||||
onOpenProjectNewSession: (server: ServerConnection.Any, directory: string) => void
|
||||
canImportSession: boolean
|
||||
onImportSession: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
onEditProject: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
onRevealProject: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
onClearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
|
||||
@@ -672,11 +670,6 @@ function HomeProjectRow(
|
||||
<Menu.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
|
||||
{props.language.t("command.session.new")}
|
||||
</Menu.Item>
|
||||
<Show when={props.canImportSession}>
|
||||
<Menu.Item onSelect={() => props.onImportSession(props.server, props.project)}>
|
||||
{props.language.t("command.session.import")}
|
||||
</Menu.Item>
|
||||
</Show>
|
||||
<Menu.Item onSelect={() => props.onEditProject(props.server, props.project)}>
|
||||
{props.language.t("dialog.project.edit.title")}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -21,8 +21,7 @@ import { errorMessage } from "@/shell/layout/helpers"
|
||||
import { useSessionTabAvatarState } from "@/shell/layout/project-avatar-state"
|
||||
import { removedSessionIDs } from "@/session/session-domain"
|
||||
import { pathKey } from "@/workspaces/path-key"
|
||||
import { fetchSessionExport, saveSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { sessionLabel, sessionTitle } from "@/session/title"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { archiveHomeSession } from "./archive"
|
||||
@@ -46,7 +45,6 @@ export function createHomeSessionsController(home: HomeController) {
|
||||
const command = useCommand()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const queryClient = useQueryClient()
|
||||
const projectDirectories = createMemo(() => {
|
||||
const selected = home.selection.value().directory
|
||||
@@ -174,7 +172,7 @@ export function createHomeSessionsController(home: HomeController) {
|
||||
try {
|
||||
const data = await fetchSessionExport({ sessionID: session.id, api: ctx.sdk.api })
|
||||
const filename = sessionExportFilename(data.info)
|
||||
if (!(await saveSessionExport(filename, data, platform))) return
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "ሳይክል ቋንቋ",
|
||||
"command.language.set": "ቋንቋን ተጠቀም፡ {{language}}",
|
||||
"command.session.new": "አዲስ ክፍለ ጊዜ",
|
||||
"command.session.import": "ክፍለ ጊዜ ማስመጣት",
|
||||
"command.file.open": "ክፍት ፋይል",
|
||||
"command.tab.close": "ትርፉን ዝጋ",
|
||||
"command.tab.reopenClosed": "የተዘጋውን ትር እንደገና ክፈት",
|
||||
|
||||
@@ -139,7 +139,6 @@ export const dict = {
|
||||
"command.language.cycle": "تغيير اللغة",
|
||||
"command.language.set": "استخدام اللغة: {{language}}",
|
||||
"command.session.new": "جلسة جديدة",
|
||||
"command.session.import": "استيراد جلسة",
|
||||
"command.file.open": "فتح ملف",
|
||||
"command.tab.close": "إغلاق علامة التبويب",
|
||||
"command.tab.reopenClosed": "إعادة فتح علامة التبويب المغلقة",
|
||||
|
||||
@@ -135,7 +135,6 @@ export const dict = {
|
||||
"command.language.cycle": "Dili dəyiş",
|
||||
"command.language.set": "Dildən istifadə et: {{language}}",
|
||||
"command.session.new": "Yeni sessiya",
|
||||
"command.session.import": "Sessiyanı idxal et",
|
||||
"command.file.open": "Faylı aç",
|
||||
"command.tab.close": "Tabı bağla",
|
||||
"command.tab.reopenClosed": "Bağlanmış tabı yenidən aç",
|
||||
|
||||
@@ -135,7 +135,6 @@ export const dict = {
|
||||
"command.language.cycle": "Цикличен език",
|
||||
"command.language.set": "Използвайте език: {{language}}",
|
||||
"command.session.new": "Нова сесия",
|
||||
"command.session.import": "Импортиране на сесия",
|
||||
"command.file.open": "Отворете файла",
|
||||
"command.tab.close": "Затваряне на раздела",
|
||||
"command.tab.reopenClosed": "Повторно отваряне на затворен раздел",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict: Record<string, string> = {
|
||||
"command.language.cycle": "সাইকেল ভাষা",
|
||||
"command.language.set": "ভাষা ব্যবহার করুন: {{language}}",
|
||||
"command.session.new": "নতুন সেশন",
|
||||
"command.session.import": "সেশন আমদানি করুন",
|
||||
"command.file.open": "ফাইল খুলুন",
|
||||
"command.tab.close": "ট্যাব বন্ধ করুন",
|
||||
"command.tab.reopenClosed": "বন্ধ ট্যাব আবার খুলুন",
|
||||
|
||||
@@ -141,7 +141,6 @@ export const dict = {
|
||||
"command.language.cycle": "Alternar idioma",
|
||||
"command.language.set": "Usar idioma: {{language}}",
|
||||
"command.session.new": "Nova sessão",
|
||||
"command.session.import": "Importar sessão",
|
||||
"command.file.open": "Abrir arquivo",
|
||||
"command.tab.close": "Fechar aba",
|
||||
"command.tab.reopenClosed": "Reabrir aba fechada",
|
||||
|
||||
@@ -147,7 +147,6 @@ export const dict = {
|
||||
"command.language.set": "Koristi jezik: {{language}}",
|
||||
|
||||
"command.session.new": "Nova sesija",
|
||||
"command.session.import": "Uvezi sesiju",
|
||||
"command.file.open": "Otvori datoteku",
|
||||
"command.tab.close": "Zatvori karticu",
|
||||
"command.tab.reopenClosed": "Ponovo otvori zatvorenu karticu",
|
||||
|
||||
@@ -135,7 +135,6 @@ export const dict = {
|
||||
"command.language.cycle": "Llenguatge de cicle",
|
||||
"command.language.set": "Utilitza l'idioma: {{language}}",
|
||||
"command.session.new": "Nova sessió",
|
||||
"command.session.import": "Importa la sessió",
|
||||
"command.file.open": "Obre el fitxer",
|
||||
"command.tab.close": "Tanca la pestanya",
|
||||
"command.tab.reopenClosed": "Torneu a obrir la pestanya tancada",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Jazyk cyklu",
|
||||
"command.language.set": "Použít jazyk: {{language}}",
|
||||
"command.session.new": "Nová relace",
|
||||
"command.session.import": "Importovat relaci",
|
||||
"command.file.open": "Otevřít soubor",
|
||||
"command.tab.close": "Zavřít kartu",
|
||||
"command.tab.reopenClosed": "Znovu otevřete zavřenou kartu",
|
||||
|
||||
@@ -46,7 +46,6 @@ export const dict = {
|
||||
"command.language.set": "Brug sprog: {{language}}",
|
||||
|
||||
"command.session.new": "Ny session",
|
||||
"command.session.import": "Importer session",
|
||||
"command.file.open": "Åbn fil",
|
||||
"command.tab.close": "Luk fane",
|
||||
"command.tab.reopenClosed": "Åbn lukket fane igen",
|
||||
|
||||
@@ -44,7 +44,6 @@ export const dict = {
|
||||
"command.language.cycle": "Sprache wechseln",
|
||||
"command.language.set": "Sprache verwenden: {{language}}",
|
||||
"command.session.new": "Neue Sitzung",
|
||||
"command.session.import": "Sitzung importieren",
|
||||
"command.file.open": "Datei öffnen",
|
||||
"command.tab.close": "Tab schließen",
|
||||
"command.tab.reopenClosed": "Geschlossenen Tab wieder öffnen",
|
||||
|
||||
@@ -136,7 +136,6 @@ export const dict = {
|
||||
"command.language.cycle": "ސައިކަލް ބަސް",
|
||||
"command.language.set": "ބަސް ބޭނުންކުރުން: {{language}}",
|
||||
"command.session.new": "އާ ޖަލްސާއެއް",
|
||||
"command.session.import": "ޖަލްސާ އިމްޕޯޓް ކުރައްވާ",
|
||||
"command.file.open": "ފައިލް ހުޅުވާލާށެވެ",
|
||||
"command.tab.close": "ޓެބް ބަންދުކުރުން",
|
||||
"command.tab.reopenClosed": "ބަންދުކޮށްފައިވާ ޓެބް އަލުން ހުޅުވާލާށެވެ",
|
||||
|
||||
@@ -136,7 +136,6 @@ export const dict: Record<string, string> = {
|
||||
"command.language.cycle": "འཁོར་བའི་སྐད་ཡིག།",
|
||||
"command.language.set": "སྐད་ཡིག་ལག་ལེན་འཐབ།: {{language}}",
|
||||
"command.session.new": "ལཱ་ཡུན་གསརཔ།",
|
||||
"command.session.import": "ལཱ་ཡུན་ནང་འདྲེན།",
|
||||
"command.file.open": "ཡིག་སྣོད་ཁ་ཕྱེ།",
|
||||
"command.tab.close": "མཆོང་ལྡེ་ཁ་བསྡམས།",
|
||||
"command.tab.reopenClosed": "ཁ་བསྡམས་ཡོད་པའི་མཆོང་ལྡེ་ལོག་ཁ་ཕྱེ།",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "Γλώσσα κύκλου",
|
||||
"command.language.set": "Γλώσσα χρήσης: {{language}}",
|
||||
"command.session.new": "Νέα συνεδρία",
|
||||
"command.session.import": "Εισαγωγή συνεδρίας",
|
||||
"command.file.open": "Άνοιγμα αρχείου",
|
||||
"command.tab.close": "Κλείσιμο καρτέλας",
|
||||
"command.tab.reopenClosed": "Άνοιγμα ξανά κλειστής καρτέλας",
|
||||
|
||||
@@ -100,7 +100,6 @@ export const dict = {
|
||||
"command.session.fork.description": "Create a new session from a previous message",
|
||||
"command.session.export": "Export session",
|
||||
"command.session.export.description": "Export the full session transcript as JSON",
|
||||
"command.session.import": "Import session",
|
||||
"command.session.copyID": "Copy Session ID",
|
||||
|
||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||
@@ -676,14 +675,11 @@ export const dict = {
|
||||
"session.error.incompatible.description":
|
||||
"{{server}} is running OpenCode {{version}}, which isn't compatible with this app. Upgrade the server to OpenCode V2 to continue.",
|
||||
"session.background.moveTasks": "Move {{tasks}} to background",
|
||||
"session.background.moveRunning": "Move running work to background",
|
||||
"session.background.inBackground": "Running {{tasks}} in background",
|
||||
"session.background.moveInline": "Press {{keybind}} to move running work to the background",
|
||||
"session.background.running": "Running work in background",
|
||||
"session.background.runningCount.one": "{{count}} item running in background",
|
||||
"session.background.runningCount.other": "{{count}} items running in background",
|
||||
"session.background.tasksRunning.one": "{{count}} background task running",
|
||||
"session.background.tasksRunning.other": "{{count}} background tasks running",
|
||||
"session.background.combine": "{{first}} and {{second}}",
|
||||
"session.background.shell.one": "{{count}} shell",
|
||||
"session.background.shell.other": "{{count}} shells",
|
||||
|
||||
@@ -147,7 +147,6 @@ export const dict = {
|
||||
"command.language.set": "Usar idioma: {{language}}",
|
||||
|
||||
"command.session.new": "Nueva sesión",
|
||||
"command.session.import": "Importar sesión",
|
||||
"command.file.open": "Abrir archivo",
|
||||
"command.tab.close": "Cerrar pestaña",
|
||||
"command.tab.reopenClosed": "Reabrir pestaña cerrada",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Tsükli keel",
|
||||
"command.language.set": "Kasuta keelt: {{language}}",
|
||||
"command.session.new": "Uus seanss",
|
||||
"command.session.import": "Impordi seanss",
|
||||
"command.file.open": "Ava fail",
|
||||
"command.tab.close": "Sule vahekaart",
|
||||
"command.tab.reopenClosed": "Ava suletud vaheleht uuesti",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "زبان چرخه",
|
||||
"command.language.set": "استفاده از زبان: {{language}}",
|
||||
"command.session.new": "جلسه جدید",
|
||||
"command.session.import": "وارد کردن جلسه",
|
||||
"command.file.open": "باز کردن فایل",
|
||||
"command.tab.close": "بستن برگه",
|
||||
"command.tab.reopenClosed": "برگه بسته را دوباره باز کنید",
|
||||
|
||||
@@ -40,7 +40,6 @@ export const dict = {
|
||||
"command.language.cycle": "Vaihda kieltä",
|
||||
"command.language.set": "Käytä kieltä: {{language}}",
|
||||
"command.session.new": "Uusi istunto",
|
||||
"command.session.import": "Tuo istunto",
|
||||
"command.file.open": "Avaa tiedosto",
|
||||
"command.tab.close": "Sulje välilehti",
|
||||
"command.tab.reopenClosed": "Avaa suljettu välilehti uudelleen",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Súkklumál",
|
||||
"command.language.set": "Brúka mál: {{language}}",
|
||||
"command.session.new": "Nýggj setan",
|
||||
"command.session.import": "Innflyt setan",
|
||||
"command.file.open": "Opna fíluna",
|
||||
"command.tab.close": "Lat flipan aftur",
|
||||
"command.tab.reopenClosed": "Opna aftur stongdan flipan",
|
||||
|
||||
@@ -141,7 +141,6 @@ export const dict = {
|
||||
"command.language.cycle": "Changer de langue",
|
||||
"command.language.set": "Utiliser la langue : {{language}}",
|
||||
"command.session.new": "Nouvelle session",
|
||||
"command.session.import": "Importer une session",
|
||||
"command.file.open": "Ouvrir un fichier",
|
||||
"command.tab.close": "Fermer l'onglet",
|
||||
"command.tab.reopenClosed": "Rouvrir l'onglet fermé",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "מעבר לשפה הבאה",
|
||||
"command.language.set": "השתמש בשפה: {{language}}",
|
||||
"command.session.new": "הפעלה חדשה",
|
||||
"command.session.import": "ייבוא הפעלה",
|
||||
"command.file.open": "פתח את הקובץ",
|
||||
"command.tab.close": "סגור כרטיסייה",
|
||||
"command.tab.reopenClosed": "פתח מחדש את הכרטיסייה הסגורה",
|
||||
|
||||
@@ -140,7 +140,6 @@ export const dict = {
|
||||
"command.language.cycle": "भाषा बदलें",
|
||||
"command.language.set": "भाषा का प्रयोग करें: {{language}}",
|
||||
"command.session.new": "नया सेशन",
|
||||
"command.session.import": "सेशन आयात करें",
|
||||
"command.file.open": "फ़ाइल खोलें",
|
||||
"command.tab.close": "टैब बंद करें",
|
||||
"command.tab.reopenClosed": "बंद टैब पुनः खोलें",
|
||||
|
||||
@@ -137,7 +137,6 @@ export const dict = {
|
||||
"command.language.cycle": "Promijeni jezik",
|
||||
"command.language.set": "Koristite jezik: {{language}}",
|
||||
"command.session.new": "Nova sesija",
|
||||
"command.session.import": "Uvezi sesiju",
|
||||
"command.file.open": "Otvori datoteku",
|
||||
"command.tab.close": "Zatvori karticu",
|
||||
"command.tab.reopenClosed": "Ponovno otvori zatvorenu karticu",
|
||||
|
||||
@@ -137,7 +137,6 @@ export const dict = {
|
||||
"command.language.cycle": "Nyelv váltása",
|
||||
"command.language.set": "Nyelv használata: {{language}}",
|
||||
"command.session.new": "Új munkamenet",
|
||||
"command.session.import": "Munkamenet importálása",
|
||||
"command.file.open": "Nyissa meg a fájlt",
|
||||
"command.tab.close": "Lap bezárása",
|
||||
"command.tab.reopenClosed": "Nyissa meg újra a bezárt lapot",
|
||||
|
||||
@@ -135,7 +135,6 @@ export const dict = {
|
||||
"command.language.cycle": "Ցիկլի լեզու",
|
||||
"command.language.set": "Օգտագործել լեզուն՝ {{language}}",
|
||||
"command.session.new": "Նոր նիստ",
|
||||
"command.session.import": "Ներմուծել նիստը",
|
||||
"command.file.open": "Բացել ֆայլ",
|
||||
"command.tab.close": "Փակել ներդիրը",
|
||||
"command.tab.reopenClosed": "Վերաբացել փակ ներդիրը",
|
||||
|
||||
@@ -147,7 +147,6 @@ export const dict = {
|
||||
"command.language.set": "Gunakan bahasa: {{language}}",
|
||||
|
||||
"command.session.new": "Sesi baru",
|
||||
"command.session.import": "Impor sesi",
|
||||
"command.file.open": "Buka berkas",
|
||||
"command.tab.close": "Tutup tab",
|
||||
"command.tab.reopenClosed": "Buka kembali tab yang ditutup",
|
||||
|
||||
@@ -137,7 +137,6 @@ export const dict = {
|
||||
"command.language.cycle": "Skipta um tungumál",
|
||||
"command.language.set": "Notaðu tungumál: {{language}}",
|
||||
"command.session.new": "Ný seta",
|
||||
"command.session.import": "Flytja inn setu",
|
||||
"command.file.open": "Opna skrá",
|
||||
"command.tab.close": "Loka flipa",
|
||||
"command.tab.reopenClosed": "Opnaðu aftur lokaðan flipa",
|
||||
|
||||
@@ -41,7 +41,6 @@ export const dict = {
|
||||
"command.language.cycle": "Cambia lingua",
|
||||
"command.language.set": "Usa la lingua: {{language}}",
|
||||
"command.session.new": "Nuova sessione",
|
||||
"command.session.import": "Importa sessione",
|
||||
"command.file.open": "Apri file",
|
||||
"command.tab.close": "Chiudi scheda",
|
||||
"command.tab.reopenClosed": "Riapri la scheda chiusa",
|
||||
|
||||
@@ -139,7 +139,6 @@ export const dict = {
|
||||
"command.language.cycle": "言語の切り替え",
|
||||
"command.language.set": "言語を使用: {{language}}",
|
||||
"command.session.new": "新しいセッション",
|
||||
"command.session.import": "セッションをインポート",
|
||||
"command.file.open": "ファイルを開く",
|
||||
"command.tab.close": "タブを閉じる",
|
||||
"command.tab.reopenClosed": "閉じたタブを再度開く",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "ციკლის ენა",
|
||||
"command.language.set": "გამოიყენე ენა: {{language}}",
|
||||
"command.session.new": "ახალი სესია",
|
||||
"command.session.import": "სესიის იმპორტი",
|
||||
"command.file.open": "გახსენით ფაილი",
|
||||
"command.tab.close": "ჩანართის დახურვა",
|
||||
"command.tab.reopenClosed": "დახურული ჩანართის ხელახლა გახსნა",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "ភាសាវដ្ត",
|
||||
"command.language.set": "ប្រើភាសា៖ {{language}}",
|
||||
"command.session.new": "សម័យថ្មី។",
|
||||
"command.session.import": "នាំចូលសម័យ",
|
||||
"command.file.open": "បើកឯកសារ",
|
||||
"command.tab.close": "បិទផ្ទាំង",
|
||||
"command.tab.reopenClosed": "បើកផ្ទាំងបិទឡើងវិញ",
|
||||
|
||||
@@ -37,7 +37,6 @@ export const dict = {
|
||||
"command.language.cycle": "언어 순환",
|
||||
"command.language.set": "언어 사용: {{language}}",
|
||||
"command.session.new": "새 세션",
|
||||
"command.session.import": "세션 가져오기",
|
||||
"command.file.open": "파일 열기",
|
||||
"command.tab.close": "탭 닫기",
|
||||
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "ພາສາຮອບວຽນ",
|
||||
"command.language.set": "ໃຊ້ພາສາ: {{language}}",
|
||||
"command.session.new": "ເຊດຊັນໃໝ່",
|
||||
"command.session.import": "ນຳເຂົ້າເຊດຊັນ",
|
||||
"command.file.open": "ເປີດໄຟລ໌",
|
||||
"command.tab.close": "ປິດແຖບ",
|
||||
"command.tab.reopenClosed": "ເປີດແຖບປິດຄືນໃໝ່",
|
||||
|
||||
@@ -137,7 +137,6 @@ export const dict = {
|
||||
"command.language.cycle": "Perjungti kalbą",
|
||||
"command.language.set": "Naudokite kalbą: {{language}}",
|
||||
"command.session.new": "Naujas seansas",
|
||||
"command.session.import": "Importuoti seansą",
|
||||
"command.file.open": "Atidaryti failą",
|
||||
"command.tab.close": "Uždaryti skirtuką",
|
||||
"command.tab.reopenClosed": "Iš naujo atidaryti uždarytą skirtuką",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Mainīt valodu",
|
||||
"command.language.set": "Izmantot valodu: {{language}}",
|
||||
"command.session.new": "Jauna sesija",
|
||||
"command.session.import": "Importēt sesiju",
|
||||
"command.file.open": "Atvērt failu",
|
||||
"command.tab.close": "Aizvērt cilni",
|
||||
"command.tab.reopenClosed": "Atvērt aizvērtu cilni",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "Јазик на циклус",
|
||||
"command.language.set": "Користете јазик: {{language}}",
|
||||
"command.session.new": "Нова сесија",
|
||||
"command.session.import": "Увези сесија",
|
||||
"command.file.open": "Отворете ја датотеката",
|
||||
"command.tab.close": "Затвори ја картичката",
|
||||
"command.tab.reopenClosed": "Повторно отворете го затворениот таб",
|
||||
|
||||
@@ -135,7 +135,6 @@ export const dict = {
|
||||
"command.language.cycle": "Циклийн хэл",
|
||||
"command.language.set": "Хэл ашиглах: {{language}}",
|
||||
"command.session.new": "Шинэ сесс",
|
||||
"command.session.import": "Сесс импортлох",
|
||||
"command.file.open": "Файлыг нээх",
|
||||
"command.tab.close": "Табыг хаах",
|
||||
"command.tab.reopenClosed": "Хаагдсан табыг дахин нээнэ үү",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Tukar bahasa",
|
||||
"command.language.set": "Guna bahasa: {{language}}",
|
||||
"command.session.new": "Sesi baharu",
|
||||
"command.session.import": "Import sesi",
|
||||
"command.file.open": "Buka fail",
|
||||
"command.tab.close": "Tutup tab",
|
||||
"command.tab.reopenClosed": "Buka semula tab tertutup",
|
||||
|
||||
@@ -135,7 +135,6 @@ export const dict = {
|
||||
"command.language.cycle": "စက်ဝိုင်းဘာသာစကား",
|
||||
"command.language.set": "ဘာသာစကားကို အသုံးပြုပါ- {{language}}",
|
||||
"command.session.new": "စက်ရှင်အသစ်",
|
||||
"command.session.import": "စက်ရှင် တင်သွင်းရန်",
|
||||
"command.file.open": "ဖိုင်ကိုဖွင့်ပါ။",
|
||||
"command.tab.close": "တဘ်ကို ပိတ်ပါ။",
|
||||
"command.tab.reopenClosed": "ပိတ်ထားသော တက်ဘ်ကို ပြန်ဖွင့်ပါ။",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict: Record<string, string> = {
|
||||
"command.language.cycle": "साइकल भाषा",
|
||||
"command.language.set": "भाषा प्रयोग गर्नुहोस्: {{language}}",
|
||||
"command.session.new": "नयाँ सत्र",
|
||||
"command.session.import": "सत्र आयात गर्नुहोस्",
|
||||
"command.file.open": "फाइल खोल्नुहोस्",
|
||||
"command.tab.close": "ट्याब बन्द गर्नुहोस्",
|
||||
"command.tab.reopenClosed": "बन्द ट्याब पुन: खोल्नुहोस्",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Volgende taal",
|
||||
"command.language.set": "Gebruik taal: {{language}}",
|
||||
"command.session.new": "Nieuwe sessie",
|
||||
"command.session.import": "Sessie importeren",
|
||||
"command.file.open": "Bestand openen",
|
||||
"command.tab.close": "Tabblad sluiten",
|
||||
"command.tab.reopenClosed": "Gesloten tabblad opnieuw openen",
|
||||
|
||||
@@ -146,7 +146,6 @@ export const dict = {
|
||||
"command.language.set": "Bruk språk: {{language}}",
|
||||
|
||||
"command.session.new": "Ny sesjon",
|
||||
"command.session.import": "Importer sesjon",
|
||||
"command.file.open": "Åpne fil",
|
||||
"command.tab.close": "Lukk fane",
|
||||
"command.context.addSelection": "Legg til markering i kontekst",
|
||||
|
||||
@@ -139,7 +139,6 @@ export const dict = {
|
||||
"command.language.cycle": "اگلی بولی ورتو",
|
||||
"command.language.set": "بولی ورتو: {{language}}",
|
||||
"command.session.new": "نواں سیشن",
|
||||
"command.session.import": "سیشن درآمد کرو",
|
||||
"command.file.open": "فائل کھولو",
|
||||
"command.tab.close": "ٹیب بند کرو",
|
||||
"command.tab.reopenClosed": "بند ٹیب دوبارہ کھولو",
|
||||
|
||||
@@ -140,7 +140,6 @@ export const dict = {
|
||||
"command.language.cycle": "Przełącz język",
|
||||
"command.language.set": "Użyj języka: {{language}}",
|
||||
"command.session.new": "Nowa sesja",
|
||||
"command.session.import": "Importuj sesję",
|
||||
"command.file.open": "Otwórz plik",
|
||||
"command.tab.close": "Zamknij kartę",
|
||||
"command.tab.reopenClosed": "Otwórz ponownie zamkniętą kartę",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Schimbă limba",
|
||||
"command.language.set": "Folosește limba: {{language}}",
|
||||
"command.session.new": "Sesiune nouă",
|
||||
"command.session.import": "Importă sesiunea",
|
||||
"command.file.open": "Deschide fișier",
|
||||
"command.tab.close": "Închide fila",
|
||||
"command.tab.reopenClosed": "Redeschide fila închisă",
|
||||
|
||||
@@ -146,7 +146,6 @@ export const dict = {
|
||||
"command.language.set": "Использовать язык: {{language}}",
|
||||
|
||||
"command.session.new": "Новая сессия",
|
||||
"command.session.import": "Импортировать сессию",
|
||||
"command.file.open": "Открыть файл",
|
||||
"command.tab.close": "Закрыть вкладку",
|
||||
"command.tab.reopenClosed": "Повторно открыть закрытую вкладку",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict: Record<string, string> = {
|
||||
"command.language.cycle": "චක්ර භාෂාව",
|
||||
"command.language.set": "භාෂාව භාවිතා කරන්න: {{language}}",
|
||||
"command.session.new": "නව සැසිය",
|
||||
"command.session.import": "සැසිය ආනයනය කරන්න",
|
||||
"command.file.open": "ගොනුව විවෘත කරන්න",
|
||||
"command.tab.close": "ටැබ් එක වසන්න",
|
||||
"command.tab.reopenClosed": "වසා දැමූ ටැබය නැවත විවෘත කරන්න",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Prepnúť jazyk",
|
||||
"command.language.set": "Použiť jazyk: {{language}}",
|
||||
"command.session.new": "Nová relácia",
|
||||
"command.session.import": "Importovať reláciu",
|
||||
"command.file.open": "Otvoriť súbor",
|
||||
"command.tab.close": "Zavrieť kartu",
|
||||
"command.tab.reopenClosed": "Obnoviť zatvorenú kartu",
|
||||
|
||||
@@ -133,7 +133,6 @@ export const dict = {
|
||||
"command.language.cycle": "Jezik cikla",
|
||||
"command.language.set": "Uporabi jezik: {{language}}",
|
||||
"command.session.new": "Nova seja",
|
||||
"command.session.import": "Uvozi sejo",
|
||||
"command.file.open": "Odpri datoteko",
|
||||
"command.tab.close": "Zapri zavihek",
|
||||
"command.tab.reopenClosed": "Ponovno odpri zaprt zavihek",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "Gjuha e ciklit",
|
||||
"command.language.set": "Përdorni gjuhën: {{language}}",
|
||||
"command.session.new": "Sesion i ri",
|
||||
"command.session.import": "Importo sesionin",
|
||||
"command.file.open": "Hap skedarin",
|
||||
"command.tab.close": "Mbyll skedën",
|
||||
"command.tab.reopenClosed": "Rihap skedën e mbyllur",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "језик циклуса",
|
||||
"command.language.set": "Користи језик: {{language}}",
|
||||
"command.session.new": "Нова сесија",
|
||||
"command.session.import": "Увези сесију",
|
||||
"command.file.open": "Отворите датотеку",
|
||||
"command.tab.close": "Затвори картицу",
|
||||
"command.tab.reopenClosed": "Поново отворите затворену картицу",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "Växla språk",
|
||||
"command.language.set": "Använd språk: {{language}}",
|
||||
"command.session.new": "Ny session",
|
||||
"command.session.import": "Importera session",
|
||||
"command.file.open": "Öppna filen",
|
||||
"command.tab.close": "Stäng fliken",
|
||||
"command.tab.reopenClosed": "Öppna stängd flik igen",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "Забони даврӣ",
|
||||
"command.language.set": "Истифодаи забон: {{language}}",
|
||||
"command.session.new": "Сеанси нав",
|
||||
"command.session.import": "Воридоти сеанс",
|
||||
"command.file.open": "Файлро кушоед",
|
||||
"command.tab.close": "Варақаро пӯшед",
|
||||
"command.tab.reopenClosed": "Варақаи пӯшидаро аз нав кушоед",
|
||||
|
||||
@@ -145,7 +145,6 @@ export const dict = {
|
||||
"command.language.set": "ใช้ภาษา: {{language}}",
|
||||
|
||||
"command.session.new": "เซสชันใหม่",
|
||||
"command.session.import": "นำเข้าเซสชัน",
|
||||
"command.file.open": "เปิดไฟล์",
|
||||
"command.tab.close": "ปิดแท็บ",
|
||||
"command.tab.reopenClosed": "เปิดแท็บที่ปิดไปอีกครั้ง",
|
||||
|
||||
@@ -134,7 +134,6 @@ export const dict = {
|
||||
"command.language.cycle": "Sikl dili",
|
||||
"command.language.set": "Dil ulanyň: {{language}}",
|
||||
"command.session.new": "Täze sessiýa",
|
||||
"command.session.import": "Sessiýany import et",
|
||||
"command.file.open": "Faýl açyň",
|
||||
"command.tab.close": "Salgy ýapyň",
|
||||
"command.tab.reopenClosed": "Closedapyk goýmany açyň",
|
||||
|
||||
@@ -151,7 +151,6 @@ export const dict = {
|
||||
"command.language.set": "Dil kullan: {{language}}",
|
||||
|
||||
"command.session.new": "Yeni oturum",
|
||||
"command.session.import": "Oturumu içe aktar",
|
||||
"command.file.open": "Dosya aç",
|
||||
"command.tab.close": "Sekmeyi kapat",
|
||||
"command.tab.reopenClosed": "Kapatılan sekmeyi yeniden aç",
|
||||
|
||||
@@ -147,7 +147,6 @@ export const dict = {
|
||||
"command.language.set": "Використати мову: {{language}}",
|
||||
|
||||
"command.session.new": "Нова сесія",
|
||||
"command.session.import": "Імпортувати сесію",
|
||||
"command.file.open": "Відкрити файл",
|
||||
"command.tab.close": "Закрити вкладку",
|
||||
"command.tab.reopenClosed": "Повторно відкрити закриту вкладку",
|
||||
|
||||
@@ -141,7 +141,6 @@ export const dict = {
|
||||
"command.language.cycle": "اگلی زبان منتخب کریں",
|
||||
"command.language.set": "زبان استعمال کریں: {{language}}",
|
||||
"command.session.new": "نیا سیشن",
|
||||
"command.session.import": "سیشن درآمد کریں",
|
||||
"command.file.open": "فائل کھولیں۔",
|
||||
"command.tab.close": "ٹیب بند کریں۔",
|
||||
"command.tab.reopenClosed": "بند ٹیب کو دوبارہ کھولیں۔",
|
||||
|
||||
@@ -135,7 +135,6 @@ export const dict = {
|
||||
"command.language.cycle": "Keyingi til",
|
||||
"command.language.set": "Tildan foydalaning: {{language}}",
|
||||
"command.session.new": "Yangi sessiya",
|
||||
"command.session.import": "Sessiyani import qilish",
|
||||
"command.file.open": "Faylni ochish",
|
||||
"command.tab.close": "Tabni yoping",
|
||||
"command.tab.reopenClosed": "Yopiq tabni qayta oching",
|
||||
|
||||
@@ -140,7 +140,6 @@ export const dict = {
|
||||
"command.language.cycle": "Chuyển ngôn ngữ",
|
||||
"command.language.set": "Sử dụng ngôn ngữ: {{language}}",
|
||||
"command.session.new": "Phiên mới",
|
||||
"command.session.import": "Nhập phiên",
|
||||
"command.file.open": "Mở tệp",
|
||||
"command.tab.close": "Đóng tab",
|
||||
"command.tab.reopenClosed": "Mở lại tab đã đóng",
|
||||
|
||||
@@ -154,7 +154,6 @@ export const dict = {
|
||||
"command.language.set": "使用语言:{{language}}",
|
||||
|
||||
"command.session.new": "新建会话",
|
||||
"command.session.import": "导入会话",
|
||||
|
||||
"command.file.open": "打开文件",
|
||||
|
||||
|
||||
@@ -149,7 +149,6 @@ export const dict = {
|
||||
"command.language.set": "使用語言: {{language}}",
|
||||
|
||||
"command.session.new": "新增工作階段",
|
||||
"command.session.import": "匯入工作階段",
|
||||
"command.file.open": "開啟檔案",
|
||||
"command.tab.close": "關閉分頁",
|
||||
"command.tab.reopenClosed": "重新開啟已關閉的分頁",
|
||||
|
||||
@@ -59,8 +59,8 @@ type PlatformBase = {
|
||||
/** Resolve the native source path for a desktop File. */
|
||||
getPathForFile?(file: File): string
|
||||
|
||||
/** Open a native save file dialog and write content to the selected path (desktop only) */
|
||||
saveFile?(opts: SaveFilePickerOptions, content: string): Promise<boolean>
|
||||
/** Open a native save file picker dialog (desktop only) */
|
||||
saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise<string | null>
|
||||
|
||||
/** Storage mechanism, defaults to localStorage */
|
||||
storage?: (name?: string) => SyncStorage | AsyncStorage
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { AgentListOutput, ModelListOutput, Project, ProviderListOutput } from "@opencode-ai/client/promise"
|
||||
import {
|
||||
directoryKey,
|
||||
normalizeAgentList,
|
||||
normalizeProjectInfo,
|
||||
normalizeProviderList,
|
||||
updateProjectInfo,
|
||||
} from "./utils"
|
||||
import { directoryKey, normalizeAgentList, normalizeProjectInfo, normalizeProviderList } from "./utils"
|
||||
|
||||
describe("normalizeAgentList", () => {
|
||||
test("adapts current agents to the app agent shape", () => {
|
||||
@@ -116,34 +110,3 @@ describe("directoryKey", () => {
|
||||
expect(String(directoryKey("/"))).toBe("/")
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateProjectInfo", () => {
|
||||
test("applies saved metadata without losing workspace inventory", () => {
|
||||
const update = {
|
||||
id: "project",
|
||||
canonical: "/repo",
|
||||
name: "Repo",
|
||||
icon: { color: "purple" },
|
||||
time: { created: 1, updated: 2 },
|
||||
sandboxes: ["/repo-sandbox"],
|
||||
} satisfies Project
|
||||
|
||||
expect(
|
||||
updateProjectInfo(
|
||||
{
|
||||
...update,
|
||||
name: "Old name",
|
||||
icon: { color: "gray" },
|
||||
worktree: "/old-repo",
|
||||
worktrees: [{ directory: "/repo", strategy: "git" }],
|
||||
},
|
||||
update,
|
||||
),
|
||||
).toMatchObject({
|
||||
name: "Repo",
|
||||
icon: { color: "purple" },
|
||||
worktree: "/repo",
|
||||
worktrees: [{ directory: "/repo", strategy: "git" }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -137,12 +137,3 @@ export function normalizeProjectInfo(project: Project | CurrentProject): Project
|
||||
worktrees: "worktrees" in project ? project.worktrees : [{ directory: worktree }],
|
||||
}
|
||||
}
|
||||
|
||||
export function updateProjectInfo(project: Project, update: CurrentProject): Project {
|
||||
return {
|
||||
...project,
|
||||
...update,
|
||||
worktree: update.canonical,
|
||||
worktrees: project.worktrees,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ProjectMeta } from "./global-sync/types"
|
||||
import { formatServerError } from "@/runtime/server/errors"
|
||||
import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
|
||||
import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { directoryKey, updateProjectInfo } from "./global-sync/utils"
|
||||
import { directoryKey } from "./global-sync/utils"
|
||||
import { PathKey } from "@/workspaces/path-key"
|
||||
import type { ServerScope } from "@/runtime/server/scope"
|
||||
import { persisted } from "@/runtime/persistence/storage"
|
||||
@@ -215,15 +215,8 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) {
|
||||
return promise
|
||||
}
|
||||
|
||||
function applyProjectUpdate(update: Parameters<typeof updateProjectInfo>[1]) {
|
||||
setProjects((projects) =>
|
||||
projects.map((project) => (project.id === update.id ? updateProjectInfo(project, update) : project)),
|
||||
)
|
||||
}
|
||||
|
||||
const unsub = serverSDK.event.listen((event) => {
|
||||
connection.handleEvent({ type: event.type })
|
||||
if (event.type === "project.updated") applyProjectUpdate(event.data)
|
||||
|
||||
if (!event.location) {
|
||||
if (event.type === "config.updated" || event.type === "agent.updated" || event.type === "worktree.updated")
|
||||
@@ -250,7 +243,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) {
|
||||
})
|
||||
|
||||
const projectApi = {
|
||||
update: applyProjectUpdate,
|
||||
meta(directory: string, patch: ProjectMeta) {
|
||||
children.projectMeta(directory, patch)
|
||||
},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
import type { ServerApi } from "@/runtime/server/api"
|
||||
import type { Platform } from "@/runtime/platform/platform"
|
||||
import { fetchSessionExport, saveSessionExport, sessionExportFilename } from "./export"
|
||||
import { fetchSessionExport, sessionExportFilename } from "./export"
|
||||
|
||||
describe("sessionExportFilename", () => {
|
||||
test("generates filename from title", () => {
|
||||
@@ -56,31 +55,3 @@ describe("fetchSessionExport", () => {
|
||||
expect(fetchSessionExport({ sessionID: "ses_missing", api })).rejects.toThrow("Session not found")
|
||||
})
|
||||
})
|
||||
|
||||
describe("saveSessionExport", () => {
|
||||
test("returns false when the native save dialog is cancelled", async () => {
|
||||
const calls: string[][] = []
|
||||
const platform: Pick<Platform, "saveFile"> = {
|
||||
saveFile: async (_options, content) => {
|
||||
calls.push([content])
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
expect(await saveSessionExport("session.json", { id: "ses_1" }, platform)).toBe(false)
|
||||
expect(calls).toEqual([['{\n "id": "ses_1"\n}']])
|
||||
})
|
||||
|
||||
test("passes serialized data to the native save operation", async () => {
|
||||
const writes: string[][] = []
|
||||
const platform: Pick<Platform, "saveFile"> = {
|
||||
saveFile: async (options, content) => {
|
||||
writes.push([options.defaultPath ?? "", content])
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
expect(await saveSessionExport("session.json", { id: "ses_1" }, platform)).toBe(true)
|
||||
expect(writes).toEqual([["session.json", '{\n "id": "ses_1"\n}']])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
import type { ServerApi } from "@/runtime/server/api"
|
||||
import type { Platform } from "@/runtime/platform/platform"
|
||||
|
||||
export type SessionExportData = {
|
||||
info: SessionInfo
|
||||
@@ -54,15 +53,3 @@ export function downloadSessionExport(filename: string, data: unknown) {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export async function saveSessionExport(
|
||||
filename: string,
|
||||
data: unknown,
|
||||
platform: Pick<Platform, "saveFile">,
|
||||
) {
|
||||
if (!platform.saveFile) {
|
||||
downloadSessionExport(filename, data)
|
||||
return true
|
||||
}
|
||||
return platform.saveFile({ defaultPath: filename }, JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useServerSDK } from "@/runtime/server/client"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { useTerminal } from "@/session/terminal/context"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { fetchSessionExport, saveSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import type { SessionModel } from "@/session/model"
|
||||
import type { SessionRevert } from "@/session/revert"
|
||||
@@ -104,7 +104,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
api: serverSDK.api,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
if (!(await saveSessionExport(filename, data, platform))) return
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -156,10 +156,10 @@ function SessionQueueRow(props: { queue: SessionQueueView; id: string; index: nu
|
||||
data-action="session-queue-steer"
|
||||
type="button"
|
||||
size="small"
|
||||
variant="ghost-faint"
|
||||
variant="ghost-muted"
|
||||
icon="arrow-up"
|
||||
disabled={props.queue.busy()}
|
||||
class="![font-weight:530]"
|
||||
class="text-v2-text-text-muted ![font-weight:530]"
|
||||
onClick={() => void props.queue.steer(props.id)}
|
||||
>
|
||||
{props.queue.working() ? language.t("session.queue.steer") : language.t("session.queue.send")}
|
||||
|
||||
@@ -12,9 +12,8 @@ import { Markdown } from "@opencode-ai/session-ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { fetchSessionExport, saveSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useProviders } from "@/providers/catalog/providers"
|
||||
import { useWorkspaceLocation } from "@/workspaces/location"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
@@ -85,7 +84,6 @@ const emptyMessages: SessionMessageInfo[] = []
|
||||
export function SessionContextTab() {
|
||||
const data = useData()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const sdk = useWorkspaceLocation()
|
||||
const serverSDK = useServerSDK()
|
||||
const providers = useProviders(() => sdk().directory)
|
||||
@@ -201,7 +199,7 @@ export function SessionContextTab() {
|
||||
api: serverSDK.api,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
if (!(await saveSessionExport(filename, data, platform))) return
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -15,9 +15,8 @@ import { removedSessionIDs } from "@/session/session-domain"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { sessionHref } from "@/shell/routes/session"
|
||||
import { sessionTitle } from "@/session/title"
|
||||
import { fetchSessionExport, saveSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/session/commands/export"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { applyTimelineMessageHandoff, timelineChildTitle, visibleTimelineMessages } from "./controller-projection"
|
||||
import { createTimelineProjection } from "./projection"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
@@ -56,7 +55,6 @@ export function createTimelineController(input: { session: TimelineSessionSource
|
||||
const tabs = useTabs()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const handedOffMessages = createMemo(() =>
|
||||
applyTimelineMessageHandoff(
|
||||
input.session.history.messages(),
|
||||
@@ -172,7 +170,7 @@ export function createTimelineController(input: { session: TimelineSessionSource
|
||||
try {
|
||||
const data = await fetchSessionExport({ sessionID: id, api: serverSDK.api })
|
||||
const filename = sessionExportFilename(data.info)
|
||||
if (!(await saveSessionExport(filename, data, platform))) return
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createEffect, createMemo, createSignal, For, on, onCleanup, Show, type
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createAnimatedPresence } from "@/runtime/animated-presence"
|
||||
import type { SessionUserActions } from "@opencode-ai/session-ui/actions"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Badge } from "@opencode-ai/ui/badge"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
@@ -47,42 +47,32 @@ type SessionBackground = {
|
||||
move: () => Promise<void>
|
||||
}
|
||||
|
||||
export function BackgroundMoveHint(props: { keybind?: string[]; onMove?: () => void }) {
|
||||
export function BackgroundMoveHint(props: { keybind?: string[] }) {
|
||||
const language = useLanguage()
|
||||
const command = useCommand()
|
||||
const marker = "__OPENCODE_BACKGROUND_KEYBIND__"
|
||||
const parts = createMemo(() => language.t("session.background.moveInline", { keybind: marker }).split(marker))
|
||||
const keys = () => props.keybind ?? command.keybindParts("session.background")
|
||||
const keybind = () => props.keybind?.join("+") ?? command.keybind("session.background")
|
||||
|
||||
return (
|
||||
<Button
|
||||
<div
|
||||
data-component="session-background-hint"
|
||||
type="button"
|
||||
variant="ghost-faint"
|
||||
size="small"
|
||||
icon="outline-arrow-to-corner-top-right"
|
||||
class="max-w-full"
|
||||
class="flex h-6 max-w-full items-center justify-center gap-[3px] overflow-hidden text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted"
|
||||
aria-label={language.t("session.background.moveInline", { keybind: keybind() })}
|
||||
onClick={() => props.onMove?.()}
|
||||
>
|
||||
<span class="min-w-0 truncate">{language.t("session.background.moveRunning")}</span>
|
||||
<span data-slot="session-background-hint-prefix" class="shrink-0">
|
||||
{parts()[0].trim()}
|
||||
</span>
|
||||
<Keybind keys={keys()} variant="neutral" />
|
||||
</Button>
|
||||
<span class="min-w-0 truncate">{parts()[1].trim()}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function BackgroundWorkSummary(props: { tasks: BackgroundTask[]; mobile?: boolean }) {
|
||||
const language = useLanguage()
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [triggerRef, setTriggerRef] = createSignal<HTMLButtonElement>()
|
||||
const tasks = createMemo<BackgroundTask[]>((previous = []) => (props.tasks.length > 0 ? props.tasks : previous))
|
||||
const presence = createAnimatedPresence(
|
||||
() => (props.tasks.length > 0 ? true : undefined),
|
||||
() => triggerRef() ?? null,
|
||||
)
|
||||
createEffect(() => {
|
||||
if (props.tasks.length > 0) return
|
||||
setOpen(false)
|
||||
})
|
||||
const taskType = (task: BackgroundTask) => {
|
||||
if (task.type === "shell") return language.t("ui.tool.shell")
|
||||
if (!task.agent) return language.t("ui.tool.agent.default")
|
||||
@@ -94,38 +84,31 @@ export function BackgroundWorkSummary(props: { tasks: BackgroundTask[]; mobile?:
|
||||
open={open()}
|
||||
placement={props.mobile ? "top-end" : language.direction() === "rtl" ? "right-end" : "left-end"}
|
||||
gutter={4}
|
||||
onOpenChange={(value) => setOpen(value && props.tasks.length > 0)}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Show when={presence.present()}>
|
||||
<Popover.Trigger
|
||||
ref={setTriggerRef}
|
||||
as="button"
|
||||
type="button"
|
||||
data-component="session-background-summary"
|
||||
class="flex h-7 w-full items-center gap-2 rounded-[4px] px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed duration-150 motion-reduce:animate-none"
|
||||
classList={{
|
||||
"animate-out fade-out fill-mode-forwards": presence.animate() && !presence.show(),
|
||||
}}
|
||||
aria-label={language.plural("session.background.tasksRunning", tasks().length)}
|
||||
>
|
||||
<Icon
|
||||
name="outline-arrow-to-corner-top-right"
|
||||
class="shrink-0 text-v2-icon-icon-muted"
|
||||
/>
|
||||
<TextShimmer
|
||||
as="span"
|
||||
text={language.plural("session.background.tasksRunning", tasks().length)}
|
||||
active
|
||||
class="min-w-0 flex-1 truncate text-start"
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
</Show>
|
||||
<Popover.Trigger
|
||||
as="button"
|
||||
type="button"
|
||||
data-component="session-background-summary"
|
||||
class="flex h-7 w-full items-center gap-2 rounded-[4px] px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed"
|
||||
aria-label={language.plural("session.background.runningCount", props.tasks.length)}
|
||||
>
|
||||
<Badge class="!w-4 !px-0 !border-v2-border-border-strong !bg-v2-background-bg-layer-03">
|
||||
{props.tasks.length}
|
||||
</Badge>
|
||||
<TextShimmer
|
||||
as="span"
|
||||
text={language.t("session.background.running")}
|
||||
active
|
||||
class="min-w-0 flex-1 truncate text-start"
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
data-component="session-background-list"
|
||||
class="z-[60] w-[200px] overflow-hidden rounded-[6px] bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] outline-none data-[closed]:animate-out data-[closed]:fade-out data-[closed]:duration-150 motion-reduce:data-[closed]:animate-none"
|
||||
class="z-[60] w-[200px] overflow-hidden rounded-[6px] bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] outline-none"
|
||||
>
|
||||
<For each={tasks().slice(0, 10)}>
|
||||
<For each={props.tasks.slice(0, 10)}>
|
||||
{(task) => (
|
||||
<div
|
||||
data-component="session-background-list-item"
|
||||
@@ -270,7 +253,7 @@ export function SessionSummaryPanel(props: {
|
||||
when={props.branch}
|
||||
fallback={
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
<span class="shrink-0 whitespace-nowrap">{language.t("session.summary.noBranch")}</span>
|
||||
<span>{language.t("session.summary.noBranch")}</span>
|
||||
<Show when={props.baseBranch}>
|
||||
{(base) => (
|
||||
<>
|
||||
@@ -305,17 +288,9 @@ export function SessionSummaryPanel(props: {
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
<div
|
||||
class="grid transition-[grid-template-rows] duration-150 ease-out motion-reduce:transition-none"
|
||||
classList={{
|
||||
"grid-rows-[1fr]": props.backgroundTasks.length > 0,
|
||||
"grid-rows-[0fr]": props.backgroundTasks.length === 0,
|
||||
}}
|
||||
>
|
||||
<div class="min-h-0 overflow-hidden">
|
||||
<BackgroundWorkSummary tasks={props.backgroundTasks} mobile={props.mobile} />
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.backgroundTasks.length > 0}>
|
||||
<BackgroundWorkSummary tasks={props.backgroundTasks} mobile={props.mobile} />
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.local && props.diffs && props.diffs.length > 0 && props.moveEligible}>
|
||||
<WorkspaceMoveAction
|
||||
@@ -622,38 +597,44 @@ function MessageTimelineView(
|
||||
<VirtualizedTimeline
|
||||
workspaceSession={workspaceSession}
|
||||
bottomSpacer={
|
||||
<Show when={showWorking() || backgroundHintPresence.present()}>
|
||||
<div
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-[1000px] md:mx-auto": props.centered,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Show when={showWorking()}>
|
||||
<div
|
||||
class={`flex h-9 items-center gap-2 pt-3 text-[13px] font-[530] leading-text-compact ${turnPadding()}`}
|
||||
data-component="session-working"
|
||||
role="status"
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-[1000px] md:mx-auto": props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={showWorking()}>
|
||||
<div data-component="session-working" role="status">
|
||||
<TextShimmer text={language.t("session.timeline.working")} active />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={backgroundHintPresence.present()}>
|
||||
<div
|
||||
ref={setBackgroundHintRef}
|
||||
data-component="session-background-hint-row"
|
||||
class="duration-150 motion-reduce:animate-none"
|
||||
classList={{
|
||||
"animate-in fade-in": backgroundHintPresence.animate() && backgroundHintPresence.show(),
|
||||
"animate-out fade-out fill-mode-forwards":
|
||||
backgroundHintPresence.animate() && !backgroundHintPresence.show(),
|
||||
}}
|
||||
>
|
||||
<BackgroundMoveHint onMove={props.background.move} />
|
||||
</div>
|
||||
</Show>
|
||||
<div class={`flex h-9 items-start pt-3 text-[13px] font-[530] leading-text-compact ${turnPadding()}`}>
|
||||
<TextShimmer text={language.t("session.timeline.working")} active />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={backgroundHintPresence.present()}>
|
||||
<div
|
||||
data-component="session-background-hint-row"
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-[1000px] md:mx-auto": props.centered,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={setBackgroundHintRef}
|
||||
class="duration-150 motion-reduce:animate-none"
|
||||
classList={{
|
||||
[`flex items-start ${showWorking() ? "h-6" : "h-9 pt-3"} ${turnPadding()}`]: true,
|
||||
"animate-in fade-in": backgroundHintPresence.animate() && backgroundHintPresence.show(),
|
||||
"animate-out fade-out fill-mode-forwards":
|
||||
backgroundHintPresence.animate() && !backgroundHintPresence.show(),
|
||||
}}
|
||||
>
|
||||
<BackgroundMoveHint />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
deferred={(row) => {
|
||||
if (row._tag !== "AssistantPart" || row.group.type !== "part") return false
|
||||
@@ -757,7 +738,7 @@ function MessageTimelineView(
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={!parentID() && sessionID()} keyed>
|
||||
<Show when={sessionID()} keyed>
|
||||
{(id) => (
|
||||
<Menu
|
||||
gutter={6}
|
||||
|
||||
@@ -70,13 +70,12 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
|
||||
const start = store.startup.trim()
|
||||
|
||||
if (props.project.id && props.project.id !== "global") {
|
||||
const project = await serverCtx().sdk.api.project.update({
|
||||
await serverCtx().sdk.api.project.update({
|
||||
projectID: props.project.id,
|
||||
name,
|
||||
icon: { color: store.color ?? "", override: store.iconOverride ?? "" },
|
||||
commands: { start },
|
||||
})
|
||||
serverCtx().sync.project.update(project)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,10 +122,6 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] .command-palette-row:is([data-active], :focus-visible) {
|
||||
background: var(--v2-alpha-light-10);
|
||||
}
|
||||
|
||||
.command-palette-row-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
||||
@@ -434,7 +434,7 @@ export function Titlebar(props: {
|
||||
}}
|
||||
>
|
||||
<Show when={!mobile() && (!props.verticalTabs || windows())}>
|
||||
<ChannelIndicator debugTools={props.debugTools} />
|
||||
<ChannelIndicator debugTools={props.debugTools} height={windows() ? minHeight() : undefined} />
|
||||
</Show>
|
||||
<Show when={windows() || linux()}>
|
||||
<WindowsAppMenu command={command} platform={platform} />
|
||||
@@ -753,23 +753,18 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () => void } }) {
|
||||
function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () => void }; height?: string }) {
|
||||
const platform = usePlatform()
|
||||
const windows = () => platform.platform === "desktop" && platform.os === "windows"
|
||||
const classes = () => ({
|
||||
"px-2 rounded-sm": windows(),
|
||||
"inline-flex h-4 shrink-0 items-center leading-4 px-1.5 rounded-full": !windows(),
|
||||
})
|
||||
const style = () => ({
|
||||
"font-size": windows() ? undefined : platform.platform === "desktop" && platform.os === "macos" ? "9px" : "10px",
|
||||
height: props.height,
|
||||
"font-size": platform.platform === "desktop" && platform.os === "macos" ? "9px" : "10px",
|
||||
})
|
||||
const channel = import.meta.env.VITE_OPENCODE_CHANNEL
|
||||
if (channel === "dev" && props.debugTools) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="bg-icon-interactive-base text-[#FFF] font-medium uppercase font-mono cursor-pointer [app-region:no-drag]"
|
||||
classList={classes()}
|
||||
class="inline-flex h-4 shrink-0 items-center bg-icon-interactive-base text-[#FFF] leading-4 font-medium px-1.5 rounded-full uppercase font-mono cursor-pointer [app-region:no-drag]"
|
||||
style={style()}
|
||||
onClick={props.debugTools.toggle}
|
||||
aria-label="Toggle debug tools"
|
||||
@@ -785,8 +780,7 @@ function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () =
|
||||
<Show when={label}>
|
||||
{(value) => (
|
||||
<div
|
||||
class="bg-icon-interactive-base text-[#FFF] font-medium uppercase font-mono"
|
||||
classList={classes()}
|
||||
class="inline-flex h-4 shrink-0 items-center bg-icon-interactive-base text-[#FFF] leading-4 font-medium px-1.5 rounded-full uppercase font-mono"
|
||||
style={style()}
|
||||
>
|
||||
{value()}
|
||||
|
||||
@@ -1971,7 +1971,6 @@ export type ConfigEntry =
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string | { providerID: string; model: string; variant?: string }
|
||||
subagent?: boolean
|
||||
subtask?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user