Compare commits

..
Author SHA1 Message Date
Aiden Cline 00daa5ce52 fix(client): isolate shared event consumers 2026-08-30 22:29:21 -05:00
372 changed files with 3066 additions and 9121 deletions
-15
View File
@@ -54,7 +54,6 @@
"name": "@opencode-ai/app",
"version": "1.18.15",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
"@dnd-kit/helpers": "0.5.0",
@@ -1643,10 +1642,6 @@
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251008.0", "", {}, "sha512-dZLkO4PbCL0qcCSKzuW7KE4GYe49lI12LCfQ5y9XeSwgYBoAUbwH4gmJ6A0qUIURiTJTkGkRkhVPqpq2XNgYRA=="],
"@corvu/dialog": ["@corvu/dialog@0.2.4", "", { "dependencies": { "@corvu/utils": "~0.4.2", "solid-dismissible": "~0.1.1", "solid-focus-trap": "~0.1.8", "solid-presence": "~0.2.0", "solid-prevent-scroll": "~0.1.10" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-n54vJq+fOy8GVrnYBdJpD6JXNuyx7LOeMrRxwzAvZnYGpW8+AA12tnb/P/2emJj/HjOO5otheGKb0breshdFlA=="],
"@corvu/drawer": ["@corvu/drawer@0.2.4", "", { "dependencies": { "@corvu/dialog": "~0.2.4", "@corvu/utils": "~0.4.2", "@solid-primitives/memo": "^1.4.1", "solid-transition-size": "~0.1.4" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-7jQoGZ8ROB9CmXam2nMY2wEskU3IoFwZQywkF/7vrBc/edGsPv7mOVQ1GN6G+4nd7nrMZ3UtqHAhmRh9V0azlw=="],
"@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="],
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
@@ -2881,8 +2876,6 @@
"@solid-primitives/media": ["@solid-primitives/media@2.3.6", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.6", "@solid-primitives/rootless": "^1.5.4", "@solid-primitives/static-store": "^0.1.4", "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-pk49gPOq/UMRUJ+pTSrOfBiR8xJjRYHXIf1iR/jSnyQ/KroU+ZXhkZzavC7hvfp2vJeOTW5k2/HN0r3Q1VJ7Pw=="],
"@solid-primitives/memo": ["@solid-primitives/memo@1.5.1", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-VDPrkl9epp0tbby9MvsqphGFCYCtDRC5J8FKzTqHbQiG5hhR8n6xv4MfjhTW231IaBxxPHLxS43EE8c5Q23mSQ=="],
"@solid-primitives/props": ["@solid-primitives/props@3.2.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-MXXdvi2TSB6d+0N6ueA/HP1j/Kh9SEc4WdF1ZDMLwPagxW6pIHHSS9xbRO0RjqSVpNP4j0nxCWT1hx3CkJNhNA=="],
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-bLjwIs6ZPu8NQnuw04sU3Zc8qKSpbc0umUU/O4SHf6oWOdO4+dHY8vb1T7C4b/Tg103+9WGr6sEE0+NFlbaB/A=="],
@@ -5309,10 +5302,6 @@
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"solid-dismissible": ["solid-dismissible@0.1.1", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-9kcKBJIMdS+586cA1g63HYWxKh3h89leeNHbPZ1csYjuni+NvPBtNr11l0iEX2AKKEt6FHk6qNhc/gjoYAW1pA=="],
"solid-focus-trap": ["solid-focus-trap@0.1.9", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-LTyNki6GUJPRLXV5uMWPkYClB07SUMubbr2EkAddiR0CJCF/I283txilMU9RURSr/P8EewMfXWu2o3aWrK7A5A=="],
"solid-js": ["solid-js@1.9.15", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg=="],
"solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="],
@@ -5327,8 +5316,6 @@
"solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
"solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="],
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
"sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="],
@@ -6643,8 +6630,6 @@
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"solid-transition-size/@corvu/utils": ["@corvu/utils@0.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.7" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ZWlyWEE8qV9+CB9OAyo2bTrZGXQN9ZeM+JfYv89zoR+lRACKTDuoOZEdiyL8Uc7U5dUSH1uTqKhTTnaHWb+wZA=="],
"sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-JStMvgtXBA5GrhyBJ5FtdqD8LWkcaPA9NXef+c2xUzw=",
"aarch64-linux": "sha256-WQxF+yS0ZImW0KW620XnZUBsKAJDoJ1jLDOMBaXI2/E=",
"aarch64-darwin": "sha256-km7G6s45dfFdW3Z6lFrs4NohD+vmwN1vR8PmEL0WCto=",
"x86_64-darwin": "sha256-YFbkcHpuspgTp+B+th3IJ3cnu5C2gEUSiy3NDXpD8UA="
"x86_64-linux": "sha256-fG6VYtNC0pce4VM9po7vVucPuJul42yuuijTjNSr7rk=",
"aarch64-linux": "sha256-3TznrmNqdt25cOxia6vcdi/5qKaeyLPIsNXGYBSJNrs=",
"aarch64-darwin": "sha256-8Kmagb5tfECSWZNsIJgrRP1d3X5tuEoWLEWkV3UENZo=",
"x86_64-darwin": "sha256-mIV+mDwIGD02BNYZVi37sY4ls1T01N6z76eBtH0sKiA="
}
}
+20 -16
View File
@@ -510,10 +510,9 @@ interface ParserState {
readonly tools: ToolStream.State<number>
readonly finishedTools: ReadonlySet<number>
// Bedrock splits the finish into `messageStop` (carries `stopReason`) and
// `metadata` (carries usage). Hold both in state so `onHalt` can emit exactly
// one finish after both chunks have had a chance to arrive.
readonly finishReason: FinishReasonDetails | undefined
readonly usage: Usage | undefined
// `metadata` (carries usage). Hold the terminal event in state so `onHalt`
// can emit exactly one finish after both chunks have had a chance to arrive.
readonly pendingFinish: { readonly reason: FinishReasonDetails; readonly usage?: Usage } | undefined
readonly hasToolCalls: boolean
readonly lifecycle: Lifecycle.State
readonly reasoningSignatures: Readonly<Record<number, string>>
@@ -693,9 +692,12 @@ const step = (state: ParserState, event: BedrockEvent) =>
return [
{
...state,
finishReason: {
normalized: mapFinishReason(event.messageStop.stopReason),
raw: event.messageStop.stopReason,
pendingFinish: {
reason: {
normalized: mapFinishReason(event.messageStop.stopReason),
raw: event.messageStop.stopReason,
},
usage: state.pendingFinish?.usage,
},
},
[],
@@ -703,11 +705,14 @@ const step = (state: ParserState, event: BedrockEvent) =>
}
if (event.metadata) {
const usage = mapUsage(event.metadata.usage, state.providerMetadataKey) ?? state.usage
const usage = mapUsage(event.metadata.usage, state.providerMetadataKey) ?? state.pendingFinish?.usage
return [
{
...state,
usage,
pendingFinish: {
reason: state.pendingFinish?.reason ?? { normalized: "stop" },
usage,
},
},
[],
] as const
@@ -731,18 +736,18 @@ const step = (state: ParserState, event: BedrockEvent) =>
const framing = BedrockEventStream.framing(ADAPTER)
const onHalt = (state: ParserState): ReadonlyArray<LLMEvent> => {
if (!state.finishReason) return []
if (!state.pendingFinish) return []
const normalized = (() => {
if (state.finishReason.normalized === "stop" && state.hasToolCalls) return "tool-calls"
return state.finishReason.normalized
if (state.pendingFinish.reason.normalized === "stop" && state.hasToolCalls) return "tool-calls"
return state.pendingFinish.reason.normalized
})()
const events: LLMEvent[] = []
Lifecycle.finish(state.lifecycle, events, {
reason: {
...state.finishReason,
...state.pendingFinish.reason,
normalized,
},
usage: state.usage,
usage: state.pendingFinish.usage,
})
return events
}
@@ -766,8 +771,7 @@ export const protocol = Protocol.make({
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
tools: ToolStream.empty<number>(),
finishedTools: new Set<number>(),
finishReason: undefined,
usage: undefined,
pendingFinish: undefined,
hasToolCalls: false,
lifecycle: Lifecycle.initial(),
reasoningSignatures: {},
+2 -11
View File
@@ -609,27 +609,18 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
}
const step = (state: ParserState, event: GeminiEvent) => {
if (ProviderShared.isRecord(event.error)) {
if (ProviderShared.isRecord(event.error) && typeof event.error.message === "string") {
const body = ProviderShared.encodeJson(event)
return Effect.fail(
new AIError({
reason: classifyProviderFailure({
message:
typeof event.error.message === "string" && event.error.message.length > 0
? event.error.message
: typeof event.error.status === "string" && event.error.status.length > 0
? event.error.status
: "Gemini provider error",
message: event.error.message,
status: typeof event.error.code === "number" ? event.error.code : undefined,
rawBody: body,
}),
}),
)
}
if ("error" in event)
return Effect.fail(
ProviderShared.eventError(state.route, `Invalid ${state.route} stream event`, ProviderShared.encodeJson(event)),
)
const nextState = {
...state,
promptFeedback: event.promptFeedback ?? state.promptFeedback,
@@ -1,4 +1,4 @@
import { Effect, Encoding, Schema } from "effect"
import { Effect, Schema } from "effect"
import type { MediaPart } from "../../schema/index.js"
import { ProviderShared } from "../shared.js"
@@ -57,16 +57,6 @@ const documentBlock = (name: string, format: DocumentFormat, bytes: string): Doc
},
})
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) {
const media = ProviderShared.normalizeMedia(part)
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(
Effect.mapError((cause) =>
ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause),
),
)
return Encoding.encodeBase64(bytes)
})
// Route by MIME. Known image/document formats lower into a typed block; anything
// else fails with a clear error instead of silently degrading to a malformed
// document block. Image MIME types not in `IMAGE_FORMATS` (e.g. `image/svg+xml`)
@@ -76,7 +66,8 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
const mime = part.mediaType.toLowerCase()
const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS]
if (imageFormat) {
return { image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } } satisfies ImageBlock
const media = ProviderShared.normalizeMedia(part)
return { image: { format: imageFormat, source: { bytes: media.base64 } } } satisfies ImageBlock
}
if (mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`)
@@ -84,7 +75,8 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
if (documentFormat) {
if (!part.filename)
return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename")
return documentBlock(part.filename, documentFormat, yield* mediaBase64(part))
const media = ProviderShared.normalizeMedia(part)
return documentBlock(part.filename, documentFormat, media.base64)
}
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`)
})
+6 -1
View File
@@ -9,6 +9,7 @@ import {
LanguageModelSchema,
ProviderOptions,
} from "./options.js"
import { isRecord } from "../utils/record.js"
export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
export type MessageRole = Schema.Schema.Type<typeof MessageRole>
@@ -55,6 +56,11 @@ export const MediaPart = Schema.Struct({
}).annotate({ identifier: "LLM.Content.Media" })
export type MediaPart = Schema.Schema.Type<typeof MediaPart>
const isToolResultValue = (value: unknown): value is ToolResultValue =>
isRecord(value) &&
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
"value" in value
const toolResultValueSchema = Schema.Union([
Schema.Struct({
type: Schema.Literal("json"),
@@ -74,7 +80,6 @@ const toolResultValueSchema = Schema.Union([
}),
]).annotate({ identifier: "LLM.ToolResult" })
export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
const isToolResultValue = Schema.is(toolResultValueSchema)
export const ToolResultValue = Object.assign(toolResultValueSchema, {
is: isToolResultValue,
+1 -1
View File
@@ -267,7 +267,7 @@ export const CachePolicyObject = Schema.Struct({
Schema.Union([
Schema.Literal("latest-user-message"),
Schema.Literal("latest-assistant"),
Schema.Struct({ tail: Schema.Natural }),
Schema.Struct({ tail: Schema.Number }),
]),
),
ttlSeconds: Schema.optional(Schema.Number),
@@ -631,45 +631,10 @@ describe("Bedrock Converse route", () => {
)
const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)))
expect(response.events.filter((event) => event.type === "finish")).toHaveLength(1)
expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 2, totalTokens: 7 })
}),
)
it.effect("retains metadata usage that arrives before messageStop", () =>
Effect.gen(function* () {
const body = eventStreamBody(
["metadata", { usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } }],
["messageStop", { stopReason: "end_turn" }],
)
const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)))
expect(response.events.filter((event) => event.type === "finish")).toHaveLength(1)
expect(response.finishReason).toEqual({ normalized: "stop", raw: "end_turn" })
expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 2, totalTokens: 7 })
}),
)
it.effect("rejects metadata-only streams as incomplete with HTTP context", () =>
Effect.gen(function* () {
const error = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(
fixedBytes(eventStreamBody(["metadata", { usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } }])),
),
Effect.flip,
)
expect(error.reason).toMatchObject({
_tag: "InvalidProviderOutput",
classification: "incomplete-stream",
http: {
status: 200,
headers: { "content-type": "application/vnd.amazon.eventstream" },
},
})
}),
)
it.effect("assembles streamed tool call input", () =>
Effect.gen(function* () {
const body = eventStreamBody(
@@ -1464,20 +1429,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects image media that is not valid base64", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
model,
messages: [Message.user({ type: "media", mediaType: "image/png", data: "https://example.test/image.png" })],
}),
).pipe(Effect.flip)
expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } })
expect(error.message).toContain("Bedrock Converse media data must be valid base64")
}),
)
it.effect("lowers document media into Bedrock document blocks with format and name", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -1601,37 +1552,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects remote media URLs in tool results", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]),
Message.tool({
id: "call_1",
name: "read",
result: {
type: "content",
value: [
{
type: "file",
uri: "https://example.test/report.pdf",
mime: "application/pdf",
name: "report.pdf",
},
],
},
}),
],
}),
).pipe(Effect.flip)
expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } })
expect(error.message).toContain("Bedrock Converse media data must be valid base64")
}),
)
it.effect("rejects unsupported image media types", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
@@ -62,62 +62,6 @@ describe("provider error retention", () => {
)
}
it.effect("classifies a message-less Gemini 429 and retains its event and HTTP context", () =>
Effect.gen(function* () {
const body = JSON.stringify({
error: { code: 429, status: "RESOURCE_EXHAUSTED", details: { opaque: [1, 2] } },
trace: { opaque: "outer" },
})
const error = yield* LLMClient.generate(
LLM.request({ model: Google.configure(options).model("gemini"), prompt: "hello" }),
).pipe(
Effect.provide(
fixedResponse(sseEvents(body), {
headers: { "content-type": "text/event-stream", "x-provider-trace": "trace-1" },
}),
),
Effect.flip,
)
expect(error.message).toBe("RESOURCE_EXHAUSTED")
expect(error.reason._tag).toBe("RateLimit")
expect(error.reason.body).toBe(body)
expect(error.reason.http).toMatchObject({ status: 200, headers: { "x-provider-trace": "trace-1" } })
expect(error.reason.http?.url).toStartWith("https://provider.test/")
}),
)
it.effect("rejects a malformed non-record Gemini error", () =>
Effect.gen(function* () {
const body = JSON.stringify({ error: "RESOURCE_EXHAUSTED", trace: { opaque: "outer" } })
const error = yield* LLMClient.generate(
LLM.request({ model: Google.configure(options).model("gemini"), prompt: "hello" }),
).pipe(Effect.provide(fixedResponse(sseEvents(body))), Effect.flip)
expect(error.reason._tag).toBe("InvalidProviderOutput")
expect(error.message).toContain("Invalid google/gemini stream event")
expect(error.reason.body).toBe(body)
expect(error.reason.http?.status).toBe(200)
}),
)
it.effect("rejects and retains an explicit null Gemini error", () =>
Effect.gen(function* () {
const body = JSON.stringify({ error: null, trace: { opaque: "outer" } })
const error = yield* LLMClient.generate(
LLM.request({ model: Google.configure(options).model("gemini"), prompt: "hello" }),
).pipe(
Effect.provide(fixedResponse(sseEvents(body), { headers: { "x-provider-trace": "trace-null" } })),
Effect.flip,
)
expect(error.reason._tag).toBe("InvalidProviderOutput")
expect(error.reason.body).toBe(body)
expect(error.reason.http).toMatchObject({ status: 200, headers: { "x-provider-trace": "trace-null" } })
expect(error.reason.http?.url).toStartWith("https://provider.test/")
}),
)
it.effect("retains malformed provider frames and the original decode cause", () =>
Effect.gen(function* () {
const body = '{"type":"error","error":{"message":42,"opaque":{"nested":true}},"trace":"outer"}'
-32
View File
@@ -21,7 +21,6 @@ import {
QuotaExceededError,
RateLimitError,
RouteID,
ToolResultValue,
TransportError,
UnknownProviderError,
Usage,
@@ -84,37 +83,6 @@ describe("llm schema", () => {
})
})
describe("ToolResultValue", () => {
test("uses the canonical schema guard", () => {
const cases: ReadonlyArray<{ readonly value: unknown; readonly expected: boolean }> = [
{ value: { type: "json", value: { ok: true } }, expected: true },
{ value: { type: "text", value: "done" }, expected: true },
{ value: { type: "error", value: "failed" }, expected: true },
{ value: { type: "content", value: [{ type: "text", text: "done" }] }, expected: true },
{ value: { type: "content", value: [{ type: "text" }] }, expected: false },
{ value: { type: "content", value: "done" }, expected: false },
{ value: { type: "json" }, expected: false },
{ value: { type: "unknown", value: "done" }, expected: false },
]
for (const item of cases) {
expect(Schema.is(ToolResultValue)(item.value)).toBe(item.expected)
expect(ToolResultValue.is(item.value)).toBe(item.expected)
}
})
test("accepts canonical results with extra fields", () => {
expect(ToolResultValue.is({ type: "json", value: { ok: true }, metadata: { source: "tool" } })).toBe(true)
expect(
ToolResultValue.is({
type: "content",
value: [{ type: "file", uri: "https://example.test/result.txt", mime: "text/plain", checksum: "abc" }],
metadata: { source: "tool" },
}),
).toBe(true)
})
})
describe("AI.Usage", () => {
test("subtractTokens clamps non-sensical breakdowns to zero", () => {
// Defense against a provider reporting cached_tokens > prompt_tokens or
-27
View File
@@ -442,33 +442,6 @@ describe("LLMClient tools", () => {
}),
)
it.effect("projects malformed tagged dynamic output as opaque JSON", () =>
Effect.gen(function* () {
const malformed = { type: "content", value: [{ type: "text" }] }
const dynamic = Tool.make({
description: "Return caller-defined JSON.",
jsonSchema: { type: "object", properties: {} },
execute: () => Effect.succeed(malformed),
})
const dispatched = yield* ToolRuntime.dispatch(
{ dynamic },
LLMEvent.toolCall({ id: "call_1", name: "dynamic", input: {} }),
)
expect(dispatched.result).toEqual({ type: "json", value: malformed })
expect(dispatched.output).toEqual({ structured: malformed, content: [] })
expect(dispatched.events).toEqual([
LLMEvent.toolResult({
id: "call_1",
name: "dynamic",
result: { type: "json", value: malformed },
output: { structured: malformed, content: [] },
}),
])
}),
)
it.effect("executes tool calls for one step without looping by default", () =>
Effect.gen(function* () {
const layer = scriptedResponses([
@@ -1,34 +1,5 @@
import { expect, story } from "../../storybook/playwright/story"
for (const draft of ["empty-draft", "multiline-draft", "mixed-attachments"]) {
story(`select all stays inside the composer with ${draft}`, async ({ mount, page }) => {
const component = await mount(`opencode-composer-flow--${draft}`)
const input = component.getByRole("textbox", { name: "Prompt", exact: true })
const text = await input.textContent()
for (let count = 0; count < 2; count++) {
await input.press("ControlOrMeta+a")
expect(
await input.evaluate((editor) => {
const selection = window.getSelection()
return {
text: selection?.toString(),
inside: editor.contains(selection?.anchorNode ?? null) && editor.contains(selection?.focusNode ?? null),
}
}),
).toEqual({ text, inside: true })
}
await page.keyboard.type("Replacement draft")
await expect(input).toHaveText("Replacement draft")
await expect(component.getByRole("status")).toHaveText("Ready")
if (draft === "mixed-attachments") {
await expect(component.getByAltText("layout.png")).toBeVisible()
await expect(component.getByText("Keep the normal flow flat", { exact: true })).toBeVisible()
}
})
}
story("renders a draft once and supports editing, caret restoration, and failure recovery", async ({ mount, page }) => {
await page.addInitScript(() => {
const replace = Element.prototype.replaceChildren
@@ -8,30 +8,6 @@ story.beforeEach(async ({ mount }) => {
await expect(component.getByRole("textbox", { name: "Prompt", exact: true })).toBeVisible()
})
story("spaces the first mobile message without changing desktop spacing", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.evaluate(async (fixture) => {
const { mountTimelineVirtualizer } = await import(fixture)
mountTimelineVirtualizer({ count: 1, rowHeight: 60, immediate: true })
}, fixture)
const root = page.getByTestId("timeline-virtualizer-fixture")
await root.getByRole("button", { name: "Complete Markdown", exact: true }).click()
const content = root.locator("[data-timeline-virtual-content]")
await expect(content).toHaveCSS("visibility", "visible")
const gap = () =>
root.locator('[data-timeline-key="user-message:message-0"]').evaluate((element) => {
const viewport = element.closest("[data-scrollable]")!
return element.getBoundingClientRect().top - viewport.getBoundingClientRect().top
})
await expect.poll(gap).toBe(16)
await root.evaluate((element) => element.setAttribute("dir", "rtl"))
await expect.poll(gap).toBe(16)
await page.setViewportSize({ width: 1280, height: 900 })
await expect.poll(gap).toBe(0)
await page.setViewportSize({ width: 390, height: 844 })
await expect.poll(gap).toBe(16)
})
story("bounds the cheap suffix and reveals only ready measured rows", async ({ page }) => {
await page.evaluate(async (fixture) => {
const { mountTimelineVirtualizer } = await import(fixture)
@@ -1,4 +1,4 @@
import { expect, test, type Locator } from "@playwright/test"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
@@ -65,64 +65,11 @@ for (const lines of [6000, 25000]) {
await expect.poll(async () => (await input.innerText()) === text).toBe(true)
expect(await events.evaluate((events) => events.count)).toBe(1)
await expect(input).toBeFocused()
await expectCaretVisible(input)
const scroll = page.locator('[data-component="composer-scroll"]')
await expect(scroll.locator(".scroll-view__viewport")).toHaveCSS("scrollbar-width", "none")
await expect(scroll.locator(".scroll-view__thumb")).toBeVisible()
await page.keyboard.type("!")
await expect.poll(async () => (await input.innerText()) === text + "!").toBe(true)
await expectCaretVisible(input)
const thumb = await scroll.locator(".scroll-view__thumb").boundingBox()
const bounds = await scroll.boundingBox()
if (!thumb || !bounds) throw new Error("Missing composer scrollbar bounds")
await page.mouse.move(thumb.x + thumb.width / 2, thumb.y + thumb.height / 2)
await page.mouse.down()
await page.mouse.move(thumb.x + thumb.width / 2, bounds.y + 8 + thumb.height / 2)
await page.mouse.up()
await expect(scroll.locator(".scroll-view__viewport")).toHaveJSProperty("scrollTop", 0)
await expect(input).toBeFocused()
await page.keyboard.press("ControlOrMeta+Home")
await page.keyboard.press("ControlOrMeta+End")
await expectCaretVisible(input)
})
}
async function expectCaretVisible(input: Locator) {
await expect
.poll(() =>
input.evaluate((element) => {
const selection = window.getSelection()
if (!selection?.isCollapsed || !selection.rangeCount || !element.contains(selection.anchorNode)) return false
const caret = selection.getRangeAt(0).getBoundingClientRect()
const viewport = (element.closest("[data-scrollable]") ?? element).getBoundingClientRect()
return caret.height > 0 && caret.top >= viewport.top - 1 && caret.bottom <= viewport.bottom + 1
}),
)
.toBe(true)
}
for (const width of [390, 1280]) {
for (const direction of ["ltr", "rtl"]) {
test(`reveals a multiline paste in the middle at ${width}px in ${direction}`, async ({ page }) => {
await page.setViewportSize({ width, height: 800 })
await page.evaluate((direction) => (document.documentElement.dir = direction), direction)
const input = page.getByRole("textbox", { name: "Prompt", exact: true })
const suffix = "\nExisting trailing content".repeat(100)
await input.fill("Before " + suffix)
await input.press("ControlOrMeta+Home")
await input.press("ArrowRight")
const text = "Pasted line /tmp/example.ts 123 \u0645\u0631\u062d\u0628\u0627\n".repeat(100) + "End of paste"
await page.evaluate((text) => navigator.clipboard.writeText(text), text)
await page.keyboard.press("ControlOrMeta+V")
await expect.poll(() => input.innerText()).toBe("B" + text + "efore " + suffix)
await expectCaretVisible(input)
await page.keyboard.type("!")
await expect.poll(() => input.innerText()).toBe("B" + text + "!efore " + suffix)
await expectCaretVisible(input)
})
}
}
for (const text of [
"single line <b> &amp;",
"first\nsecond",
@@ -1,110 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../performance/timeline/session-timeline-stress.fixture"
import { stressSessionHref } from "../performance/timeline/timeline-test-helpers"
import { mockOpenCodeServer } from "../utils/mock-server"
for (const direction of ["ltr", "rtl"] as const) {
test(`mobile files browse, search, switch, and close shared file tabs in ${direction}`, async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await mockOpenCodeServer(page, {
directory: fixture.directory,
project: fixture.project,
sessions: fixture.sessions,
provider: fixture.provider,
pageMessages,
fileList: (path) =>
path
? []
: ["first.ts", "second.ts"].map((name) => ({
name,
path: name,
absolute: `${fixture.directory}/${name}`,
type: "file",
ignored: false,
})),
fileContent: (path) => `contents:${path}`,
findFiles: ({ query }) => ["first.ts", "second.ts"].filter((path) => path.includes(query)),
})
await page.goto(stressSessionHref(fixture.targetID))
const navigation = page.getByRole("tablist", { name: "Session view", exact: true })
await navigation.getByRole("tab", { name: "Files", exact: true }).click()
const files = page.locator('[data-slot="session-mobile-files"]')
await expect(files.getByRole("button", { name: "first.ts", exact: true })).toBeVisible()
await page.evaluate((direction) => (document.documentElement.dir = direction), direction)
await expect(files.locator('[data-slot="session-mobile-files-header"]')).toHaveCSS("border-bottom-width", "0px")
await expect
.poll(() =>
files.getByRole("tablist", { name: "Open files", exact: true }).evaluate((element) => {
const header = element.closest('[data-slot="session-mobile-files-header"]')!
const separator = getComputedStyle(element, "::before")
return {
height: separator.height,
fullWidth: parseFloat(separator.width) === header.clientWidth,
start: separator.insetInlineStart,
bottom: separator.bottom,
}
}),
)
.toEqual({ height: "1px", fullWidth: true, start: "0px", bottom: "0px" })
await files.getByRole("button", { name: "first.ts", exact: true }).click()
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
await files.locator('[data-column-number="1"]').click()
const editor = files.locator('[data-component="line-comment-v2"][data-variant="editor"]')
await expect(editor.getByRole("textbox")).toBeVisible()
for (const width of [390, 700]) {
await page.setViewportSize({ width, height: 844 })
await expect
.poll(async () => {
const panel = await files.boundingBox()
const comment = await editor.boundingBox()
if (!panel || !comment) return false
return Math.abs(comment.x - panel.x - 12) < 2 && Math.abs(comment.width - panel.width + 24) < 2
})
.toBe(true)
}
await editor.getByRole("textbox").fill("Full-width file comment")
await editor.getByRole("button", { name: "Comment", exact: true }).click()
const comment = files.locator('[data-component="line-comment-v2"][data-variant="display"]')
await expect(comment).toContainText("Full-width file comment")
await expect
.poll(async () => {
const panel = await files.boundingBox()
const card = await comment.boundingBox()
if (!panel || !card) return false
return Math.abs(card.x - panel.x - 12) < 2 && Math.abs(card.width - panel.width + 24) < 2
})
.toBe(true)
await page.setViewportSize({ width: 390, height: 844 })
await expect(files.getByRole("combobox", { name: "Filter files", exact: true })).toBeHidden()
await files.getByRole("button", { name: "All files", exact: true }).click()
await files.getByRole("combobox", { name: "Filter files", exact: true }).fill("second")
await files.getByRole("option", { name: "second.ts", exact: true }).click()
await expect(files.getByText("contents:second.ts", { exact: true })).toBeVisible()
const openTabs = files.getByRole("tablist", { name: "Open files", exact: true })
await expect(openTabs.getByRole("tab")).toHaveText(["first.ts", "second.ts"])
await expect
.poll(() =>
openTabs.getByRole("tab", { name: "second.ts", exact: true }).evaluate((element) => {
const tab = element.closest('[data-slot="tabs-v2-trigger-wrapper"]')!
const header = element.closest('[data-slot="session-mobile-files-header"]')!
return Math.abs(tab.getBoundingClientRect().bottom - header.getBoundingClientRect().bottom) < 1
}),
)
.toBe(true)
await openTabs.getByRole("tab", { name: "first.ts", exact: true }).click()
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
await navigation.getByRole("tab", { name: "Session", exact: true }).click()
await navigation.getByRole("tab", { name: "Files", exact: true }).click()
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
await files
.locator('[data-slot="tabs-v2-trigger-wrapper"]')
.filter({ has: page.getByRole("tab", { name: "first.ts", exact: true }) })
.getByRole("button", { name: "Close tab", exact: true })
.click()
await expect(openTabs.getByRole("tab")).toHaveText(["second.ts"])
await expect(files.getByText("contents:second.ts", { exact: true })).toBeVisible()
await files.getByRole("button", { name: "Close tab", exact: true }).click()
await expect(files.getByRole("combobox", { name: "Filter files", exact: true })).toBeVisible()
await expect(openTabs.getByRole("tab")).toHaveCount(0)
})
}
@@ -1,43 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../performance/timeline/session-timeline-stress.fixture"
import { stressSessionHref } from "../performance/timeline/timeline-test-helpers"
import { mockOpenCodeServer } from "../utils/mock-server"
test("opening changed files selects the requested tab before file loading completes", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
const loading = Promise.withResolvers<void>()
await mockOpenCodeServer(page, {
directory: fixture.directory,
project: fixture.project,
sessions: fixture.sessions,
provider: fixture.provider,
pageMessages,
fileList: () => [],
fileContent: async (path) => {
if (path === "second.ts") await loading.promise
return `contents:${path}`
},
vcsDiff: ["first.ts", "second.ts"].map((file) => ({
file,
status: "modified",
additions: 1,
deletions: 1,
patch: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-before\n+after\n`,
})),
})
await page.goto(stressSessionHref(fixture.targetID))
const navigation = page.getByRole("tablist", { name: "Session view", exact: true })
const files = page.locator('[data-slot="session-mobile-files"]')
for (const file of ["first.ts", "second.ts", "first.ts", "second.ts"]) {
await navigation.getByRole("tab", { name: "Changes", exact: true }).click()
const diff = page.locator(`[data-component="session-review"] [data-file="${file}"]`)
const header = diff.getByRole("button", { name: file, exact: true })
await expect(header).toBeEnabled()
if ((await header.getAttribute("aria-expanded")) === "false") await header.click()
await diff.getByRole("button", { name: "Open file", exact: true }).click()
await expect(navigation.getByRole("tab", { name: "Files", exact: true })).toHaveAttribute("aria-selected", "true")
await expect(files.getByRole("tab", { name: file, exact: true })).toHaveAttribute("aria-selected", "true")
if (file === "second.ts") loading.resolve()
await expect(files.getByText(`contents:${file}`, { exact: true })).toBeVisible()
}
})
@@ -1,138 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture } from "../performance/timeline/session-timeline-stress.fixture"
import { mockStressTimeline, stressSessionHref } from "../performance/timeline/timeline-test-helpers"
for (const direction of ["ltr", "rtl"] as const) {
test(`mobile change summaries appear only for expanded diffs in ${direction}`, async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await mockStressTimeline(page, {
vcsDiff: [
{
file: "added.ts",
status: "added",
additions: 1,
deletions: 0,
patch:
"diff --git a/added.ts b/added.ts\n--- /dev/null\n+++ b/added.ts\n@@ -0,0 +1 @@\n+export const added = 1\n",
},
{
file: "removed.ts",
status: "deleted",
additions: 0,
deletions: 1,
patch:
"diff --git a/removed.ts b/removed.ts\n--- a/removed.ts\n+++ /dev/null\n@@ -1 +0,0 @@\n-export const removed = 1\n",
},
{
file: "modified.ts",
status: "modified",
additions: 1,
deletions: 1,
patch: `diff --git a/modified.ts b/modified.ts\n--- a/modified.ts\n+++ b/modified.ts\n@@ -1 +1 @@\n-export const value = 1\n+export const value = "${"long content ".repeat(30)}"\n`,
},
],
})
await page.goto(stressSessionHref(fixture.targetID))
await page.getByRole("tab", { name: "Changes", exact: true }).click()
const review = page.locator('[data-component="session-review"]')
await expect(review.getByRole("button", { name: "Expand all", exact: true })).toBeVisible()
await page.evaluate((direction) => (document.documentElement.dir = direction), direction)
await expect(review.locator('[data-file="added.ts"] [data-slot="accordion-trigger"]')).toHaveCSS(
"border-top-width",
"0px",
)
await expect(review.locator('[data-file="modified.ts"] [data-slot="accordion-trigger"]')).toHaveCSS(
"border-bottom-width",
"0px",
)
for (const change of [
{ file: "added.ts", status: "Added", additions: "+1", deletions: "-0" },
{ file: "removed.ts", status: "Removed", additions: "+0", deletions: "-1" },
{ file: "modified.ts", status: undefined, additions: "+1", deletions: "-1" },
]) {
const item = review.locator(`[data-file="${change.file}"]`)
const trigger = item.getByRole("button", { name: change.file, exact: true })
const summary = item.locator('[data-slot="session-review-change-summary"]')
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await expect(trigger.locator('[data-component="diff-changes"]')).toHaveCount(0)
await expect(trigger.locator('[data-slot="session-review-change"]')).toHaveCount(0)
await expect(summary).toHaveCount(0)
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
if (change.file === "modified.ts") {
await expect(item.locator('[data-slot="accordion-content"]')).toHaveCSS("border-bottom-width", "0px")
await expect(trigger).not.toHaveCSS("border-bottom-width", "0px")
}
await expect(summary).toBeVisible()
await expect(summary.getByRole("button", { name: "Open file", exact: true })).toBeVisible()
await expect(summary.locator('[data-slot="diff-changes-additions"]')).toHaveText(change.additions)
await expect(summary.locator('[data-slot="diff-changes-deletions"]')).toHaveText(change.deletions)
if (change.status) await expect(summary.getByText(change.status, { exact: true })).toBeVisible()
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await expect(summary).toHaveCount(0)
}
await expect(review.locator('[data-slot="session-review-view-button"]')).toHaveCount(0)
const modified = review.locator('[data-file="modified.ts"]')
await modified.getByRole("button", { name: "modified.ts", exact: true }).click()
await expect(modified.locator("[data-line-number-content]")).toHaveText(["1", "1"])
await expect(modified.locator("[data-diff]")).not.toHaveAttribute("data-disable-line-numbers")
await expect(modified.locator("[data-diff]")).toHaveAttribute("data-overflow", "wrap")
await expect(review.getByRole("button", { name: "Diff options", exact: true })).toHaveCount(0)
await page.keyboard.press("Control+,")
const settings = page.getByTestId("settings-screen")
const wrap = settings.getByRole("switch", { name: "Wrap lines", exact: true })
const wrapControl = settings.locator('[data-action="settings-mobile-diff-wrap"] [data-slot="switch-control"]')
await expect(wrap).toBeChecked()
await wrapControl.click()
await expect(wrap).not.toBeChecked()
await expect
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem("settings.v3") ?? "{}").general?.mobileDiffWrap))
.toBe(false)
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
await expect(modified.locator("[data-diff]")).toHaveAttribute("data-overflow", "scroll")
await expect
.poll(() => modified.locator("[data-code]").evaluate((element) => element.scrollWidth > element.clientWidth))
.toBe(true)
await modified.locator("[data-code]").evaluate((element) => {
element.scrollLeft = 100
})
await expect
.poll(() => modified.locator("[data-code]").evaluate((element) => Math.abs(element.scrollLeft)))
.toBeGreaterThan(0)
const navigation = page.getByRole("tablist", { name: "Session view", exact: true })
await navigation.getByRole("tab", { name: "Session", exact: true }).click()
await navigation.getByRole("tab", { name: "Changes", exact: true }).click()
await expect(modified.locator("[data-diff]")).toHaveAttribute("data-overflow", "scroll")
await page.keyboard.press("Control+,")
await expect(wrap).not.toBeChecked()
await wrapControl.click()
await expect(wrap).toBeChecked()
await expect
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem("settings.v3") ?? "{}").general?.mobileDiffWrap))
.toBe(true)
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
await expect(modified.locator("[data-diff]")).toHaveAttribute("data-overflow", "wrap")
const openFile = modified.getByRole("button", { name: "Open file", exact: true })
await expect(openFile).toBeVisible()
await expect
.poll(async () => {
const button = await openFile.boundingBox()
const summary = await modified.locator('[data-slot="session-review-change-summary"]').boundingBox()
if (!button || !summary) return false
return direction === "ltr"
? button.x > summary.x + summary.width / 2
: button.x + button.width < summary.x + summary.width / 2
})
.toBe(true)
await openFile.click()
await expect(
page.getByRole("tablist", { name: "Session view", exact: true }).getByRole("tab", { name: "Files", exact: true }),
).toHaveAttribute("aria-selected", "true")
const files = page.locator('[data-slot="session-mobile-files"]')
await expect(files.getByRole("tab", { name: "modified.ts", exact: true })).toHaveAttribute("aria-selected", "true")
await expect(files).toHaveAttribute("data-browsing", "false")
})
}
@@ -1,186 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../performance/timeline/session-timeline-stress.fixture"
import { installStressSessionTabs, stressSessionHref } from "../performance/timeline/timeline-test-helpers"
import { mockOpenCodeServer } from "../utils/mock-server"
for (const position of ["top", "bottom"] as const) {
test(`mobile session tabs switch views and keep the terminal cached with ${position} navigation`, async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await mockOpenCodeServer(page, {
directory: fixture.directory,
project: fixture.project,
sessions: fixture.sessions,
provider: fixture.provider,
pageMessages,
fileList: () => [],
})
await installStressSessionTabs(page)
await page.addInitScript(
(position) =>
localStorage.setItem("settings.v3", JSON.stringify({ general: { mobileTitlebarPosition: position } })),
position,
)
await page.route("**/api/pty*", (route) =>
route.fulfill({
json: {
location: { directory: fixture.directory, project: { id: fixture.project.id, directory: fixture.directory } },
data: {
id: "pty_mobile_views",
title: "Terminal 1",
command: "sh",
args: [],
cwd: fixture.directory,
status: "running",
pid: 1,
},
},
}),
)
await page.routeWebSocket("**/api/pty/pty_mobile_views/connect", () => undefined)
await page.route("**/api/pty/pty_mobile_views/connect-token*", (route) =>
route.fulfill({
json: {
location: { directory: fixture.directory, project: { id: fixture.project.id, directory: fixture.directory } },
data: { ticket: "e2e-ticket", expires_in: 60 },
},
}),
)
await page.goto(stressSessionHref(fixture.targetID))
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
const navigation = page.locator('[data-slot="session-mobile-view-navigation"]')
const more = navigation.getByRole("button", { name: "More options", exact: true })
const picker = tabs.getByRole("tab", { selected: true })
const message = page.locator(
`[data-timeline-row="UserMessage"][data-message-id="${fixture.expected.targetMessageIDs.at(-1)}"]`,
)
const composer = page.getByRole("textbox", { name: "Prompt", exact: true })
await expect(picker).toHaveText("Session")
await expect(message).toBeVisible()
await expect(composer).toBeVisible()
await expect(tabs.getByRole("tab")).toHaveText(["Session", "Changes", "Files", "Terminal"])
await expect(tabs).toHaveCSS("padding-left", "0px")
await expect(tabs).toHaveCSS("padding-right", "0px")
await expect
.poll(async () => {
const bounds = await navigation.boundingBox()
return !!bounds && bounds.x >= 8 && bounds.x <= 9 && bounds.width >= 372 && bounds.width <= 374
})
.toBe(true)
await expect
.poll(async () => {
const bar = await tabs.boundingBox()
const input = await composer.boundingBox()
const panel = await page.locator('[data-slot="session-chat-panel"]').boundingBox()
return !!bar && !!input && !!panel && Math.abs(bar.y - panel.y) <= 1 && bar.y + bar.height <= input.y
})
.toBe(true)
await expect(page.locator("[data-session-title]")).toHaveCount(0)
await expect(page.locator('[data-slot="mobile-tabs-trigger"]')).toContainText(fixture.expected.targetTitle)
await page.getByRole("button", { name: "Tabs", exact: true }).click()
const drawer = page.getByRole("dialog", { name: "Tabs", exact: true })
await expect(drawer).toHaveAttribute("data-open", "")
await expect(drawer).not.toHaveAttribute("data-transitioning")
await expect(drawer.getByRole("button", { name: "Settings", exact: true })).toBeInViewport()
await drawer.getByRole("button", { name: "Settings", exact: true }).click()
await expect(page.getByTestId("settings-screen")).toBeVisible()
await page.getByRole("button", { name: "Back to app", exact: true }).click()
await page.getByRole("button", { name: "Tabs", exact: true }).click()
await expect(drawer).not.toHaveAttribute("data-transitioning")
await expect(drawer.getByRole("button", { name: "Settings", exact: true })).toBeInViewport()
await page.keyboard.press("Escape")
await expect(drawer).toBeHidden()
await more.click()
await page.getByRole("menuitem", { name: "Usage", exact: true }).click()
await expect(picker).toHaveCount(0)
await expect(page.getByText("Total Cost", { exact: true })).toBeVisible()
const usage = page.locator('[data-slot="session-usage-content"]')
await expect(usage).toHaveCSS("padding-top", "16px")
await expect(usage).toHaveCSS("padding-inline-start", "16px")
await expect(usage).toHaveCSS("padding-inline-end", "16px")
await expect(composer).toBeHidden()
await more.click()
await page.getByRole("menuitem", { name: "Status", exact: true }).click()
const status = page.getByRole("dialog", { name: "Status", exact: true })
await expect(status.getByRole("tab", { name: "MCP", exact: true })).toBeVisible()
await status.getByRole("tab", { name: "Plugins", exact: true }).click()
await expect(status.getByText("opencode.json", { exact: true })).toBeVisible()
await status.getByRole("button", { name: "Close", exact: true }).click()
await expect(status).toBeHidden()
await expect(more).toBeFocused()
await more.click()
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
const details = page.getByRole("dialog", { name: "Session details", exact: true })
await expect(details.getByText(fixture.project.name, { exact: true })).toBeVisible()
await expect(details.getByRole("button", { name: "No changes", exact: true })).toBeVisible()
await details.getByRole("button", { name: "Close", exact: true }).click()
await expect(details).toBeHidden()
await expect(more).toBeFocused()
await more.click()
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
await expect(details.getByRole("button", { name: "No changes", exact: true })).toBeVisible()
await expect(details).not.toHaveAttribute("data-transitioning")
await page.keyboard.press("Escape")
await expect(details).toBeHidden()
await expect(more).toBeFocused()
await more.click()
await page.getByRole("menuitem", { name: "Session details", exact: true }).click()
await details.getByRole("button", { name: "No changes", exact: true }).click()
await expect(details).toBeHidden()
await expect(picker).toHaveText("Changes")
await expect(page.getByText("No uncommitted changes yet", { exact: true })).toBeVisible()
await expect(page.locator('[data-slot="session-review-header"]')).toHaveCSS("height", "40px")
await expect(page.locator('[data-slot="session-review-header"]')).toHaveCSS("padding-left", "8px")
await expect(composer).toBeHidden()
await tabs.getByRole("tab", { name: "Files", exact: true }).click()
await expect(picker).toHaveText("Files")
await expect(page.getByRole("combobox", { name: "Filter files", exact: true })).toBeVisible()
await expect(composer).toBeHidden()
await tabs.getByRole("tab", { name: "Terminal", exact: true }).click()
const panel = page.locator("#terminal-panel")
await expect(panel).toHaveAttribute("data-opened", "true")
await expect(panel.getByRole("tab", { name: /Terminal 1/ })).toBeVisible()
await expect(panel.locator('[data-component="terminal"]')).toBeVisible()
await expect(panel.locator("textarea")).toBeEditable()
await expect(panel).toHaveCount(1)
await panel.evaluate((element) => element.setAttribute("data-cache-probe", "original"))
await expect(composer).toBeHidden()
await tabs.getByRole("tab", { name: "Session", exact: true }).click()
await expect(message).toBeVisible()
await expect(panel).toBeHidden()
await expect(panel).toHaveAttribute("inert", "")
await expect(panel).toHaveAttribute("data-cache-probe", "original")
await page.keyboard.press("Control+Backquote")
await expect(picker).toHaveText("Terminal")
await expect(panel).toBeVisible()
await expect(panel).toHaveAttribute("data-cache-probe", "original")
await page.keyboard.press("Control+Backquote")
await expect(picker).toHaveText("Session")
await page.keyboard.press("Control+Backquote")
await expect(picker).toHaveText("Terminal")
await panel.getByRole("button", { name: "Close terminal", exact: true }).click()
await expect(picker).toHaveText("Session")
await expect(panel).toBeHidden()
await more.click()
await page.getByRole("menuitem", { name: "Usage", exact: true }).click()
await expect(page.getByText("Total Cost", { exact: true })).toBeVisible()
await page.goto(stressSessionHref(fixture.sourceID))
await expect(picker).toHaveText("Session")
await expect(page.locator('[data-slot="mobile-tabs-trigger"]')).toContainText(fixture.expected.sourceTitle)
await page.setViewportSize({ width: 1280, height: 900 })
await expect(picker).toBeHidden()
await expect(page.locator("[data-session-title]")).toBeVisible()
})
}
@@ -1,92 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true })
test.beforeEach(async ({ page }) => {
await mockOpenCodeServer(page, {
directory: fixture.directory,
project: fixture.project,
provider: fixture.provider,
sessions: fixture.sessions,
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
({ directory, server, sessions }) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] } }),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify(sessions.map((session) => ({ type: "session", server, sessionId: session.id }))),
)
},
{ directory: fixture.directory, server: fixture.serverKey, sessions: fixture.sessions },
)
await page.goto("/")
await expect(page.locator('[data-component="home-session-row"]')).toHaveCount(fixture.sessions.length)
})
test("mobile project selection and drawer navigation preserve session identity", async ({ page }) => {
const projects = page.getByRole("button", { name: "Projects", exact: true })
await projects.click()
const picker = page.getByRole("dialog", { name: "Projects", exact: true })
await expect(picker.getByRole("button", { name: "All projects", exact: true })).toBeVisible()
await picker.getByRole("button", { name: new RegExp(` ${fixture.project.name}$`) }).click()
await expect(picker).toBeHidden()
await expect(projects).toContainText(fixture.project.name)
const trigger = page.locator('[data-slot="mobile-tabs-trigger"]')
const drawer = page.locator('[data-slot="mobile-tabs-drawer"]')
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(drawer.locator('[data-slot="tab-project"]')).toHaveText([fixture.project.name, fixture.project.name])
const settings = drawer.getByRole("button", { name: "Settings", exact: true })
const help = drawer.getByRole("button", { name: "Help", exact: true })
await expect(settings).toBeVisible()
await expect(help).toBeVisible()
expect((await settings.boundingBox())?.width).toBeGreaterThan((await help.boundingBox())?.width ?? 0)
await drawer.locator('[data-slot="tab-link"]').filter({ hasText: fixture.expected.sourceTitle }).click()
await expect(page).toHaveURL(new RegExp(`/session/${fixture.sourceID}$`))
await expect(drawer).toBeHidden()
await expect(trigger).toContainText(fixture.expected.sourceTitle)
await trigger.click()
await drawer.getByRole("button", { name: "Home", exact: true }).click()
await expect(page).toHaveURL("/")
await expect(drawer).toBeHidden()
await expect(page.locator('[data-component="home-session-row"]')).toHaveCount(fixture.sessions.length)
})
test("mobile settings section menu stays above a full-width panel", async ({ page }) => {
await page.locator('[data-slot="mobile-tabs-trigger"]').click()
await page.locator('[data-slot="mobile-tabs-drawer"]').getByRole("button", { name: "Settings", exact: true }).click()
const settings = page.getByTestId("settings-screen")
const menu = settings.getByRole("button", { name: "Preferences", exact: true })
const panel = settings.getByRole("tabpanel")
await expect(settings.getByRole("heading", { name: "General", exact: true })).toBeVisible()
await expect(menu).toBeVisible()
await menu.click()
await expect(page.getByRole("menuitemradio", { name: "Preferences", exact: true })).toBeChecked()
await page.keyboard.press("Escape")
await expect(page.getByRole("menuitemradio", { name: "Preferences", exact: true })).toBeHidden()
await expect(settings).toBeVisible()
await menu.click()
await page.getByRole("menuitemradio", { name: "Models", exact: true }).click()
await expect(settings.getByRole("heading", { name: "Models", exact: true })).toBeVisible()
await expect(settings.getByRole("button", { name: "Models", exact: true })).toBeVisible()
await expect(settings.getByRole("switch", { name: "Claude Opus 4.6", exact: true })).toBeAttached()
await expect
.poll(async () => {
const navigation = await settings.getByRole("button", { name: "Models", exact: true }).boundingBox()
const content = await panel.boundingBox()
return !!navigation && !!content && navigation.y + navigation.height <= content.y
})
.toBe(true)
await expect.poll(async () => (await panel.boundingBox())?.width ?? 0).toBeGreaterThan(350)
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
await expect(settings).toBeHidden()
await expect(page.locator('[data-component="home-session-row"]')).toHaveCount(fixture.sessions.length)
})
@@ -1,37 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture } from "../performance/timeline/session-timeline-stress.fixture"
import { mockStressTimeline, stressSessionHref } from "../performance/timeline/timeline-test-helpers"
test("status drawer dismisses and reopens after button, backdrop, Escape, and drag", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await mockStressTimeline(page)
await page.goto(stressSessionHref(fixture.targetID))
const more = page
.locator('[data-slot="session-mobile-view-navigation"]')
.getByRole("button", { name: "More options", exact: true })
const drawer = page.getByRole("dialog", { name: "Status", exact: true })
const overlay = page.locator('[data-slot="mobile-status-overlay"]')
for (const dismissal of ["button", "backdrop", "escape", "drag", "button"] as const) {
await more.click()
await page.getByRole("menuitem", { name: "Status", exact: true }).click()
await expect(drawer.getByRole("tab", { name: "MCP", exact: true })).toBeVisible()
await expect(drawer).not.toHaveAttribute("data-transitioning")
if (dismissal === "button") await drawer.getByRole("button", { name: "Close", exact: true }).click()
if (dismissal === "backdrop") await overlay.click({ position: { x: 10, y: 10 } })
if (dismissal === "escape") await page.keyboard.press("Escape")
if (dismissal === "drag") {
const handle = drawer.locator('[data-slot="mobile-status-drag-handle"]')
const bounds = await handle.boundingBox()
expect(bounds).not.toBeNull()
await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2)
await page.mouse.down()
await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2 + 1)
await page.mouse.move(bounds!.x + bounds!.width / 2, 843)
await page.mouse.up()
}
await expect(drawer, `dismissal: ${dismissal}`).toBeHidden()
await expect(overlay).toHaveCount(0)
await expect(more).toBeFocused()
}
})
@@ -89,7 +89,7 @@ async function openReview(page: Page) {
)
await changes.click()
expect((await (await diffResponse).json()).data).toHaveLength(1)
await expect(changes).toHaveAttribute("aria-selected", "true")
await expect(page.getByRole("tab", { selected: true })).toHaveAccessibleName(/Files Changed/)
const review = page.locator('[data-component="session-review"]')
await expectAppVisible(review)
@@ -32,13 +32,12 @@ test("server dialog keeps focus above fullscreen settings", async ({ page }) =>
const editor = page.getByRole("dialog", { name: "Add server" })
await expect(editor.getByPlaceholder("http://localhost:4096")).toBeFocused()
await expect(editor.getByPlaceholder("username")).toHaveCount(0)
const name = editor.getByPlaceholder("Localhost", { exact: true })
const username = editor.getByPlaceholder("username")
const password = editor.getByPlaceholder("password")
await name.click()
await expect(name).toBeFocused()
await name.fill("Remote")
await expect(name).toHaveValue("Remote")
await username.click()
await expect(username).toBeFocused()
await username.fill("kit")
await expect(username).toHaveValue("kit")
await page.keyboard.press("Tab")
await expect(password).toBeFocused()
await password.fill("secret")
@@ -126,10 +126,6 @@ for (const create of [false, true]) {
json: [
{ directory: canonical, strategy: null },
{ directory: "/projects/existing-worktree", strategy: "git" },
...Array.from({ length: 20 }, (_, index) => ({
directory: `/projects/worktree-${index}`,
strategy: "git",
})),
],
headers,
})
@@ -165,9 +161,7 @@ for (const create of [false, true]) {
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
await expect(page.getByText("Session location unavailable", { exact: true })).toBeVisible()
listing.resolve()
const existing = page.getByRole("menuitem", { name: "existing-worktree", exact: true })
await expect(existing).toBeVisible()
await expect(existing).toHaveCSS("height", "28px")
await expect(page.getByRole("menuitem", { name: "existing-worktree", exact: true })).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("menu")).toHaveCount(0)
listing = Promise.withResolvers<void>()
@@ -1,130 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
test.beforeEach(async ({ page }) => {
const sessions = fixture.sessions.map((session) => ({ ...session }))
await mockOpenCodeServer(page, {
sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
})
await page.route("**/api/session/*/rename", async (route) => {
if (route.request().method() !== "POST") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-2)
const session = sessions.find((item) => item.id === id)
const payload: unknown = route.request().postDataJSON()
if (
!session ||
!payload ||
typeof payload !== "object" ||
!("title" in payload) ||
typeof payload.title !== "string"
)
throw new Error("Invalid rename request")
session.title = payload.title
await route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
})
await page.goto("/")
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.targetTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
for (const commit of ["Enter", "blur", "click outside"]) {
test(`saves the session heading on ${commit}`, async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await expect(input).toBeFocused()
await input.fill("Renamed session")
if (commit === "Enter") await input.press("Enter")
if (commit === "blur") await input.press("Tab")
if (commit === "click outside") await page.locator('[data-component="composer-editor"]').click()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
await expect(page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed session" })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
})
}
test("cancels the session heading with Escape", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Discard this title")
await input.press("Escape")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
test("keeps the draft when saving the session heading fails", async ({ page }) => {
await page.route("**/api/session/*/rename", (route) =>
route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } }),
)
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Retry this title")
await input.press("Tab")
await expect(page.getByText("Request failed", { exact: true })).toBeVisible()
await expect(input).toBeEnabled()
await expect(input).toHaveValue("Retry this title")
await expect(
page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle }),
).toBeVisible()
})
test("does not save an empty session heading", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill(" ")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
test("renames and closes the session tab from its context menu", async ({ page }) => {
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeHidden()
await expect(tab).toBeFocused()
await tab.press("Shift+F10")
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Renamed from tab")
await input.press("Enter")
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
const renamed = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed from tab" })
await renamed.click({ button: "right" })
await page.getByRole("menuitem", { name: "Close tab", exact: true }).click()
await expect(renamed).toBeHidden()
await page.getByRole("button", { name: "Home", exact: true }).click()
await expect(
page.locator('[data-component="home-session-row"]').filter({ hasText: "Renamed from tab" }),
).toBeVisible()
})
test("renames an inactive tab without switching sessions", async ({ page }) => {
await page.getByRole("button", { name: "Home", exact: true }).click()
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.sourceTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Inactive tab renamed")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/session/${fixture.sourceID}$`))
await page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Inactive tab renamed" }).click()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
})
@@ -141,7 +141,7 @@ test("shows the not found fallback when the viewed session is deleted", async ({
})
await expect(page.getByText("This session cannot be found")).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab", exact: true })).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab" })).toBeVisible()
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
})
@@ -69,7 +69,7 @@ test("keyboard navigation follows the visible tab order", async ({ page }) => {
await expect(page).toHaveURL(new RegExp(`${hrefC.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
})
test("mobile drawer exposes close controls and navigates between tabs", async ({ page }) => {
test("cramped tabs only show the close button for the active tab", async ({ page }) => {
await page.setViewportSize({ width: 360, height: 720 })
await mockServer(page)
await page.addInitScript(
@@ -89,31 +89,26 @@ test("mobile drawer exposes close controls and navigates between tabs", async ({
const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}`
const hrefB = `/server/${base64Encode(server)}/session/${sessionB.id}`
await page.goto(hrefA)
await page.getByRole("button", { name: "Tabs", exact: true }).click()
const tabA = page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefA}"])`)
const tabB = page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefB}"])`)
await expect(tabA).toHaveAttribute("data-active", "true")
await expect(tabB).toBeVisible()
await expect(tabA.locator('[data-slot="tab-close"]')).toBeVisible()
await expect(tabB.locator('[data-slot="tab-close"]')).toBeVisible()
await expect(tabB.locator('[data-slot="tab-close"]')).toBeHidden()
await tabB.locator(`a[href="${hrefB}"]`).click()
await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
await expect(page.getByRole("dialog", { name: "Tabs", exact: true })).toBeHidden()
await page.getByRole("button", { name: "Tabs", exact: true }).click()
await expect(tabA.locator('[data-slot="tab-close"]')).toBeVisible()
await expect(tabA.locator('[data-slot="tab-close"]')).toBeHidden()
await expect(tabB.locator('[data-slot="tab-close"]')).toBeVisible()
for (const direction of ["ltr", "rtl"]) {
await page.evaluate((direction) => document.documentElement.setAttribute("dir", direction), direction)
await page.setViewportSize({ width: 450, height: 720 })
await expect(tabA).toBeVisible()
await expect(tabA.locator("[data-titlebar-tab]")).toHaveAttribute("data-title-overflow", "true")
await page.setViewportSize({ width: 1280, height: 720 })
await expect(tabA.locator("[data-titlebar-tab]")).toHaveAttribute("data-title-overflow", "false")
await page.setViewportSize({ width: 450, height: 720 })
await page.getByRole("button", { name: "Tabs", exact: true }).click()
}
})
@@ -207,28 +202,28 @@ test("appearance experimental setting switches tab orientation", async ({ page }
await page.setViewportSize({ width: 920, height: 720 })
await expect(page.locator('[data-slot="vertical-tabs-sidebar"]')).toHaveCSS("width", "260px")
await expect(settings.getByRole("tablist")).toBeHidden()
await expect(settings.getByRole("button", { name: "Appearance", exact: true })).toBeVisible()
await expect(settings.getByRole("tablist")).toHaveCSS("width", "160px")
await page.setViewportSize({ width: 800, height: 720 })
await expect(settings.getByRole("tablist")).toBeHidden()
await expect(settings.getByRole("button", { name: "Appearance", exact: true })).toBeVisible()
await expect(settings.getByRole("tablist")).toHaveCSS("width", "160px")
await expect(version).toBeInViewport()
await page.setViewportSize({ width: 390, height: 720 })
await expect(settings.getByRole("button", { name: "Appearance", exact: true })).toBeVisible()
await expect(version).toBeInViewport()
await settings.evaluate((element) => element.setAttribute("dir", "rtl"))
await expect(settings.getByRole("button", { name: "Appearance", exact: true })).toBeInViewport()
await expect(version).toBeInViewport()
await expect(version).toHaveCSS("direction", "ltr")
await page.setViewportSize({ width: 390, height: 360 })
await expect(settings.getByRole("button", { name: "Appearance", exact: true })).toBeInViewport()
await version.scrollIntoViewIfNeeded()
await expect(version).toBeInViewport()
// Reload the UI-selected preference without seeding settings storage.
await page.reload()
const href = `/server/${base64Encode(server)}/session/${sessionA.id}`
await page.getByRole("button", { name: "Tabs", exact: true }).click()
await expect(
page
.locator('[data-slot="mobile-tabs-drawer"]')
.locator('[data-slot="titlebar-tabs"]')
.locator(`[data-titlebar-tab-link][href="${href}"]`)
.getByText(sessionA.title, { exact: true }),
).toBeVisible()
@@ -247,7 +242,7 @@ test("appearance experimental setting switches tab orientation", async ({ page }
await expect(layout).toContainText("Vertical")
})
test("vertical tab preference uses the drawer on mobile", async ({ page }) => {
test("vertical tab preference falls back to horizontal on mobile", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 720 })
await mockServer(page)
await page.addInitScript(
@@ -264,8 +259,7 @@ test("vertical tab preference uses the drawer on mobile", async ({ page }) => {
const href = `/server/${base64Encode(server)}/session/${sessionA.id}`
await page.goto(href)
await page.getByRole("button", { name: "Tabs", exact: true }).click()
const tabs = page.locator('[data-slot="mobile-tabs-drawer"]')
const tabs = page.locator('[data-slot="titlebar-tabs"]')
await expect(tabs.locator(`[data-titlebar-tab-link][href="${href}"]`)).toContainText(sessionA.title)
await expect(page.locator('[data-slot="vertical-tabs-sidebar"]')).toHaveCount(0)
-6
View File
@@ -8,12 +8,6 @@ export async function expectAppVisible(locator: Locator) {
}
export async function expectSessionTitle(page: Page, title: string) {
if ((page.viewportSize()?.width ?? 1280) < 768) {
const trigger = page.locator('[data-slot="mobile-tabs-trigger"]')
await expectAppVisible(trigger)
await expect(trigger.locator('span[dir="auto"]')).toHaveText(title, { timeout: APP_READY_TIMEOUT })
return
}
await expectAppVisible(page.getByRole("heading", { name: title }))
}
+2 -2
View File
@@ -9,13 +9,13 @@
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
"purpose": "any"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
"purpose": "any"
}
],
"theme_color": "#080808",
-1
View File
@@ -56,7 +56,6 @@
"vite-plugin-solid": "2.11.14"
},
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
"@dnd-kit/helpers": "0.5.0",
+5 -29
View File
@@ -9,7 +9,6 @@ import { Button } from "@opencode-ai/ui/button"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { AttachmentCard } from "@opencode-ai/session-ui/attachment-card"
import { CommentCard } from "@opencode-ai/session-ui/comment-card"
import { typeLabel } from "@opencode-ai/session-ui/message-file"
@@ -54,7 +53,6 @@ export function ComposerEditor(props: ComposerEditorProps) {
const state = props.controller.state
const view = props.controller.view
let editor: HTMLDivElement | undefined
let viewport: HTMLDivElement | undefined
let localInput = false
const updateCursor = () => {
if (!editor || !window.getSelection()?.isCollapsed) return
@@ -114,10 +112,8 @@ export function ComposerEditor(props: ComposerEditorProps) {
<form
data-component="composer"
data-dock-border-underlay={props.borderUnderlay ? "true" : undefined}
class="group/composer relative min-h-[96px] w-full overflow-clip rounded-xl"
class="group/composer relative min-h-[96px] w-full overflow-clip rounded-xl bg-v2-background-bg-base"
classList={{
"bg-v2-background-bg-layer-01": props.borderUnderlay,
"bg-v2-background-bg-base": !props.borderUnderlay,
"shadow-[var(--v2-elevation-raised)]": !props.borderUnderlay,
"border border-v2-icon-icon-info border-dashed": state.drag === "active",
}}
@@ -149,14 +145,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
/>
</Show>
<ScrollView
data-component="composer-scroll"
class="min-h-[60px] max-h-[180px]"
viewportRef={(element) => {
viewport = element
element.tabIndex = -1
}}
>
<div class="relative min-h-[60px]">
<div
ref={(element) => {
editor = element
@@ -173,7 +162,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
spellcheck={state.mode === "normal"}
// @ts-expect-error
autocomplete="off"
class="relative z-10 block min-h-[60px] w-full whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
classList={{ "font-mono!": state.mode === "shell", "opacity-50": props.disabled }}
style={{
"unicode-bidi": state.mode === "normal" ? "plaintext" : undefined,
@@ -201,20 +190,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
}}
onKeyUp={updateCursor}
onPointerUp={updateCursor}
onPaste={(event) => {
props.controller.onPaste(event)
// Programmatic multiline insertion does not reliably reveal the caret.
requestAnimationFrame(() => {
const selection = window.getSelection()
if (!editor || !viewport || !selection?.isCollapsed || !selection.rangeCount) return
if (!editor.contains(selection.anchorNode)) return
const caret = selection.getRangeAt(0).getBoundingClientRect()
if (!caret.height) return
const bounds = viewport.getBoundingClientRect()
if (caret.bottom > bounds.bottom - 8) viewport.scrollTop += caret.bottom - bounds.bottom + 8
if (caret.top < bounds.top + 8) viewport.scrollTop += caret.top - bounds.top - 8
})
}}
onPaste={props.controller.onPaste}
onFocus={() => props.controller.dispatch({ type: "focus.editor" })}
/>
<Show when={!props.controller.value()}>
@@ -230,7 +206,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
: i18n.t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" }))}
</div>
</Show>
</ScrollView>
</div>
<div class="flex h-11 items-center px-2">
<div
+1 -6
View File
@@ -6,7 +6,6 @@ import { AppBaseProviders, AppInterface } from "@/app"
import { loadInitialLocale } from "@/runtime/i18n/language"
import { PlatformProvider } from "@/runtime/platform/platform"
import { createWebPlatform } from "@/runtime/platform/web"
import { isStandalone, PwaRoutePersistence, restorePwaRoute } from "@/runtime/platform/pwa"
import en from "@/runtime/i18n/en"
import zh from "@/runtime/i18n/zh"
import { authFromToken } from "@/runtime/server/api"
@@ -72,8 +71,6 @@ if (root instanceof HTMLElement && root.dataset.opencodeMounted === undefined) {
void loadInitialLocale().then((locale) => {
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
clearAuthToken()
const standalone = isStandalone()
if (standalone) restorePwaRoute()
const server: ServerConnection.Http = {
type: "http",
authToken: !!auth,
@@ -90,9 +87,7 @@ if (root instanceof HTMLElement && root.dataset.opencodeMounted === undefined) {
defaultServer={ServerConnection.Key.make(web.defaultServerUrl)}
canonicalLocalServer={ServerConnection.key(server)}
servers={[server]}
>
{standalone && <PwaRoutePersistence />}
</AppInterface>
/>
</AppBaseProviders>
</PlatformProvider>
),
+1 -6
View File
@@ -2,14 +2,9 @@ import type { HomeProjectsController } from "./controller"
import { HomeProjectsView } from "./view"
import type { HomeScrollController } from "../scroll"
export function HomeProjects(props: {
projects: HomeProjectsController
scroll: HomeScrollController
dropdown?: boolean
}) {
export function HomeProjects(props: { projects: HomeProjectsController; scroll: HomeScrollController }) {
return (
<HomeProjectsView
dropdown={props.dropdown}
language={props.projects.copy.language}
servers={props.projects.server.list()}
projects={props.projects.project.list()}
+22 -131
View File
@@ -1,6 +1,5 @@
import { createMemo, For, type JSX, onCleanup, Show, splitProps } from "solid-js"
import { createStore } from "solid-js/store"
import { Popover } from "@kobalte/core/popover"
import { DragDropProvider, PointerSensor } from "@dnd-kit/solid"
import { isSortable, useSortable } from "@dnd-kit/solid/sortable"
import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom"
@@ -29,7 +28,6 @@ const projectContextMenuID = (server: ServerConnection.Any, directory: string) =
`project:${ServerConnection.key(server)}:${directory}`
export type HomeProjectsViewProps = {
dropdown?: boolean
language: ReturnType<typeof useLanguage>
servers: ServerConnection.Any[]
projects: LocalProject[]
@@ -66,81 +64,6 @@ export type HomeProjectsViewProps = {
}
export function HomeProjectsView(props: HomeProjectsViewProps) {
const [state, setState] = createStore({ open: false })
const selected = createMemo(() => props.projects.find((project) => project.worktree === props.selection.directory))
const server = createMemo(() =>
props.servers.find((server) => ServerConnection.key(server) === props.selection.server),
)
return (
<Show when={props.dropdown} fallback={<HomeProjectsPanel {...props} />}>
<Popover
open={state.open}
onOpenChange={(open) => setState("open", open)}
placement="bottom-start"
sameWidth
gutter={6}
>
<Popover.Trigger
data-component="home-projects-dropdown"
aria-label={props.language.t("home.projects")}
class="flex h-10 w-full min-w-0 items-center gap-2 rounded-[6px] bg-v2-background-bg-base px-1.5 text-start text-v2-text-text-base outline-none hover:bg-v2-background-bg-layer-01"
>
<Show when={selected()} fallback={<Icon name="folder" size="small" />}>
{(project) => <HomeProjectAvatar project={project()} />}
</Show>
<span class="flex min-w-0 flex-1 flex-col leading-[var(--line-height-compact)]">
<bdi class="truncate">
<Show when={selected()} fallback={props.language.t("home.projects.all")}>
{(project) => displayName(project())}
</Show>
</bdi>
<Show when={props.servers.length > 1 && server()}>
{(server) => (
<bdi class="truncate text-v2-text-text-muted opacity-70">
{server().displayName ?? new URL(server().http.url).host}
</bdi>
)}
</Show>
</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
aria-label={props.language.t("home.projects")}
dir={props.language.direction()}
class="z-50 max-h-[min(70dvh,var(--kb-popper-content-available-height))] overflow-hidden rounded-[10px] bg-v2-background-bg-base p-1.5 shadow-[var(--v2-elevation-floating)] outline-none data-[expanded]:animate-in data-[expanded]:fade-in data-[expanded]:slide-in-from-top-2 duration-150 ease-out motion-reduce:animate-none"
>
<HomeProjectsPanel
{...props}
onSelectProject={(server, directory) => {
props.onSelectProject(server, directory)
setState("open", false)
}}
onFocusServer={(server) => {
props.onFocusServer(server)
setState("open", false)
}}
onChooseProject={(server) => {
setState("open", false)
props.onChooseProject(server)
}}
onAddProjects={(server, directories) => {
setState("open", false)
props.onAddProjects(server, directories)
}}
onOpenProjectNewSession={(server, directory) => {
setState("open", false)
props.onOpenProjectNewSession(server, directory)
}}
/>
</Popover.Content>
</Popover.Portal>
</Popover>
</Show>
)
}
function HomeProjectsPanel(props: HomeProjectsViewProps) {
const [contextMenu, setContextMenu] = createStore({ open: undefined as string | undefined })
const contextMenuProps = {
contextMenuOpen: (id: string) => contextMenu.open === id,
@@ -148,77 +71,48 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
}
return (
<aside
class={
props.dropdown
? "flex max-h-[min(60dvh,calc(var(--kb-popper-content-available-height)-12px))] min-h-0 min-w-0 flex-col overflow-hidden"
: `
class={`
mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden
lg:sticky lg:top-14 lg:mt-14 lg:h-[calc(100cqh-56px)] lg:self-start lg:pt-[52px]
`
}
`}
aria-label={props.language.t("home.projects")}
onWheel={(event) => {
if (event.target === event.currentTarget) return
props.onWheel(event)
}}
>
<Show when={!props.dropdown}>
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
<Show when={props.servers.length === 1 && !(props.projects.length === 0 && props.recentlyClosed.length > 0)}>
<Tooltip placement="bottom" value={props.language.t("home.project.add")}>
<IconButton
data-action="home-add-project"
variant="ghost-muted"
size="large"
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
icon={<Icon name="folder-add-left" />}
disabled={props.serverHealth(props.servers[0])?.healthy === false}
onClick={() => props.onChooseProject(props.servers[0])}
aria-label={props.language.t("home.project.add")}
/>
</Tooltip>
</Show>
</div>
</Show>
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
<Show when={props.dropdown && props.servers.length === 1}>
<HomeProjectNavButton
type="button"
class="mb-1"
data-selected={!props.selection.directory ? "" : undefined}
onClick={() => props.onFocusServer(props.servers[0])}
>
<Icon name="folder" size="small" />
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.projects.all")}</span>
</HomeProjectNavButton>
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
<Show when={props.servers.length === 1 && !(props.projects.length === 0 && props.recentlyClosed.length > 0)}>
<Tooltip placement="bottom" value={props.language.t("home.project.add")}>
<IconButton
data-action="home-add-project"
variant="ghost-muted"
size="large"
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
icon={<Icon name="folder-add-left" />}
disabled={props.serverHealth(props.servers[0])?.healthy === false}
onClick={() => props.onChooseProject(props.servers[0])}
aria-label={props.language.t("home.project.add")}
/>
</Tooltip>
</Show>
</div>
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
<Show
when={props.servers.length > 1}
fallback={
<div class={props.dropdown ? "" : "pr-3"}>
<div class="pr-3">
<Show
when={props.projects.length > 0}
fallback={<HomeProjectEmpty {...props} server={props.servers[0]} items={props.recentlyClosed} />}
>
<HomeProjectList {...props} {...contextMenuProps} server={props.servers[0]} items={props.projects} />
<Show when={props.dropdown}>
<HomeProjectNavButton
type="button"
data-action="home-add-project-row"
class="mt-1 disabled:opacity-60"
disabled={props.serverHealth(props.servers[0])?.healthy === false}
onClick={() => props.onChooseProject(props.servers[0])}
>
<Icon name="folder-add-left" size="small" />
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.project.add")}</span>
</HomeProjectNavButton>
</Show>
</Show>
</div>
}
>
<div class={`flex min-w-0 flex-col ${props.dropdown ? "gap-1" : "gap-4 pr-3"}`}>
<div class="flex min-w-0 flex-col gap-4 pr-3">
<For each={props.servers}>
{(item) => {
const projects = () => props.projectsForServer(item)
@@ -263,9 +157,7 @@ export function HomeUtilityNav(props: {
language: ReturnType<typeof useLanguage>
}) {
return (
<div
class={`${props.class ?? ""} min-w-0 flex-row justify-between gap-1 lg:flex-col lg:justify-start lg:pr-3 [&>button]:w-auto lg:[&>button]:w-full`}
>
<div class={`${props.class ?? ""} min-w-0 flex-col gap-1 pr-3`}>
<HomeProjectNavButton
type="button"
class="text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted"
@@ -619,7 +511,6 @@ function HomeProjectRow(
// does not focus that server and load its session index. Touch is
// excluded so flick-scrolling the list cannot select rows.
pointerDownSelected = undefined
if (props.dropdown) return
if (event.button !== 0 || event.pointerType === "touch") return
if (!props.serverSelected) return
pointerDownSelected = props.selected
+11 -23
View File
@@ -1,6 +1,4 @@
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { createMediaQuery } from "@solid-primitives/media"
import { Show } from "solid-js"
import { createHomeController } from "./model"
import { createHomeProjectsController } from "./projects/controller"
import { HomeUtilityNav } from "./projects/view"
@@ -11,7 +9,6 @@ import { createHomeSessionsController } from "./sessions/controller"
import { HomeSessions } from "./sessions/region"
export function Home() {
const mobile = createMediaQuery("(max-width: 767px)")
const home = createHomeController()
const projects = createHomeProjectsController(home)
const sessions = createHomeSessionsController(home)
@@ -20,17 +17,12 @@ export function Home() {
return (
<div
class={`
mx-2 mb-[var(--shell-bottom-inset,8px)] mt-[var(--shell-top-inset,8px)] flex min-h-0 flex-1 flex-col self-stretch overflow-hidden rounded-[10px]
mx-2 mb-2 mt-[var(--shell-top-inset,8px)] min-h-0 flex-1 self-stretch overflow-hidden rounded-[10px]
bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]
`}
>
<Show when={mobile()}>
<div class="relative z-40 -mb-3 shrink-0 px-3 pt-3">
<HomeProjects projects={projects} scroll={scroll} dropdown />
</div>
</Show>
<ScrollView
class="min-h-0 flex-1 [container-type:size]"
class="h-full [container-type:size]"
thumbContainer={scroll.viewport.thumbTrack()}
thumbHoverTarget={scroll.viewport.hoverTarget()}
viewportRef={scroll.viewport.setViewport}
@@ -39,24 +31,20 @@ export function Home() {
>
<div
class={`
mx-auto grid min-h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)] gap-4 px-3
max-md:grid-rows-[minmax(0,1fr)] lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6
mx-auto grid min-h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3
lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6
`}
>
<Show when={!mobile()}>
<HomeProjects projects={projects} scroll={scroll} />
</Show>
<HomeProjects projects={projects} scroll={scroll} />
<HomeSessions sessions={sessions} search={search} scroll={scroll} />
<HomeUtilityNav
class="flex lg:hidden"
onOpenSettings={projects.utility.settings}
onOpenHelp={projects.utility.help}
language={projects.copy.language}
/>
</div>
</ScrollView>
<div class="hidden shrink-0 px-3 py-2 md:block lg:hidden">
<HomeUtilityNav
class="flex"
onOpenSettings={projects.utility.settings}
onOpenHelp={projects.utility.help}
language={projects.copy.language}
/>
</div>
</div>
)
}
-26
View File
@@ -1,26 +0,0 @@
@media (max-width: 767px) {
[data-component="home-session-row-container"][data-project-name="true"],
[data-component="home-session-row-container"][data-project-name="true"] [data-component="home-session-row"],
[data-component="home-session-row-container"][data-project-name="true"] [data-slot="home-session-editor"],
[data-component="home-session-search-row"][data-project-name="true"] {
height: 48px;
padding-block: 0;
}
[data-project-name="true"] [data-slot="home-session-labels"] {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: stretch;
gap: 0;
text-align: start;
}
[data-project-name="true"] [data-slot="home-session-labels"] > * {
max-width: 100%;
flex: none;
font-size: 13px;
line-height: var(--line-height-compact);
}
}
+54 -76
View File
@@ -15,7 +15,6 @@ import { ServerConnection } from "@/runtime/server/registry"
import { SessionTabAvatarView } from "@/shell/layout/session-tab-avatar"
import { sessionLabel } from "@/session/title"
import { shouldOpenSessionInBackground } from "./open"
import "./view.css"
import {
HomeSessionStatusController,
homeSessionSearchKey,
@@ -97,13 +96,10 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
class="min-h-0 min-w-0 flex-1 flex flex-col"
aria-label={props.language.t("sidebar.project.recentSessions")}
>
<div
class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-1 pt-6 md:pb-3 lg:pt-12"
onWheel={props.onWheel}
>
<div class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12" onWheel={props.onWheel}>
<HomeSessionSearch {...props} />
<Show when={props.groups.length > 0 && props.canCreateSession}>
<div class="pointer-events-none absolute right-0 top-[68px] z-20 flex md:top-[84px] lg:top-[108px]">
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
<Button
data-action="home-new-session"
variant="ghost-muted"
@@ -117,18 +113,18 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
</div>
</Show>
</div>
<div class="pointer-events-none sticky top-[68px] z-40 h-0 -mr-3 md:top-[84px] lg:top-[108px]">
<div class="pointer-events-none sticky top-[84px] z-40 h-0 -mr-3 lg:top-[108px]">
<div
ref={props.onSetThumbTrack}
data-component="home-session-scroll-track"
class="relative ml-auto h-[calc(100cqh-68px)] w-3 md:h-[calc(100cqh-84px)] lg:h-[calc(100cqh-108px)]"
class="relative ml-auto h-[calc(100cqh-84px)] w-3 lg:h-[calc(100cqh-108px)]"
/>
</div>
<div class="-mr-3 min-h-[calc(100cqh-64px)] md:min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)]">
<div class="-mr-3 min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)]">
<Show
when={!props.loading}
fallback={
<div class="pt-1 md:pt-3">
<div class="pt-3">
<HomeSessionSkeleton label={props.language.t("common.loading")} />
</div>
}
@@ -142,7 +138,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
/>
}
>
<div ref={props.onSetContent} class="flex flex-col pt-1 pr-3 pb-16 md:pt-3">
<div ref={props.onSetContent} class="flex flex-col pt-3 pr-3 pb-16">
{/* Index keeps group subtrees mounted when the group arrays are
rebuilt, so store updates cannot recreate rows mid-gesture. */}
<Index each={props.groups}>
@@ -154,9 +150,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
onSetRef={(element) => props.onSetHeader(group().id, element)}
elevated={index === 0}
/>
<div
class={`flex min-w-0 flex-col gap-px pt-2 md:pt-4 ${index === props.groups.length - 1 ? "" : "mb-6"}`}
>
<div class={`flex min-w-0 flex-col gap-px pt-4 ${index === props.groups.length - 1 ? "" : "mb-6"}`}>
{/* Rows key by session ID: session.sync replaces the
stored session object wholesale, so reference-keyed
rows would be disposed mid-interaction whenever a
@@ -241,11 +235,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
absolute flex flex-col overflow-hidden rounded-[12px]
bg-v2-background-bg-base shadow-[var(--v2-elevation-floating)]
`}
style={{
top: "-6px",
"inset-inline-start": "-6px",
width: "calc(100% + 12px)",
}}
style={{ top: "-6px", left: "-6px", width: "calc(100% + 12px)" }}
>
<div class="flex flex-col pt-9">
<div id={HOME_SESSION_SEARCH_RESULTS_ID} role="listbox" class="flex flex-col gap-4 pt-4">
@@ -279,7 +269,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
>
{props.language.t("home.sessions.search.sessions")}
</p>
<ScrollView class="max-h-[min(20rem,40dvh)]" viewportRef={props.onSetSearchList}>
<ScrollView class="max-h-80" viewportRef={props.onSetSearchList}>
<div class="flex flex-col gap-px pb-2">
<For each={props.searchResults}>
{(record) => (
@@ -301,7 +291,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
</Show>
<label
class={`
relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] py-1 ps-3 pe-2
relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] py-1 pl-3 pr-2
bg-v2-background-bg-layer-02/60 text-v2-icon-icon-muted transition-[background-color,box-shadow]
duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02
`}
@@ -385,7 +375,6 @@ function HomeSessionSearchResultRow(
id={`home-session-search-option-${key()}`}
data-key={key()}
data-component="home-session-search-row"
data-project-name={!!showProjectName()}
role="option"
aria-selected={props.selected}
class={`
@@ -414,7 +403,7 @@ function HomeSessionSearchResultRow(
record={props.record}
revealProjectOnHover={!!showProjectName()}
/>
<div data-slot="home-session-labels" class="flex min-w-0 flex-1 items-center gap-1.5">
<div class="flex min-w-0 flex-1 items-center gap-1.5">
<HomeSessionTitle title={title()} showProjectName={!!showProjectName()} search />
<Show when={showProjectName()}>
<HomeSessionProjectName name={props.record.projectName} search />
@@ -434,8 +423,8 @@ function HomeSessionGroupHeader(props: {
<div
ref={props.onSetRef}
class={`
pointer-events-none sticky top-[68px] flex h-7 min-w-0 items-center justify-between
bg-v2-background-bg-base ps-1.5 md:ps-3 md:top-[84px] lg:top-[108px]
pointer-events-none sticky top-[84px] flex h-7 min-w-0 items-center justify-between
bg-v2-background-bg-base pl-3 lg:top-[108px]
`}
classList={{ "home-session-group-header z-[5]": !!props.elevated, "z-10": !props.elevated }}
>
@@ -521,7 +510,6 @@ function HomeSessionRow(
return (
<div
data-component="home-session-row-container"
data-project-name={!!showProjectName()}
data-session-id={props.record.session.id}
class="group/session relative flex h-10 min-w-0 items-center rounded-[6px] outline-none focus:outline-none focus-visible:outline-none"
classList={{ group: !!showProjectName() }}
@@ -535,56 +523,51 @@ function HomeSessionRow(
<Show
when={!editor()}
fallback={
<div
data-slot="home-session-editor"
class="flex h-10 min-w-0 w-full flex-1 items-center gap-2 py-3 ps-1.5 pe-3 md:ps-3 md:pe-10"
>
<div class="flex h-10 min-w-0 w-full flex-1 items-center gap-2 py-3 pl-3 pr-10">
<HomeSessionLeadingController
server={props.server}
isOpenTab={props.isOpenTab}
record={props.record}
revealProjectOnHover={false}
/>
<div data-slot="home-session-labels" class="contents">
<InlineInput
data-component="home-session-rename"
aria-label={props.language.t("common.rename")}
dir="auto"
value={editor()?.draft ?? ""}
disabled={editor()?.renaming ?? false}
class={`
<InlineInput
data-component="home-session-rename"
aria-label={props.language.t("common.rename")}
dir="auto"
value={editor()?.draft ?? ""}
disabled={editor()?.renaming ?? false}
class={`
block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base
[font-weight:530] field-sizing-content outline-none focus:outline-none focus-visible:outline-none
${showProjectName() ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}
`}
style={{ "--inline-input-shadow": "none", "text-align": "start" }}
onInput={(event) => {
const draft = event.currentTarget.value
props.setRowUI("editor", (value) => (value?.id === sessionID() ? { ...value, draft } : value))
}}
onKeyDown={(event) => {
event.stopPropagation()
// Enter and Escape during IME composition commit or cancel
// the composition, not the rename. Safari can report the
// composition-confirming keydown with isComposing false but
// keyCode 229.
if (event.isComposing || event.keyCode === 229) return
if (event.key === "Enter") {
event.preventDefault()
void saveEditor()
return
}
if (event.key !== "Escape") return
style={{ "--inline-input-shadow": "none", "text-align": "start" }}
onInput={(event) => {
const draft = event.currentTarget.value
props.setRowUI("editor", (value) => (value?.id === sessionID() ? { ...value, draft } : value))
}}
onKeyDown={(event) => {
event.stopPropagation()
// Enter and Escape during IME composition commit or cancel
// the composition, not the rename. Safari can report the
// composition-confirming keydown with isComposing false but
// keyCode 229.
if (event.isComposing || event.keyCode === 229) return
if (event.key === "Enter") {
event.preventDefault()
closeEditor()
requestAnimationFrame(() => rowButton()?.focus())
}}
onBlur={closeEditor}
/>
<Show when={showProjectName()}>
<HomeSessionProjectName name={props.record.projectName} />
</Show>
</div>
void saveEditor()
return
}
if (event.key !== "Escape") return
event.preventDefault()
closeEditor()
requestAnimationFrame(() => rowButton()?.focus())
}}
onBlur={closeEditor}
/>
<Show when={showProjectName()}>
<HomeSessionProjectName name={props.record.projectName} />
</Show>
</div>
}
>
@@ -595,7 +578,7 @@ function HomeSessionRow(
aria-expanded={!!menu()}
class={`
flex h-10 min-w-0 w-full flex-1 shrink-0 cursor-default items-center gap-2 rounded-[6px] border-0
bg-transparent py-3 ps-1.5 pe-3 md:ps-3 md:pe-10 text-start text-v2-text-text-muted [font-weight:530]
bg-transparent py-3 pl-3 pr-10 text-left text-v2-text-text-muted [font-weight:530]
transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out
hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none
`}
@@ -658,12 +641,10 @@ function HomeSessionRow(
record={props.record}
revealProjectOnHover={!!showProjectName()}
/>
<div data-slot="home-session-labels" class="contents">
<HomeSessionTitle title={title()} showProjectName={!!showProjectName()} />
<Show when={showProjectName()}>
<HomeSessionProjectName name={props.record.projectName} />
</Show>
</div>
<HomeSessionTitle title={title()} showProjectName={!!showProjectName()} />
<Show when={showProjectName()}>
<HomeSessionProjectName name={props.record.projectName} />
</Show>
</button>
</Show>
<Menu
@@ -742,7 +723,6 @@ function HomeSessionTitle(props: { title: string; showProjectName: boolean; sear
return (
<span
data-component="home-session-title"
dir="auto"
class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base [font-weight:530]"
classList={{
"text-[13px] leading-4 tracking-[-0.04px]": !!props.search,
@@ -758,8 +738,6 @@ function HomeSessionTitle(props: { title: string; showProjectName: boolean; sear
function HomeSessionProjectName(props: { name: string; search?: boolean }) {
return (
<span
data-component="home-session-project-name"
dir="auto"
class="min-w-0 flex-[1_1_auto] overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-muted [font-weight:440]"
classList={{ "text-[13px] leading-4 tracking-[-0.04px]": !!props.search }}
>
@@ -801,7 +779,7 @@ function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnT
function HomeSessionSkeleton(props: { label: string }) {
return (
<div class="flex min-w-0 flex-col gap-4">
<div class="flex h-7 min-w-0 items-center justify-between ps-1.5 pe-4 md:ps-4">
<div class="flex h-7 min-w-0 items-center justify-between px-4">
<div class={HOME_SECTION_LABEL}>{props.label}</div>
</div>
<div class="flex min-w-0 flex-col gap-px" aria-hidden="true">
+1 -1
View File
@@ -73,7 +73,7 @@ export default function NewSessionPage(props: { draftId: string }) {
<div class="relative size-full overflow-hidden flex flex-col">
{suspendUntilPromptReady()}
<NewSessionStatus visible={settings.visibility.status()} />
<div class="flex-1 min-h-0 flex flex-col gap-2 px-2 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]">
<div class="flex-1 min-h-0 flex flex-col gap-2 px-2 pb-2 pt-[var(--shell-top-inset,8px)]">
<NewSessionView composer={model} project={project} workspace={workspace} />
</div>
</div>
+3
View File
@@ -414,6 +414,8 @@ export const dict = {
"dialog.server.add.button": "አገልጋይ አክል",
"dialog.server.add.name": "የአገልጋይ ስም (አማራጭ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "የተጠቃሚ ስም (አማራጭ)",
"dialog.server.add.usernamePlaceholder": "የተጠቃሚ ስም",
"dialog.server.add.password": "የይለፍ ቃል (አማራጭ)",
"dialog.server.add.passwordPlaceholder": "የይለፍ ቃል",
"dialog.server.edit.title": "አገልጋይ አርትዕ",
@@ -496,6 +498,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ዴስክቶፕ {{version}}",
"wsl.onboarding.versionMismatch": "የተጫነው ሥሪት ከዴስክቶፕ መተግበሪያ ሥሪት ጋር አይዛመድም።",
"wsl.onboarding.adding": "በማከል ላይ...",
"server.row.noUsername": "የተጠቃሚ ስም የለም",
"dialog.project.edit.title": "ፕሮጀክት አርትዕ",
"dialog.project.edit.name": "ስም",
"dialog.project.edit.icon": "አዶ",
+3
View File
@@ -420,6 +420,7 @@ export const dict = {
"dialog.server.add.button": "إضافة خادم",
"dialog.server.add.name": "اسم الخادم (اختياري)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "اسم المستخدم (اختياري)",
"dialog.server.add.password": "كلمة المرور (اختياري)",
"dialog.server.edit.title": "تحرير الخادم",
"dialog.server.default.title": "الخادم الافتراضي",
@@ -1023,7 +1024,9 @@ export const dict = {
"app.server.unreachable": "تعذر الوصول إلى {{server}}",
"app.server.retrying": "جارٍ إعادة المحاولة تلقائيًا...",
"app.server.otherServers": "خوادم أخرى",
"dialog.server.add.usernamePlaceholder": "اسم المستخدم",
"dialog.server.add.passwordPlaceholder": "كلمة المرور",
"server.row.noUsername": "لا يوجد اسم مستخدم",
"session.review.noVcs.createGit.title": "إنشاء مستودع Git",
"session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع",
"session.review.noVcs.createGit.actionLoading": "جارٍ إنشاء مستودع Git...",
+3
View File
@@ -421,6 +421,8 @@ export const dict = {
"dialog.server.add.button": "Server əlavə et",
"dialog.server.add.name": "Server adı (istəyə bağlı)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "İstifadəçi adı (istəyə bağlı)",
"dialog.server.add.usernamePlaceholder": "istifadəçi adı",
"dialog.server.add.password": "Şifrə (istəyə bağlı)",
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serveri redaktə et",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "masaüstü {{version}}",
"wsl.onboarding.versionMismatch": "Quraşdırılmış versiya masaüstü proqram versiyasına uyğun gəlmir.",
"wsl.onboarding.adding": "Əlavə edilir...",
"server.row.noUsername": "istifadəçi adı yoxdur",
"dialog.project.edit.title": "Layihəni redaktə et",
"dialog.project.edit.name": "Ad",
"dialog.project.edit.icon": "İkon",
+3
View File
@@ -421,6 +421,8 @@ export const dict = {
"dialog.server.add.button": "Добавете сървър",
"dialog.server.add.name": "Име на сървъра (по избор)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "потребителско име (по избор)",
"dialog.server.add.usernamePlaceholder": "потребителско име",
"dialog.server.add.password": "Парола (по избор)",
"dialog.server.add.passwordPlaceholder": "парола",
"dialog.server.edit.title": "Редактиране на сървъра",
@@ -506,6 +508,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "работен плот {{version}}",
"wsl.onboarding.versionMismatch": "Инсталираната версия не съответства на версията на настолното приложение.",
"wsl.onboarding.adding": "Добавяне...",
"server.row.noUsername": "няма потребителско име",
"dialog.project.edit.title": "Редактиране на проекта",
"dialog.project.edit.name": "Име",
"dialog.project.edit.icon": "Икона",
+3
View File
@@ -418,6 +418,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "সার্ভার যোগ করুন",
"dialog.server.add.name": "সার্ভারের নাম (ঐচ্ছিক)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ব্যবহারকারীর নাম (ঐচ্ছিক)",
"dialog.server.add.usernamePlaceholder": "ব্যবহারকারীর নাম",
"dialog.server.add.password": "পাসওয়ার্ড (ঐচ্ছিক)",
"dialog.server.add.passwordPlaceholder": "পাসওয়ার্ড",
"dialog.server.edit.title": "সার্ভার সম্পাদনা করুন",
@@ -503,6 +505,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ডেস্কটপ {{version}}",
"wsl.onboarding.versionMismatch": "ইনস্টল করা সংস্করণ ডেস্কটপ অ্যাপ সংস্করণের সাথে মেলে না।",
"wsl.onboarding.adding": "যোগ করা হচ্ছে...",
"server.row.noUsername": "ব্যবহারকারীর নাম নেই",
"dialog.project.edit.title": "প্রকল্প সম্পাদনা করুন",
"dialog.project.edit.name": "নাম",
"dialog.project.edit.icon": "আইকন",
+3
View File
@@ -422,6 +422,7 @@ export const dict = {
"dialog.server.add.button": "Adicionar servidor",
"dialog.server.add.name": "Nome do servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nome de usuário (opcional)",
"dialog.server.add.password": "Senha (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.default.title": "Servidor padrão",
@@ -1034,7 +1035,9 @@ export const dict = {
"app.server.unreachable": "Não foi possível conectar a {{server}}",
"app.server.retrying": "Tentando novamente automaticamente...",
"app.server.otherServers": "Outros servidores",
"dialog.server.add.usernamePlaceholder": "nome de usuário",
"dialog.server.add.passwordPlaceholder": "senha",
"server.row.noUsername": "sem nome de usuário",
"session.review.noVcs.createGit.title": "Criar um repositório Git",
"session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto",
"session.review.noVcs.createGit.actionLoading": "Criando repositório Git...",
+3
View File
@@ -449,6 +449,7 @@ export const dict = {
"dialog.server.add.button": "Dodaj server",
"dialog.server.add.name": "Ime servera (opcionalno)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Korisničko ime (opcionalno)",
"dialog.server.add.password": "Lozinka (opcionalno)",
"dialog.server.edit.title": "Uredi server",
"dialog.server.default.title": "Podrazumijevani server",
@@ -1112,7 +1113,9 @@ export const dict = {
"app.server.unreachable": "Nije moguće pristupiti {{server}}",
"app.server.retrying": "Automatski ponovni pokušaj...",
"app.server.otherServers": "Drugi serveri",
"dialog.server.add.usernamePlaceholder": "korisničko ime",
"dialog.server.add.passwordPlaceholder": "lozinka",
"server.row.noUsername": "nema korisničkog imena",
"session.review.noVcs.createGit.title": "Kreiraj Git repozitorij",
"session.review.noVcs.createGit.description": "Prati, pregledaj i poništi promjene u ovom projektu",
"session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Afegeix servidor",
"dialog.server.add.name": "Nom del servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nom d'usuari (opcional)",
"dialog.server.add.usernamePlaceholder": "nom d'usuari",
"dialog.server.add.password": "Contrasenya (opcional)",
"dialog.server.add.passwordPlaceholder": "contrasenya",
"dialog.server.edit.title": "Edita el servidor",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "escriptori {{version}}",
"wsl.onboarding.versionMismatch": "La versió instal·lada no coincideix amb la versió de l'aplicació d'escriptori.",
"wsl.onboarding.adding": "S'està afegint...",
"server.row.noUsername": "sense nom d'usuari",
"dialog.project.edit.title": "Edita el projecte",
"dialog.project.edit.name": "Nom",
"dialog.project.edit.icon": "Icona",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Přidat server",
"dialog.server.add.name": "Název serveru (volitelné)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "uživatelské jméno (volitelné)",
"dialog.server.add.usernamePlaceholder": "uživatelské jméno",
"dialog.server.add.password": "Heslo (volitelné)",
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upravit server",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Nainstalovaná verze neodpovídá verzi aplikace pro stolní počítače.",
"wsl.onboarding.adding": "Přidávání...",
"server.row.noUsername": "žádné uživatelské jméno",
"dialog.project.edit.title": "Upravit projekt",
"dialog.project.edit.name": "Jméno",
"dialog.project.edit.icon": "ikona",
+3
View File
@@ -346,6 +346,7 @@ export const dict = {
"dialog.server.add.button": "Tilføj server",
"dialog.server.add.name": "Servernavn (valgfrit)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brugernavn (valgfrit)",
"dialog.server.add.password": "Adgangskode (valgfrit)",
"dialog.server.edit.title": "Rediger server",
"dialog.server.default.title": "Standardserver",
@@ -1002,7 +1003,9 @@ export const dict = {
"app.server.unreachable": "Kunne ikke nå {{server}}",
"app.server.retrying": "Prøver igen automatisk...",
"app.server.otherServers": "Andre servere",
"dialog.server.add.usernamePlaceholder": "brugernavn",
"dialog.server.add.passwordPlaceholder": "adgangskode",
"server.row.noUsername": "intet brugernavn",
"session.review.noVcs.createGit.title": "Opret et Git-repository",
"session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt",
"session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...",
+3
View File
@@ -327,6 +327,7 @@ export const dict = {
"dialog.server.add.button": "Server hinzufügen",
"dialog.server.add.name": "Servername (optional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Benutzername (optional)",
"dialog.server.add.password": "Passwort (optional)",
"dialog.server.edit.title": "Server bearbeiten",
"dialog.server.default.title": "Standardserver",
@@ -944,7 +945,9 @@ export const dict = {
"app.server.unreachable": "Konnte {{server}} nicht erreichen",
"app.server.retrying": "Verbindung wird automatisch erneut hergestellt…",
"app.server.otherServers": "Andere Server",
"dialog.server.add.usernamePlaceholder": "Benutzername",
"dialog.server.add.passwordPlaceholder": "Passwort",
"server.row.noUsername": "Kein Benutzername",
"session.review.noVcs.createGit.title": "Git-Repository erstellen",
"session.review.noVcs.createGit.description":
"Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen",
+3
View File
@@ -424,6 +424,8 @@ export const dict = {
"dialog.server.add.button": "ސަރވަރ އިތުރުކުރުން",
"dialog.server.add.name": "ސަރވަރ ނަން (އިޚްތިޔާރީ)",
"dialog.server.add.namePlaceholder": "Localhost އެވެ",
"dialog.server.add.username": "ޔޫޒަރނޭމް (އިޚްތިޔާރީ)",
"dialog.server.add.usernamePlaceholder": "ޔޫޒަރނޭމް",
"dialog.server.add.password": "ޕާސްވޯޑް (އިޚްތިޔާރީ)",
"dialog.server.add.passwordPlaceholder": "ޕާސްވަރޑް",
"dialog.server.edit.title": "އެޑިޓް ސަރވަރ",
@@ -509,6 +511,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ޑެސްކްޓޮޕް {{version}} އެވެ",
"wsl.onboarding.versionMismatch": "އިންސްޓޯލް ކޮށްފައިވާ ވަރޝަން ޑެސްކްޓޮޕް އެޕް ވަރޝަން އާއި އެއްގޮތެއް ނުވެއެވެ.",
"wsl.onboarding.adding": "އިތުރުކުރަނީ...",
"server.row.noUsername": "ޔޫޒަރނޭމް އެއް ނެތެވެ",
"dialog.project.edit.title": "އެޑިޓް ޕްރޮޖެކްޓް",
"dialog.project.edit.name": "ނަން",
"dialog.project.edit.icon": "އައިކޮން",
+3
View File
@@ -423,6 +423,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "སར་བར་ཁ་སྐོང་འབད།",
"dialog.server.add.name": "སར་བར་གྱི་མིང་ (གདམ་ཁ་ཅན།)",
"dialog.server.add.namePlaceholder": "Localhost།",
"dialog.server.add.username": "ལག་ལེན་པའི་མིང་ (གདམ་ཁ་ཅན་)།",
"dialog.server.add.usernamePlaceholder": "ལག་ལེན་པའི་མིང་།",
"dialog.server.add.password": "ཆོག་ཡིག་ (གདམ་ཁ་ཅན།)",
"dialog.server.add.passwordPlaceholder": "གསང༌ཨང",
"dialog.server.edit.title": "སར་བར་ཞུན་དག་འབད།",
@@ -508,6 +510,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ཌེཀསི་ཊོཔ་ {{version}}",
"wsl.onboarding.versionMismatch": "གཞི་བཙུགས་འབད་ཡོད་པའི་ཐོན་རིམ་འདི་ ཌེཀསི་ཊོཔ་ཨེཔ་ཐོན་རིམ་དང་མཐུན་སྒྲིག་མི་འབད།",
"wsl.onboarding.adding": "ཁ་སྐོང་བརྐྱབ་དོ།",
"server.row.noUsername": "སྤྱོད་མིང་མེད།",
"dialog.project.edit.title": "ཞུན་དག་ལས་གཞི།",
"dialog.project.edit.name": "མིང",
"dialog.project.edit.icon": "ངོས་དཔར།",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Προσθήκη διακομιστή",
"dialog.server.add.name": "Όνομα διακομιστή (προαιρετικό)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Όνομα χρήστη (προαιρετικό)",
"dialog.server.add.usernamePlaceholder": "όνομα χρήστη",
"dialog.server.add.password": "Κωδικός πρόσβασης (προαιρετικό)",
"dialog.server.add.passwordPlaceholder": "κωδικός πρόσβασης",
"dialog.server.edit.title": "Επεξεργασία διακομιστή",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "επιτραπέζιος υπολογιστής {{version}}",
"wsl.onboarding.versionMismatch": "Η εγκατεστημένη έκδοση δεν ταιριάζει με την έκδοση της εφαρμογής για υπολογιστές.",
"wsl.onboarding.adding": "Προσθήκη...",
"server.row.noUsername": "χωρίς όνομα χρήστη",
"dialog.project.edit.title": "Επεξεργασία έργου",
"dialog.project.edit.name": "Όνομα",
"dialog.project.edit.icon": "Εικονίδιο",
+5 -11
View File
@@ -363,7 +363,9 @@ export const dict = {
"dialog.server.add.button": "Add server",
"dialog.server.add.name": "Server name (optional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.password": "Password",
"dialog.server.add.username": "Username (optional)",
"dialog.server.add.usernamePlaceholder": "username",
"dialog.server.add.password": "Password (optional)",
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Edit server",
"dialog.server.default.title": "Default server",
@@ -451,6 +453,7 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Installed version does not match the desktop app version.",
"wsl.onboarding.adding": "Adding...",
"server.row.noUsername": "no username",
"server.row.incompatible":
"This server is running OpenCode {{version}}, which isn't compatible with this app. Upgrade it to OpenCode V2 to continue.",
@@ -643,7 +646,6 @@ export const dict = {
"home.empty.description": "Get started by opening a local project",
"home.title": "Home",
"home.projects": "Projects",
"home.projects.all": "All projects",
"home.project.add": "Add project",
"home.recentlyClosed": "Recently closed",
"home.server.collapse": "Collapse server projects",
@@ -660,10 +662,6 @@ export const dict = {
"home.providerTip": "Connect to 75+ providers to use other models, including Claude, GPT, Gemini, etc",
"session.tab.session": "Session",
"session.tab.files": "Files",
"session.files.openTabs": "Open files",
"session.tab.usage": "Usage",
"session.view.select": "Session view",
"session.tab.review": "Review",
"session.tab.context": "Context",
"session.tab.unknown": "Unknown Session",
@@ -702,7 +700,6 @@ export const dict = {
"session.review.change.one": "Change",
"session.review.change.other": "Changes",
"session.review.loadingChanges": "Loading changes...",
"session.review.wrapLines": "Wrap lines",
"session.review.empty": "No changes in this session yet",
"session.review.noVcs": "No Git Version Control System detected, changes not displayed",
"session.review.noVcs.createGit.title": "Create a Git repository",
@@ -805,7 +802,6 @@ export const dict = {
"terminal.connectTicket.statusError": "PTY connect ticket failed with {{status}}",
"titlebar.update": "Update",
"titlebar.tabs": "Tabs",
"titlebar.updateVersion": "Update {{version}}",
"common.closeTab": "Close tab",
@@ -983,9 +979,7 @@ export const dict = {
"settings.general.row.showProjectIcon.description": "Show the project icon in the session header",
"settings.general.row.mobileTitlebarBottom.title": "Bottom navigation",
"settings.general.row.mobileTitlebarBottom.description":
"Place the title bar at the bottom of the screen on mobile",
"settings.general.row.mobileDiffWrap.description":
"Wrap long lines in mobile diffs instead of scrolling horizontally",
"Place the title bar and session tabs at the bottom of the screen on mobile",
"settings.general.row.showCustomAgents.title": "Show agent",
"settings.general.row.showCustomAgents.description":
"Switch between agents in the composer. When hidden, defaults to Build agent.",
+3
View File
@@ -450,6 +450,7 @@ export const dict = {
"dialog.server.add.button": "Añadir servidor",
"dialog.server.add.name": "Nombre del servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nombre de usuario (opcional)",
"dialog.server.add.password": "Contraseña (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.default.title": "Servidor predeterminado",
@@ -1117,7 +1118,9 @@ export const dict = {
"app.server.unreachable": "No se pudo conectar con {{server}}",
"app.server.retrying": "Reintentando automáticamente...",
"app.server.otherServers": "Otros servidores",
"dialog.server.add.usernamePlaceholder": "usuario",
"dialog.server.add.passwordPlaceholder": "contraseña",
"server.row.noUsername": "sin usuario",
"session.review.noVcs.createGit.title": "Crear repositorio Git",
"session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto",
"session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Lisa server",
"dialog.server.add.name": "Serveri nimi (valikuline)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Kasutajanimi (valikuline)",
"dialog.server.add.usernamePlaceholder": "kasutajanimi",
"dialog.server.add.password": "Parool (valikuline)",
"dialog.server.add.passwordPlaceholder": "parool",
"dialog.server.edit.title": "Muuda serverit",
@@ -501,6 +503,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "töölaud {{version}}",
"wsl.onboarding.versionMismatch": "Installitud versioon ei ühti töölauarakenduse versiooniga.",
"wsl.onboarding.adding": "Lisamine...",
"server.row.noUsername": "kasutajanime pole",
"dialog.project.edit.title": "Redigeeri projekti",
"dialog.project.edit.name": "Nimi",
"dialog.project.edit.icon": "Ikoon",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "سرور اضافه کنید",
"dialog.server.add.name": "نام سرور (اختیاری)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "نام کاربری (اختیاری)",
"dialog.server.add.usernamePlaceholder": "نام کاربری",
"dialog.server.add.password": "رمز عبور (اختیاری)",
"dialog.server.add.passwordPlaceholder": "رمز عبور",
"dialog.server.edit.title": "ویرایش سرور",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "دسکتاپ {{version}}",
"wsl.onboarding.versionMismatch": "نسخه نصب شده با نسخه برنامه دسکتاپ مطابقت ندارد.",
"wsl.onboarding.adding": "در حال افزودن...",
"server.row.noUsername": "بدون نام کاربری",
"dialog.project.edit.title": "ویرایش پروژه",
"dialog.project.edit.name": "نام",
"dialog.project.edit.icon": "نماد",
+3
View File
@@ -325,6 +325,8 @@ export const dict = {
"dialog.server.add.button": "Lisää palvelin",
"dialog.server.add.name": "Palvelimen nimi (valinnainen)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Käyttäjätunnus (valinnainen)",
"dialog.server.add.usernamePlaceholder": "käyttäjätunnus",
"dialog.server.add.password": "Salasana (valinnainen)",
"dialog.server.add.passwordPlaceholder": "salasana",
"dialog.server.edit.title": "Muokkaa palvelinta",
@@ -410,6 +412,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "Desktop {{version}}",
"wsl.onboarding.versionMismatch": "Asennettu versio ei vastaa työpöytäsovelluksen versiota.",
"wsl.onboarding.adding": "Lisätään...",
"server.row.noUsername": "ei käyttäjätunnusta",
"dialog.project.edit.title": "Muokkaa projektia",
"dialog.project.edit.name": "Nimi",
"dialog.project.edit.icon": "Kuvake",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Legg ambætara til",
"dialog.server.add.name": "Ambætaranavn (valfrítt)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brúkaranavn (valfrítt)",
"dialog.server.add.usernamePlaceholder": "brúkaranavn",
"dialog.server.add.password": "Loyniorð (valfrítt)",
"dialog.server.add.passwordPlaceholder": "loyniorð",
"dialog.server.edit.title": "Rætta ambætara",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "skriviborð {{version}}",
"wsl.onboarding.versionMismatch": "Innsetta útgávan passar ikki til skriviborðsappiútgávuna.",
"wsl.onboarding.adding": "Leggi til...",
"server.row.noUsername": "einki brúkaranavn",
"dialog.project.edit.title": "Rætta verkætlan",
"dialog.project.edit.name": "Navn",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -426,6 +426,7 @@ export const dict = {
"dialog.server.add.button": "Ajouter un serveur",
"dialog.server.add.name": "Nom du serveur (optionnel)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nom d'utilisateur (optionnel)",
"dialog.server.add.password": "Mot de passe (optionnel)",
"dialog.server.edit.title": "Modifier le serveur",
"dialog.server.default.title": "Serveur par défaut",
@@ -1043,7 +1044,9 @@ export const dict = {
"app.server.unreachable": "Impossible de joindre {{server}}",
"app.server.retrying": "Nouvelle tentative automatique...",
"app.server.otherServers": "Autres serveurs",
"dialog.server.add.usernamePlaceholder": "nom d'utilisateur",
"dialog.server.add.passwordPlaceholder": "mot de passe",
"server.row.noUsername": "aucun nom d'utilisateur",
"session.review.noVcs.createGit.title": "Créer un dépôt Git",
"session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet",
"session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...",
+3
View File
@@ -416,6 +416,8 @@ export const dict = {
"dialog.server.add.button": "הוסף שרת",
"dialog.server.add.name": "שם שרת (אופציונלי)",
"dialog.server.add.namePlaceholder": "מארח מקומי",
"dialog.server.add.username": "שם משתמש (אופציונלי)",
"dialog.server.add.usernamePlaceholder": "שם משתמש",
"dialog.server.add.password": "סיסמה (אופציונלי)",
"dialog.server.add.passwordPlaceholder": "סיסמה",
"dialog.server.edit.title": "ערוך שרת",
@@ -499,6 +501,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "גרסת שולחן העבודה: {{version}}",
"wsl.onboarding.versionMismatch": "הגרסה המותקנת אינה תואמת את גרסת האפליקציה לשולחן העבודה.",
"wsl.onboarding.adding": "מוסיף...",
"server.row.noUsername": "ללא שם משתמש",
"dialog.project.edit.title": "ערוך פרויקט",
"dialog.project.edit.name": "שם",
"dialog.project.edit.icon": "סמל",
+3
View File
@@ -425,6 +425,8 @@ export const dict = {
"dialog.server.add.button": "सर्वर जोड़ें",
"dialog.server.add.name": "सर्वर नाम (वैकल्पिक)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "उपयोगकर्ता नाम (वैकल्पिक)",
"dialog.server.add.usernamePlaceholder": "उपयोक्तानाम",
"dialog.server.add.password": "पासवर्ड (वैकल्पिक)",
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्वर संपादित करें",
@@ -510,6 +512,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "डेस्कटॉप {{version}}",
"wsl.onboarding.versionMismatch": "इंस्टॉल किया गया संस्करण डेस्कटॉप ऐप संस्करण से मेल नहीं खाता।",
"wsl.onboarding.adding": "जोड़ा जा रहा है...",
"server.row.noUsername": "कोई उपयोगकर्ता नाम नहीं",
"dialog.project.edit.title": "प्रोजेक्ट संपादित करें",
"dialog.project.edit.name": "नाम",
"dialog.project.edit.icon": "आइकन",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "Dodaj poslužitelj",
"dialog.server.add.name": "Naziv poslužitelja (neobavezno)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "korisničko ime (nije obavezno)",
"dialog.server.add.usernamePlaceholder": "korisničko ime",
"dialog.server.add.password": "Lozinka (nije obavezno)",
"dialog.server.add.passwordPlaceholder": "lozinka",
"dialog.server.edit.title": "Uredi poslužitelj",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "radna površina {{version}}",
"wsl.onboarding.versionMismatch": "Instalirana verzija ne odgovara verziji aplikacije za stolna računala.",
"wsl.onboarding.adding": "Dodavanje...",
"server.row.noUsername": "nema korisničkog imena",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.name": "Ime",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "Szerver hozzáadása",
"dialog.server.add.name": "Szerver neve (nem kötelező)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Felhasználónév (nem kötelező)",
"dialog.server.add.usernamePlaceholder": "felhasználónév",
"dialog.server.add.password": "Jelszó (nem kötelező)",
"dialog.server.add.passwordPlaceholder": "jelszó",
"dialog.server.edit.title": "Szerver szerkesztése",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "asztali {{version}}",
"wsl.onboarding.versionMismatch": "A telepített verzió nem egyezik az asztali alkalmazás verziójával.",
"wsl.onboarding.adding": "Hozzáadás...",
"server.row.noUsername": "nincs felhasználónév",
"dialog.project.edit.title": "Projekt szerkesztése",
"dialog.project.edit.name": "Név",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Ավելացնել սերվեր",
"dialog.server.add.name": "Սերվերի անունը (ըստ ցանկության)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Օգտագործողի անուն (ըստ ցանկության)",
"dialog.server.add.usernamePlaceholder": "օգտվողի անուն",
"dialog.server.add.password": "Գաղտնաբառ (ըստ ցանկության)",
"dialog.server.add.passwordPlaceholder": "գաղտնաբառ",
"dialog.server.edit.title": "Խմբագրել սերվերը",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "աշխատասեղանի {{version}}",
"wsl.onboarding.versionMismatch": "Տեղադրված տարբերակը չի համապատասխանում աշխատասեղանի հավելվածի տարբերակին։",
"wsl.onboarding.adding": "Ավելացվում է...",
"server.row.noUsername": "առանց օգտվողի անուն",
"dialog.project.edit.title": "Խմբագրել նախագիծը",
"dialog.project.edit.name": "Անուն",
"dialog.project.edit.icon": "Պատկերակ",
+3
View File
@@ -453,6 +453,8 @@ export const dict = {
"dialog.server.add.button": "Tambah server",
"dialog.server.add.name": "Nama server (opsional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nama pengguna (opsional)",
"dialog.server.add.usernamePlaceholder": "nama pengguna",
"dialog.server.add.password": "Kata sandi (opsional)",
"dialog.server.add.passwordPlaceholder": "kata sandi",
"dialog.server.edit.title": "Sunting server",
@@ -540,6 +542,7 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Versi terinstal tidak cocok dengan versi aplikasi desktop.",
"wsl.onboarding.adding": "Menambahkan...",
"server.row.noUsername": "tanpa nama pengguna",
"dialog.project.edit.title": "Sunting proyek",
"dialog.project.edit.name": "Nama",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "Bæta við netþjóni",
"dialog.server.add.name": "Nafn netþjóns (valfrjálst)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Notandanafn (valfrjálst)",
"dialog.server.add.usernamePlaceholder": "notendanafn",
"dialog.server.add.password": "Lykilorð (valfrjálst)",
"dialog.server.add.passwordPlaceholder": "lykilorð",
"dialog.server.edit.title": "Breyta miðlara",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "skrifborð {{version}}",
"wsl.onboarding.versionMismatch": "Uppsett útgáfa passar ekki við útgáfu skrifborðsforritsins.",
"wsl.onboarding.adding": "Bætir við...",
"server.row.noUsername": "ekkert notendanafn",
"dialog.project.edit.title": "Breyta verkefni",
"dialog.project.edit.name": "Nafn",
"dialog.project.edit.icon": "Táknmynd",
+3
View File
@@ -327,6 +327,8 @@ export const dict = {
"dialog.server.add.button": "Aggiungi server",
"dialog.server.add.name": "Nome del server (facoltativo)",
"dialog.server.add.namePlaceholder": "Host locale",
"dialog.server.add.username": "Nome utente (facoltativo)",
"dialog.server.add.usernamePlaceholder": "nomeutente",
"dialog.server.add.password": "Password (facoltativa)",
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Modifica server",
@@ -412,6 +414,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "La versione installata non corrisponde alla versione dell'app desktop.",
"wsl.onboarding.adding": "Aggiunta...",
"server.row.noUsername": "nessun nome utente",
"dialog.project.edit.title": "Modifica progetto",
"dialog.project.edit.name": "Nome",
"dialog.project.edit.icon": "Icona",
+3
View File
@@ -419,6 +419,7 @@ export const dict = {
"dialog.server.add.button": "サーバーを追加",
"dialog.server.add.name": "サーバー名 (オプション)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ユーザー名 (オプション)",
"dialog.server.add.password": "パスワード (オプション)",
"dialog.server.edit.title": "サーバーを編集",
"dialog.server.default.title": "デフォルトサーバー",
@@ -1018,7 +1019,9 @@ export const dict = {
"app.server.unreachable": "{{server}} に到達できませんでした",
"app.server.retrying": "自動的に再試行中...",
"app.server.otherServers": "その他のサーバー",
"dialog.server.add.usernamePlaceholder": "ユーザー名",
"dialog.server.add.passwordPlaceholder": "パスワード",
"server.row.noUsername": "ユーザー名なし",
"session.review.noVcs.createGit.title": "Git リポジトリを作成",
"session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す",
"session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "სერვერის დამატება",
"dialog.server.add.name": "სერვერის სახელი (არასავალდებულო)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "მომხმარებლის სახელი (არასავალდებულო)",
"dialog.server.add.usernamePlaceholder": "მომხმარებლის სახელი",
"dialog.server.add.password": "პაროლი (არასავალდებულო)",
"dialog.server.add.passwordPlaceholder": "პაროლი",
"dialog.server.edit.title": "სერვერის რედაქტირება",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "დესკტოპის {{version}}",
"wsl.onboarding.versionMismatch": "დაინსტალირებული ვერსია არ ემთხვევა დესკტოპის აპის ვერსიას.",
"wsl.onboarding.adding": "დამატება...",
"server.row.noUsername": "მომხმარებლის სახელის გარეშე",
"dialog.project.edit.title": "პროექტის რედაქტირება",
"dialog.project.edit.name": "სახელი",
"dialog.project.edit.icon": "ხატულა",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "បន្ថែមម៉ាស៊ីនមេ",
"dialog.server.add.name": "ឈ្មោះម៉ាស៊ីនមេ (ជាជម្រើស)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ឈ្មោះអ្នកប្រើប្រាស់ (ជាជម្រើស)",
"dialog.server.add.usernamePlaceholder": "ឈ្មោះអ្នកប្រើ",
"dialog.server.add.password": "ពាក្យសម្ងាត់ (ជាជម្រើស)\nពាក្យសម្ងាត់",
"dialog.server.add.passwordPlaceholder": "ពាក្យសម្ងាត់",
"dialog.server.edit.title": "កែសម្រួលម៉ាស៊ីនមេ",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ផ្ទៃតុ {{version}}",
"wsl.onboarding.versionMismatch": "កំណែដែលបានដំឡើងមិនត្រូវគ្នានឹងកំណែកម្មវិធីកុំព្យូទ័រលើតុទេ។",
"wsl.onboarding.adding": "កំពុងបន្ថែម...",
"server.row.noUsername": "គ្មានឈ្មោះអ្នកប្រើប្រាស់",
"dialog.project.edit.title": "កែសម្រួលគម្រោង",
"dialog.project.edit.name": "ឈ្មោះ",
"dialog.project.edit.icon": "រូបតំណាង",
+3
View File
@@ -308,6 +308,7 @@ export const dict = {
"dialog.server.add.button": "서버 추가",
"dialog.server.add.name": "서버 이름 (선택 사항)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "사용자 이름 (선택 사항)",
"dialog.server.add.password": "비밀번호 (선택 사항)",
"dialog.server.edit.title": "서버 편집",
"dialog.server.default.title": "기본 서버",
@@ -767,7 +768,9 @@ export const dict = {
"app.server.unreachable": "{{server}}에 연결할 수 없습니다",
"app.server.retrying": "자동으로 재시도 중...",
"app.server.otherServers": "다른 서버",
"dialog.server.add.usernamePlaceholder": "사용자 이름",
"dialog.server.add.passwordPlaceholder": "비밀번호",
"server.row.noUsername": "사용자 이름 없음",
"session.review.noVcs.createGit.title": "Git 저장소 생성",
"session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소",
"session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "ເພີ່ມເຊີບເວີ",
"dialog.server.add.name": "ຊື່ເຊີບເວີ (ທາງເລືອກ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ຊື່ຜູ້ໃຊ້ (ທາງເລືອກ)",
"dialog.server.add.usernamePlaceholder": "ຊື່ຜູ້ໃຊ້",
"dialog.server.add.password": "ລະຫັດຜ່ານ (ທາງເລືອກ)",
"dialog.server.add.passwordPlaceholder": "ລະຫັດຜ່ານ",
"dialog.server.edit.title": "ແກ້ໄຂເຊີບເວີ",
@@ -501,6 +503,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ເດັສທັອບ {{version}}",
"wsl.onboarding.versionMismatch": "ເວີຊັນທີ່ຕິດຕັ້ງບໍ່ກົງກັບເວີຊັນຂອງແອັບ desktop.",
"wsl.onboarding.adding": "ກຳລັງເພີ່ມ...",
"server.row.noUsername": "ບໍ່ມີຊື່ຜູ້ໃຊ້",
"dialog.project.edit.title": "ແກ້ໄຂໂຄງການ",
"dialog.project.edit.name": "ຊື່",
"dialog.project.edit.icon": "ໄອຄອນ",
+3
View File
@@ -423,6 +423,8 @@ export const dict = {
"dialog.server.add.button": "Pridėti serverį",
"dialog.server.add.name": "Serverio pavadinimas (neprivaloma)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Vartotojo vardas (neprivaloma)",
"dialog.server.add.usernamePlaceholder": "vartotojo vardas",
"dialog.server.add.password": "Slaptažodis (neprivaloma)",
"dialog.server.add.passwordPlaceholder": "slaptažodis",
"dialog.server.edit.title": "Redaguoti serverį",
@@ -508,6 +510,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "darbalaukis {{version}}",
"wsl.onboarding.versionMismatch": "Įdiegta versija neatitinka darbalaukio programos versijos.",
"wsl.onboarding.adding": "Pridedama...",
"server.row.noUsername": "jokio vartotojo vardo",
"dialog.project.edit.title": "Redaguoti projektą",
"dialog.project.edit.name": "Vardas",
"dialog.project.edit.icon": "Piktograma",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Pievienot serveri",
"dialog.server.add.name": "Servera nosaukums (nav obligāti)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Lietotājvārds (nav obligāti)",
"dialog.server.add.usernamePlaceholder": "lietotājvārds",
"dialog.server.add.password": "Parole (nav obligāti)",
"dialog.server.add.passwordPlaceholder": "parole",
"dialog.server.edit.title": "Rediģēt serveri",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "darbvirsmas {{version}}",
"wsl.onboarding.versionMismatch": "Instalētā versija neatbilst darbvirsmas lietotnes versijai.",
"wsl.onboarding.adding": "Pievieno...",
"server.row.noUsername": "nav lietotājvārda",
"dialog.project.edit.title": "Rediģēt projektu",
"dialog.project.edit.name": "Nosaukums",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Додај сервер",
"dialog.server.add.name": "Име на сервер (изборно)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Корисничко име (изборно)",
"dialog.server.add.usernamePlaceholder": "корисничко име",
"dialog.server.add.password": "Лозинка (изборно)",
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
@@ -504,6 +506,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "работна површина {{version}}",
"wsl.onboarding.versionMismatch": "Инсталираната верзија не се совпаѓа со верзијата на апликацијата за десктоп.",
"wsl.onboarding.adding": "Се додава...",
"server.row.noUsername": "нема корисничко име",
"dialog.project.edit.title": "Уреди проект",
"dialog.project.edit.name": "Име",
"dialog.project.edit.icon": "Икона",
+3
View File
@@ -421,6 +421,8 @@ export const dict = {
"dialog.server.add.button": "Сервер нэмэх",
"dialog.server.add.name": "Серверийн нэр (заавал биш)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Хэрэглэгчийн нэр (заавал биш)",
"dialog.server.add.usernamePlaceholder": "хэрэглэгчийн нэр",
"dialog.server.add.password": "Нууц үг (заавал биш)",
"dialog.server.add.passwordPlaceholder": "нууц үг",
"dialog.server.edit.title": "Сервер засах",
@@ -506,6 +508,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ширээний компьютер {{version}}",
"wsl.onboarding.versionMismatch": "Суулгасан хувилбар нь ширээний програмын хувилбартай таарахгүй байна.",
"wsl.onboarding.adding": "Нэмэж байна...",
"server.row.noUsername": "хэрэглэгчийн нэр байхгүй",
"dialog.project.edit.title": "Төслийг засварлах",
"dialog.project.edit.name": "Нэр",
"dialog.project.edit.icon": "Дүрс",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Tambah pelayan",
"dialog.server.add.name": "Nama pelayan (pilihan)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nama pengguna (pilihan)",
"dialog.server.add.usernamePlaceholder": "nama pengguna",
"dialog.server.add.password": "Kata laluan (pilihan)",
"dialog.server.add.passwordPlaceholder": "kata laluan",
"dialog.server.edit.title": "Edit pelayan",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versi yang dipasang tidak sepadan dengan versi aplikasi desktop.",
"wsl.onboarding.adding": "Sedang menambah...",
"server.row.noUsername": "tiada nama pengguna",
"dialog.project.edit.title": "Edit projek",
"dialog.project.edit.name": "Nama",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "ဆာဗာထည့်ပါ။",
"dialog.server.add.name": "ဆာဗာအမည် (ချန်လှပ်ထားနိုင်သည်)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "အသုံးပြုသူအမည် (ချန်လှပ်ထားနိုင်သည်)",
"dialog.server.add.usernamePlaceholder": "အသုံးပြုသူအမည်",
"dialog.server.add.password": "စကားဝှက် (ချန်လှပ်ထားနိုင်သည်)",
"dialog.server.add.passwordPlaceholder": "စကားဝှက်",
"dialog.server.edit.title": "ဆာဗာကို တည်းဖြတ်ပါ။",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ဒက်စ်တော့ {{version}}",
"wsl.onboarding.versionMismatch": "ထည့်သွင်းထားသောဗားရှင်းသည် ဒက်စ်တော့အက်ပ်ဗားရှင်းနှင့် မကိုက်ညီပါ။",
"wsl.onboarding.adding": "ထည့်နေသည်...",
"server.row.noUsername": "အသုံးပြုသူအမည်မရှိပါ။",
"dialog.project.edit.title": "ပရောဂျက်ကို တည်းဖြတ်ပါ။",
"dialog.project.edit.name": "အမည်",
"dialog.project.edit.icon": "သင်္ကေတ",
+3
View File
@@ -419,6 +419,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "सर्भर थप्नुहोस्",
"dialog.server.add.name": "सर्भर नाम (वैकल्पिक)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "प्रयोगकर्ता नाम (वैकल्पिक)",
"dialog.server.add.usernamePlaceholder": "प्रयोगकर्ता नाम",
"dialog.server.add.password": "पासवर्ड (वैकल्पिक)",
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्भर सम्पादन गर्नुहोस्",
@@ -504,6 +506,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "डेस्कटप {{version}}",
"wsl.onboarding.versionMismatch": "स्थापना गरिएको संस्करण डेस्कटप एप संस्करणसँग मेल खाँदैन।",
"wsl.onboarding.adding": "थप्दै...",
"server.row.noUsername": "प्रयोगकर्ता नाम छैन",
"dialog.project.edit.title": "परियोजना सम्पादन गर्नुहोस्",
"dialog.project.edit.name": "नाम",
"dialog.project.edit.icon": "आइकन",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Server toevoegen",
"dialog.server.add.name": "Servernaam (optioneel)",
"dialog.server.add.namePlaceholder": "Lokale host",
"dialog.server.add.username": "Gebruikersnaam (optioneel)",
"dialog.server.add.usernamePlaceholder": "gebruikersnaam",
"dialog.server.add.password": "Wachtwoord (optioneel)",
"dialog.server.add.passwordPlaceholder": "wachtwoord",
"dialog.server.edit.title": "Server bewerken",
@@ -504,6 +506,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "De geïnstalleerde versie komt niet overeen met de versie van de desktop-app.",
"wsl.onboarding.adding": "Toevoegen...",
"server.row.noUsername": "geen gebruikersnaam",
"dialog.project.edit.title": "Project bewerken",
"dialog.project.edit.name": "Naam",
"dialog.project.edit.icon": "Pictogram",
+3
View File
@@ -439,6 +439,7 @@ export const dict = {
"dialog.server.add.button": "Legg til server",
"dialog.server.add.name": "Servernavn (valgfritt)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brukernavn (valgfritt)",
"dialog.server.add.password": "Passord (valgfritt)",
"dialog.server.edit.title": "Rediger server",
"dialog.server.default.title": "Standardserver",
@@ -962,7 +963,9 @@ export const dict = {
"app.server.unreachable": "Kunne ikke nå {{server}}",
"app.server.retrying": "Prøver på nytt automatisk...",
"app.server.otherServers": "Andre servere",
"dialog.server.add.usernamePlaceholder": "brukernavn",
"dialog.server.add.passwordPlaceholder": "passord",
"server.row.noUsername": "ikke noe brukernavn",
"session.review.noVcs.createGit.title": "Opprett et Git-depot",
"session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet",
"session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...",
+3
View File
@@ -425,6 +425,8 @@ export const dict = {
"dialog.server.add.button": "سرور شامل کرو",
"dialog.server.add.name": "سرور دا ناں (اختیاری)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ورتن آلا ناں (اختیاری)",
"dialog.server.add.usernamePlaceholder": "ورتن آلا ناں",
"dialog.server.add.password": "پاس ورڈ (اختیاری)",
"dialog.server.add.passwordPlaceholder": "پاس ورڈ",
"dialog.server.edit.title": "سرور وچ ترمیم کرو",
@@ -510,6 +512,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ڈیسک ٹاپ {{version}}",
"wsl.onboarding.versionMismatch": "انسٹال کیتا گیا ورژن ڈیسک ٹاپ ایپ ورژن نال میل نئیں کھاندا۔",
"wsl.onboarding.adding": "شامل کر رہیا واں...",
"server.row.noUsername": "کوئی ورتن آلا ناں نئیں",
"dialog.project.edit.title": "پروجیکٹ وچ ترمیم کرو",
"dialog.project.edit.name": "ناں",
"dialog.project.edit.icon": "آئکن",
+3
View File
@@ -422,6 +422,7 @@ export const dict = {
"dialog.server.add.button": "Dodaj serwer",
"dialog.server.add.name": "Nazwa serwera (opcjonalnie)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nazwa użytkownika (opcjonalnie)",
"dialog.server.add.password": "Hasło (opcjonalnie)",
"dialog.server.edit.title": "Edytuj serwer",
"dialog.server.default.title": "Domyślny serwer",
@@ -1035,7 +1036,9 @@ export const dict = {
"app.server.unreachable": "Nie można połączyć z {{server}}",
"app.server.retrying": "Ponawianie automatycznie...",
"app.server.otherServers": "Inne serwery",
"dialog.server.add.usernamePlaceholder": "nazwa użytkownika",
"dialog.server.add.passwordPlaceholder": "hasło",
"server.row.noUsername": "brak nazwy użytkownika",
"session.review.noVcs.createGit.title": "Utwórz repozytorium Git",
"session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie",
"session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Adaugă server",
"dialog.server.add.name": "Nume server (opțional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Utilizator (opțional)",
"dialog.server.add.usernamePlaceholder": "utilizator",
"dialog.server.add.password": "Parolă (opțional)",
"dialog.server.add.passwordPlaceholder": "parolă",
"dialog.server.edit.title": "Editează server",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versiunea instalată nu corespunde cu versiunea aplicației desktop.",
"wsl.onboarding.adding": "Se adaugă...",
"server.row.noUsername": "fără nume de utilizator",
"dialog.project.edit.title": "Editează proiectul",
"dialog.project.edit.name": "Nume",
"dialog.project.edit.icon": "Pictogramă",
+3
View File
@@ -447,6 +447,7 @@ export const dict = {
"dialog.server.add.button": "Добавить сервер",
"dialog.server.add.name": "Имя сервера (необязательно)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Имя пользователя (необязательно)",
"dialog.server.add.password": "Пароль (необязательно)",
"dialog.server.edit.title": "Редактировать сервер",
"dialog.server.default.title": "Сервер по умолчанию",
@@ -1112,7 +1113,9 @@ export const dict = {
"app.server.unreachable": "Не удалось связаться с {{server}}",
"app.server.retrying": "Автоматическая повторная попытка...",
"app.server.otherServers": "Другие серверы",
"dialog.server.add.usernamePlaceholder": "имя пользователя",
"dialog.server.add.passwordPlaceholder": "пароль",
"server.row.noUsername": "нет имени пользователя",
"session.review.noVcs.createGit.title": "Создать репозиторий Git",
"session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте",
"session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...",
+3
View File
@@ -417,6 +417,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "සේවාදායකය එක් කරන්න",
"dialog.server.add.name": "සේවාදායකයේ නම (විකල්ප)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "පරිශීලක නාමය (විකල්ප)",
"dialog.server.add.usernamePlaceholder": "පරිශීලක නාමය",
"dialog.server.add.password": "මුරපදය (විකල්ප)",
"dialog.server.add.passwordPlaceholder": "මුරපදය",
"dialog.server.edit.title": "සේවාදායකය සංස්කරණය කරන්න",
@@ -502,6 +504,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ඩෙස්ක්ටොප් {{version}}",
"wsl.onboarding.versionMismatch": "ස්ථාපිත අනුවාදය ඩෙස්ක්ටොප් යෙදුම් අනුවාදයට නොගැලපේ.",
"wsl.onboarding.adding": "එකතු කරමින්...",
"server.row.noUsername": "පරිශීලක නාමයක් නැත",
"dialog.project.edit.title": "ව්‍යාපෘතිය සංස්කරණය කරන්න",
"dialog.project.edit.name": "නම",
"dialog.project.edit.icon": "නිරූපකය",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Pridať server",
"dialog.server.add.name": "Názov servera (voliteľné)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Používateľské meno (voliteľné)",
"dialog.server.add.usernamePlaceholder": "používateľ",
"dialog.server.add.password": "Heslo (voliteľné)",
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upraviť server",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Nainštalovaná verzia sa nezhoduje s verziou desktopovej aplikácie.",
"wsl.onboarding.adding": "Pridáva sa...",
"server.row.noUsername": "bez používateľského mena",
"dialog.project.edit.title": "Upraviť projekt",
"dialog.project.edit.name": "Názov",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Dodaj strežnik",
"dialog.server.add.name": "Ime strežnika (neobvezno)",
"dialog.server.add.namePlaceholder": "Lokalni gostitelj",
"dialog.server.add.username": "uporabniško ime (neobvezno)",
"dialog.server.add.usernamePlaceholder": "uporabniško ime",
"dialog.server.add.password": "Geslo (neobvezno)",
"dialog.server.add.passwordPlaceholder": "geslo",
"dialog.server.edit.title": "Uredi strežnik",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "namizje {{version}}",
"wsl.onboarding.versionMismatch": "Nameščena različica se ne ujema z različico namizne aplikacije.",
"wsl.onboarding.adding": "Dodajanje ...",
"server.row.noUsername": "brez uporabniškega imena",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.name": "Ime",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Shto server",
"dialog.server.add.name": "Emri i serverit (opsionale)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Emri i përdoruesit (opsionale)",
"dialog.server.add.usernamePlaceholder": "emri i përdoruesit",
"dialog.server.add.password": "Fjalëkalimi (opsionale)",
"dialog.server.add.passwordPlaceholder": "fjalëkalimin",
"dialog.server.edit.title": "Redakto serverin",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versioni i instaluar nuk përputhet me versionin e aplikacionit desktop.",
"wsl.onboarding.adding": "Po shtohet...",
"server.row.noUsername": "asnjë emër përdoruesi",
"dialog.project.edit.title": "Redakto projektin",
"dialog.project.edit.name": "Emri",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Додај сервер",
"dialog.server.add.name": "Име сервера (опционо)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Корисничко име (опционално)",
"dialog.server.add.usernamePlaceholder": "корисничко име",
"dialog.server.add.password": "Лозинка (опционо)",
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "десктоп {{version}}",
"wsl.onboarding.versionMismatch": "Инсталирана верзија не одговара верзији апликације за рачунар.",
"wsl.onboarding.adding": "Додавање...",
"server.row.noUsername": "нема корисничког имена",
"dialog.project.edit.title": "Уреди пројекат",
"dialog.project.edit.name": "Назив",
"dialog.project.edit.icon": "Икона",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Lägg till server",
"dialog.server.add.name": "Servernamn (valfritt)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Användarnamn (valfritt)",
"dialog.server.add.usernamePlaceholder": "användarnamn",
"dialog.server.add.password": "Lösenord (valfritt)",
"dialog.server.add.passwordPlaceholder": "lösenord",
"dialog.server.edit.title": "Redigera server",
@@ -504,6 +506,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Den installerade versionen matchar inte versionen av skrivbordsappen.",
"wsl.onboarding.adding": "Lägger till...",
"server.row.noUsername": "inget användarnamn",
"dialog.project.edit.title": "Redigera projekt",
"dialog.project.edit.name": "Namn",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Илова кардани сервер",
"dialog.server.add.name": "Номи сервер (ихтиёрӣ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Номи корбар (ихтиёрӣ)",
"dialog.server.add.usernamePlaceholder": "номи корбар",
"dialog.server.add.password": "Рамз (ихтиёрӣ)",
"dialog.server.add.passwordPlaceholder": "парол",
"dialog.server.edit.title": "Серверро таҳрир кунед",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "мизи корӣ {{version}}",
"wsl.onboarding.versionMismatch": "Версияи насбшуда ба версияи барномаи мизи корӣ мувофиқат намекунад.",
"wsl.onboarding.adding": "Илова кардан...",
"server.row.noUsername": "номи корбар нест",
"dialog.project.edit.title": "Таҳрири лоиҳа",
"dialog.project.edit.name": "Ном",
"dialog.project.edit.icon": "Нишона",
+3
View File
@@ -446,6 +446,7 @@ export const dict = {
"dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์",
"dialog.server.add.name": "ชื่อเซิร์ฟเวอร์ (ไม่บังคับ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ชื่อผู้ใช้ (ไม่บังคับ)",
"dialog.server.add.password": "รหัสผ่าน (ไม่บังคับ)",
"dialog.server.edit.title": "แก้ไขเซิร์ฟเวอร์",
"dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น",
@@ -1096,7 +1097,9 @@ export const dict = {
"app.server.unreachable": "ไม่สามารถติดต่อ {{server}}",
"app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...",
"app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ",
"dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้",
"dialog.server.add.passwordPlaceholder": "รหัสผ่าน",
"server.row.noUsername": "ไม่มีชื่อผู้ใช้",
"session.review.noVcs.createGit.title": "สร้าง Git รีโพซิทอรี",
"session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำการเปลี่ยนแปลงในโปรเจกต์นี้",
"session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git รีโพซิทอรี...",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Serwer goşuň",
"dialog.server.add.name": "Serweriň ady (islege görä)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Ulanyjy ady (islege görä)",
"dialog.server.add.usernamePlaceholder": "ulanyjy ady",
"dialog.server.add.password": "Parol (islege görä)",
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serweri redaktirläň",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "iş stoly {{version}}",
"wsl.onboarding.versionMismatch": "Gurlan wersiýa iş stoly programma wersiýasyna gabat gelenok.",
"wsl.onboarding.adding": "Goşmak ...",
"server.row.noUsername": "ulanyjy ady ýok",
"dialog.project.edit.title": "Taslamany redaktirläň",
"dialog.project.edit.name": "Ady",
"dialog.project.edit.icon": "Nyşan",
+3
View File
@@ -453,6 +453,7 @@ export const dict = {
"dialog.server.add.button": "Sunucu ekle",
"dialog.server.add.name": "Sunucu adı (isteğe bağlı)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Kullanıcı adı (isteğe bağlı)",
"dialog.server.add.password": "Parola (isteğe bağlı)",
"dialog.server.edit.title": "Sunucuyu düzenle",
"dialog.server.default.title": "Varsayılan sunucu",
@@ -1119,7 +1120,9 @@ export const dict = {
"app.server.unreachable": "{{server}} sunucusuna ulaşılamadı",
"app.server.retrying": "Otomatik olarak tekrar deneniyor...",
"app.server.otherServers": "Diğer sunucular",
"dialog.server.add.usernamePlaceholder": "kullanıcı adı",
"dialog.server.add.passwordPlaceholder": "parola",
"server.row.noUsername": "kullanıcı adı yok",
"session.review.noVcs.createGit.title": "Git deposu oluştur",
"session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al",
"session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...",
+3
View File
@@ -454,6 +454,8 @@ export const dict = {
"dialog.server.add.button": "Додати сервер",
"dialog.server.add.name": "Назва сервера (необов'язково)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Ім'я користувача (необов'язково)",
"dialog.server.add.usernamePlaceholder": "ім'я користувача",
"dialog.server.add.password": "Пароль (необов'язково)",
"dialog.server.add.passwordPlaceholder": "пароль",
"dialog.server.edit.title": "Редагувати сервер",
@@ -541,6 +543,7 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Встановлена версія не відповідає версії десктопного застосунку.",
"wsl.onboarding.adding": "Додавання...",
"server.row.noUsername": "без імені користувача",
"dialog.project.edit.title": "Редагувати проєкт",
"dialog.project.edit.name": "Назва",
+3
View File
@@ -427,6 +427,8 @@ export const dict = {
"dialog.server.add.button": "سرور شامل کریں۔",
"dialog.server.add.name": "سرور کا نام (اختیاری)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "صارف نام (اختیاری)",
"dialog.server.add.usernamePlaceholder": "صارف نام",
"dialog.server.add.password": "پاس ورڈ (اختیاری)",
"dialog.server.add.passwordPlaceholder": "پاس ورڈ",
"dialog.server.edit.title": "سرور میں ترمیم کریں۔",
@@ -512,6 +514,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ڈیسک ٹاپ {{version}}",
"wsl.onboarding.versionMismatch": "انسٹال کردہ ورژن ڈیسک ٹاپ ایپ ورژن سے مماثل نہیں ہے۔",
"wsl.onboarding.adding": "شامل کیا جا رہا ہے...",
"server.row.noUsername": "کوئی صارف نام نہیں",
"dialog.project.edit.title": "پروجیکٹ میں ترمیم کریں۔",
"dialog.project.edit.name": "نام",
"dialog.project.edit.icon": "آئیکن",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Server qo'shish",
"dialog.server.add.name": "Server nomi (ixtiyoriy)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Foydalanuvchi nomi (ixtiyoriy)",
"dialog.server.add.usernamePlaceholder": "foydalanuvchi nomi",
"dialog.server.add.password": "Parol (ixtiyoriy)",
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serverni tahrirlash",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ish stoli {{version}}",
"wsl.onboarding.versionMismatch": "Oʻrnatilgan versiya ish stoli ilovasi versiyasiga mos kelmaydi.",
"wsl.onboarding.adding": "Qo'shilmoqda...",
"server.row.noUsername": "foydalanuvchi nomi yo'q",
"dialog.project.edit.title": "Loyihani tahrirlash",
"dialog.project.edit.name": "Ism",
"dialog.project.edit.icon": "Belgi",
+3
View File
@@ -425,6 +425,8 @@ export const dict = {
"dialog.server.add.button": "Thêm máy chủ",
"dialog.server.add.name": "Tên máy chủ (tùy chọn)",
"dialog.server.add.namePlaceholder": "Máy chủ cục bộ",
"dialog.server.add.username": "Tên người dùng (tùy chọn)",
"dialog.server.add.usernamePlaceholder": "tên người dùng",
"dialog.server.add.password": "Mật khẩu (tùy chọn)",
"dialog.server.add.passwordPlaceholder": "mật khẩu",
"dialog.server.edit.title": "Chỉnh sửa máy chủ",
@@ -509,6 +511,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "Desktop {{version}}",
"wsl.onboarding.versionMismatch": "Phiên bản đã cài đặt không khớp với phiên bản ứng dụng desktop.",
"wsl.onboarding.adding": "Đang thêm...",
"server.row.noUsername": "không có tên người dùng",
"dialog.project.edit.title": "Chỉnh sửa dự án",
"dialog.project.edit.name": "Tên",
"dialog.project.edit.icon": "Biểu tượng",
+3
View File
@@ -466,6 +466,7 @@ export const dict = {
"dialog.server.add.button": "添加服务器",
"dialog.server.add.name": "服务器名称(可选)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "用户名(可选)",
"dialog.server.add.password": "密码(可选)",
"dialog.server.edit.title": "编辑服务器",
"dialog.server.default.title": "默认服务器",
@@ -1088,7 +1089,9 @@ export const dict = {
"app.server.unreachable": "无法连接到 {{server}}",
"app.server.retrying": "正在自动重试...",
"app.server.otherServers": "其他服务器",
"dialog.server.add.usernamePlaceholder": "用户名",
"dialog.server.add.passwordPlaceholder": "密码",
"server.row.noUsername": "无用户名",
"session.review.noVcs.createGit.title": "创建 Git 仓库",
"session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改",
"session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...",
+3
View File
@@ -446,6 +446,7 @@ export const dict = {
"dialog.server.add.button": "新增伺服器",
"dialog.server.add.name": "伺服器名稱(選填)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "使用者名稱(選填)",
"dialog.server.add.password": "密碼(選填)",
"dialog.server.edit.title": "編輯伺服器",
"dialog.server.default.title": "預設伺服器",
@@ -1084,7 +1085,9 @@ export const dict = {
"app.server.unreachable": "無法連線至 {{server}}",
"app.server.retrying": "正在自動重試...",
"app.server.otherServers": "其他伺服器",
"dialog.server.add.usernamePlaceholder": "使用者名稱",
"dialog.server.add.passwordPlaceholder": "密碼",
"server.row.noUsername": "無使用者名稱",
"session.review.noVcs.createGit.title": "建立 Git 儲存庫",
"session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更",
"session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...",

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