mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: connect resumed native terminals reliably (#107688)
* fix: render resumed paired-node terminals * fix(control-ui): connect native catalog terminals reliably * chore: keep release notes in PR body * fix(control-ui): require terminal reconnect cancellation * fix(gateway): cancel timed-out terminal opens safely * fix(control-ui): recover stalled terminal opens * chore(ui): refresh i18n fallback baseline * fix: preserve login shell path for resumed terminals * refactor(plugin-sdk): avoid extra resolver export * fix(gateway): normalize terminal open failures * test(control-ui): split terminal readiness coverage * docs: explain catalog terminal PATH restoration * fix(control-ui): complete terminal readiness release gates * fix(ci): remove unused shell resolver exports * chore(plugin-sdk): refresh API baseline after rebase
This commit is contained in:
committed by
GitHub
parent
576c209110
commit
57d926b4fd
@@ -79,7 +79,7 @@ struct MacNodeHostWorkerTests {
|
||||
test "$OPENCLAW_NODE_EXEC_HOST" = app || exit 42
|
||||
test "$OPENCLAW_NODE_EXEC_FALLBACK" = 0 || exit 43
|
||||
printf '%s\\n' '{"type":"ready","version":"test","manifest":{"caps":["system"],"commands":["system.run"],"pathEnv":"/usr/bin:/bin"},"inventory":{"skills":null,"pluginTools":[]}}'
|
||||
printf '%s\\n' '{"type":"gateway-request","id":"gateway-1","method":"skills.bins","params":{},"timeoutMs":1000}'
|
||||
printf '%s\\n' '{"type":"gateway-request","id":"gateway-1","method":"node.invoke.progress","params":{"invokeId":"terminal-1","nodeId":"node-1","seq":0,"chunk":"hello"},"timeoutMs":1000}'
|
||||
IFS= read -r unavailable
|
||||
printf '%s' "$unavailable" | grep -q '"type":"gateway-response"' || exit 44
|
||||
printf '%s' "$unavailable" | grep -q '"ok":false' || exit 45
|
||||
|
||||
@@ -1536,13 +1536,7 @@ extension GatewayNodeSession {
|
||||
NSLocalizedDescriptionKey: "paramsJSON not UTF-8",
|
||||
])
|
||||
}
|
||||
let raw = try JSONSerialization.jsonObject(with: data)
|
||||
guard let dict = raw as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
return dict.reduce(into: [:]) { acc, entry in
|
||||
acc[entry.key] = AnyCodable(entry.value)
|
||||
}
|
||||
return try JSONDecoder().decode([String: AnyCodable].self, from: data)
|
||||
}
|
||||
|
||||
private func broadcastServerEvent(_ evt: EventFrame) {
|
||||
|
||||
@@ -604,6 +604,51 @@ private func nodeInvokePush(id: String, command: String) -> GatewayPush {
|
||||
|
||||
@Suite(.serialized)
|
||||
struct GatewayNodeSessionTests {
|
||||
@Test func `node requests preserve numeric JSON params`() async throws {
|
||||
let session = FakeGatewayWebSocketSession()
|
||||
let gateway = GatewayNodeSession()
|
||||
let options = GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: ["terminal"],
|
||||
commands: ["codex.terminal.resume.v1"],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-macos",
|
||||
clientMode: "node",
|
||||
clientDisplayName: "macOS Test",
|
||||
includeDeviceIdentity: false)
|
||||
|
||||
try await gateway.connect(
|
||||
url: #require(URL(string: "ws://gateway.example.invalid")),
|
||||
connectOptions: options,
|
||||
sessionBox: WebSocketSessionBox(session: session),
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { request in BridgeInvokeResponse(id: request.id, ok: true) })
|
||||
|
||||
let pending = Task {
|
||||
try await gateway.request(
|
||||
method: "node.invoke.progress",
|
||||
paramsJSON: #"{"invokeId":"terminal-1","nodeId":"node-1","seq":0,"ratio":1.5}"#,
|
||||
timeoutSeconds: 30)
|
||||
}
|
||||
try await waitUntil("progress request sent") {
|
||||
session.latestTask()?.sentRequestCount(method: "node.invoke.progress") == 1
|
||||
}
|
||||
let task = try #require(session.latestTask())
|
||||
let request = try #require(task.sentRequests(method: "node.invoke.progress").first)
|
||||
let params = try #require(request["params"] as? [String: Any])
|
||||
let seq = try #require(params["seq"] as? NSNumber)
|
||||
let ratio = try #require(params["ratio"] as? NSNumber)
|
||||
|
||||
#expect(CFGetTypeID(seq) != CFBooleanGetTypeID())
|
||||
#expect(seq.intValue == 0)
|
||||
#expect(ratio.doubleValue == 1.5)
|
||||
|
||||
await gateway.disconnect()
|
||||
_ = try? await pending.value
|
||||
}
|
||||
|
||||
@Test func `node invoke input and cancellation reach route callbacks`() async throws {
|
||||
let session = FakeGatewayWebSocketSession()
|
||||
let gateway = GatewayNodeSession()
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
202ccd0c71f23d8448f37dfc7b10ee2e8b2e12a90b9498740da5b757fb90513f plugin-sdk-api-baseline.json
|
||||
265925043de4f1e2beed2660c5fcdc9c7b79b570e94afbe1dd3074bc304cea98 plugin-sdk-api-baseline.jsonl
|
||||
8e04207c5befa97e4b9cc3e2b885f2f00eef115cad139a5a0907eb985a817904 plugin-sdk-api-baseline.json
|
||||
495f677730bc73c9e021ebe8ce9dc984d99436f7700c69d2a0a95c88850c383a plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -326,7 +326,9 @@ async function listPiHosts(
|
||||
}).then((page) =>
|
||||
setTerminalCapability(
|
||||
page,
|
||||
resolveExecutableFromPathEnv("pi", process.env.PATH ?? "") !== undefined,
|
||||
resolveExecutableFromPathEnv("pi", process.env.PATH ?? "", process.env, {
|
||||
fallbackToLoginShell: true,
|
||||
}) !== undefined,
|
||||
),
|
||||
)),
|
||||
});
|
||||
@@ -396,14 +398,18 @@ async function openPiTerminal(params: {
|
||||
const title = `pi --session ${params.threadId.slice(0, 12)}…`;
|
||||
if (params.hostId === LOCAL_HOST_ID) {
|
||||
const record = await requireLocalPiSession(params.threadId);
|
||||
const executable = resolveExecutableFromPathEnv("pi", process.env.PATH ?? "");
|
||||
if (!executable) {
|
||||
const resolution = resolveExecutableFromPathEnv("pi", process.env.PATH ?? "", process.env, {
|
||||
fallbackToLoginShell: true,
|
||||
withPathEnv: true,
|
||||
});
|
||||
if (!resolution) {
|
||||
throw new Error("Pi CLI is unavailable");
|
||||
}
|
||||
return {
|
||||
kind: "local",
|
||||
argv: [executable, "--session", params.threadId],
|
||||
argv: [resolution.executable, "--session", params.threadId],
|
||||
...(record.cwd ? { cwd: record.cwd } : {}),
|
||||
...(resolution.pathEnv ? { pathEnv: resolution.pathEnv } : {}),
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,19 @@ const nodeHostMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/node-host", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/node-host")>();
|
||||
return { ...actual, runNodePtyCommand: nodeHostMocks.runNodePtyCommand };
|
||||
return {
|
||||
...actual,
|
||||
runNodePtyCommand: nodeHostMocks.runNodePtyCommand,
|
||||
resolveExecutableFromPathEnv: (
|
||||
command: string,
|
||||
pathEnv: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: { withPathEnv?: boolean },
|
||||
) =>
|
||||
options?.withPathEnv
|
||||
? actual.resolveExecutableFromPathEnv(command, pathEnv, env, { withPathEnv: true })
|
||||
: actual.resolveExecutableFromPathEnv(command, pathEnv, env),
|
||||
};
|
||||
});
|
||||
|
||||
import { registerPiSessionCatalog } from "./pi-session-catalog-plugin.js";
|
||||
|
||||
@@ -25,7 +25,11 @@ type ClaudeTerminalDependencies = {
|
||||
};
|
||||
|
||||
export function isClaudeCliAvailable(pathEnv = process.env.PATH ?? ""): boolean {
|
||||
return resolveExecutableFromPathEnv("claude", pathEnv) !== undefined;
|
||||
const env = { ...process.env, PATH: pathEnv };
|
||||
return (
|
||||
resolveExecutableFromPathEnv("claude", pathEnv, env, { fallbackToLoginShell: true }) !==
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function claudeNodeTerminalCapability(node: {
|
||||
@@ -90,14 +94,18 @@ export async function openClaudeCatalogTerminal(
|
||||
if (!source?.isFile()) {
|
||||
throw new ClaudeCatalogParamsError("Claude session transcript is unavailable");
|
||||
}
|
||||
const executable = resolveExecutableFromPathEnv("claude", process.env.PATH ?? "");
|
||||
if (!executable) {
|
||||
const resolution = resolveExecutableFromPathEnv("claude", process.env.PATH ?? "", process.env, {
|
||||
fallbackToLoginShell: true,
|
||||
withPathEnv: true,
|
||||
});
|
||||
if (!resolution) {
|
||||
throw new ClaudeCatalogParamsError("Claude CLI is unavailable");
|
||||
}
|
||||
return {
|
||||
kind: "local",
|
||||
argv: [executable, "--resume", params.threadId],
|
||||
argv: [resolution.executable, "--resume", params.threadId],
|
||||
...(record.cwd ? { cwd: record.cwd } : {}),
|
||||
...(resolution.pathEnv ? { pathEnv: resolution.pathEnv } : {}),
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,12 +28,39 @@ const originalHome = process.env.HOME;
|
||||
const originalPath = process.env.PATH;
|
||||
const nodeHostMocks = vi.hoisted(() => ({
|
||||
runNodePtyCommand: vi.fn(async () => ({ exitCode: 0 })),
|
||||
userShellPaths: new Map<string, string>(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/node-host", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("openclaw/plugin-sdk/node-host")>()),
|
||||
runNodePtyCommand: nodeHostMocks.runNodePtyCommand,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/node-host", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/node-host")>();
|
||||
const resolveWithTestShellPath = (
|
||||
command: string,
|
||||
pathEnv: string,
|
||||
env: NodeJS.ProcessEnv | undefined,
|
||||
options: { fallbackToLoginShell?: boolean; withPathEnv?: boolean } | undefined,
|
||||
) => {
|
||||
const fallbackPath = options?.fallbackToLoginShell
|
||||
? nodeHostMocks.userShellPaths.get(command)
|
||||
: undefined;
|
||||
const resolution = actual.resolveExecutableFromPathEnv(command, fallbackPath ?? pathEnv, env, {
|
||||
withPathEnv: true,
|
||||
});
|
||||
return resolution && fallbackPath ? { ...resolution, pathEnv: fallbackPath } : resolution;
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
runNodePtyCommand: nodeHostMocks.runNodePtyCommand,
|
||||
resolveExecutableFromPathEnv: (
|
||||
command: string,
|
||||
pathEnv: string,
|
||||
env: NodeJS.ProcessEnv | undefined,
|
||||
options: { fallbackToLoginShell?: boolean; withPathEnv?: boolean } | undefined,
|
||||
) => {
|
||||
const resolution = resolveWithTestShellPath(command, pathEnv, env, options);
|
||||
return options?.withPathEnv ? resolution : resolution?.executable;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
async function createHome(): Promise<string> {
|
||||
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-claude-catalog-"));
|
||||
@@ -103,6 +130,7 @@ function message(
|
||||
|
||||
afterEach(async () => {
|
||||
nodeHostMocks.runNodePtyCommand.mockClear();
|
||||
nodeHostMocks.userShellPaths.clear();
|
||||
process.env.HOME = originalHome;
|
||||
process.env.PATH = originalPath;
|
||||
await Promise.all(homes.splice(0).map((home) => fs.rm(home, { recursive: true, force: true })));
|
||||
@@ -1007,7 +1035,7 @@ describe("Claude session catalog", () => {
|
||||
).rejects.toThrow("Claude session cannot be resumed in a terminal");
|
||||
});
|
||||
|
||||
it("requires the Claude binary for local terminal capability and plans", async () => {
|
||||
it("uses the login-shell Claude binary for local terminal capability and plans", async () => {
|
||||
const home = await createHome();
|
||||
process.env.HOME = home;
|
||||
const sessionId = "claude-session-1";
|
||||
@@ -1023,9 +1051,11 @@ describe("Claude session catalog", () => {
|
||||
],
|
||||
transcripts: { [sessionId]: [message(sessionId, "user", "hello", 1)] },
|
||||
});
|
||||
const binDir = path.join(home, "bin");
|
||||
await fs.mkdir(binDir);
|
||||
process.env.PATH = binDir;
|
||||
const daemonBinDir = path.join(home, "daemon-bin");
|
||||
const shellBinDir = path.join(home, "shell-bin");
|
||||
await fs.mkdir(daemonBinDir);
|
||||
await fs.mkdir(shellBinDir);
|
||||
process.env.PATH = daemonBinDir;
|
||||
let provider: SessionCatalogProvider | undefined;
|
||||
registerClaudeSessionCatalog({
|
||||
id: "anthropic",
|
||||
@@ -1047,8 +1077,9 @@ describe("Claude session catalog", () => {
|
||||
provider?.openTerminal?.({ hostId: "gateway:local", threadId: sessionId }),
|
||||
).rejects.toThrow("Claude CLI is unavailable");
|
||||
|
||||
await fs.writeFile(path.join(binDir, "claude"), "#!/bin/sh\n");
|
||||
await fs.chmod(path.join(binDir, "claude"), 0o755);
|
||||
await fs.writeFile(path.join(shellBinDir, "claude"), "#!/bin/sh\n");
|
||||
await fs.chmod(path.join(shellBinDir, "claude"), 0o755);
|
||||
nodeHostMocks.userShellPaths.set("claude", shellBinDir);
|
||||
await expect(provider?.list({})).resolves.toMatchObject([
|
||||
{ sessions: [{ threadId: sessionId, canOpenTerminal: true }] },
|
||||
]);
|
||||
@@ -1056,8 +1087,9 @@ describe("Claude session catalog", () => {
|
||||
provider?.openTerminal?.({ hostId: "gateway:local", threadId: sessionId }),
|
||||
).resolves.toMatchObject({
|
||||
kind: "local",
|
||||
argv: [path.join(binDir, "claude"), "--resume", sessionId],
|
||||
argv: [path.join(shellBinDir, "claude"), "--resume", sessionId],
|
||||
cwd: home,
|
||||
pathEnv: shellBinDir,
|
||||
});
|
||||
await expect(
|
||||
provider?.openTerminal?.({ hostId: "gateway:local", threadId: "missing" }),
|
||||
|
||||
@@ -30,9 +30,18 @@ import type {
|
||||
export const CODEX_TERMINAL_RESUME_COMMAND = "codex.terminal.resume.v1";
|
||||
|
||||
export function resolveLocalCodexTerminalExecutable(
|
||||
pathEnv = process.env.PATH ?? "",
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
return resolveExecutableFromPathEnv("codex", pathEnv);
|
||||
return resolveExecutableFromPathEnv("codex", env.PATH ?? env.Path ?? "", env, {
|
||||
fallbackToLoginShell: true,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveLocalCodexTerminalExecutableWithPathEnv(env: NodeJS.ProcessEnv = process.env) {
|
||||
return resolveExecutableFromPathEnv("codex", env.PATH ?? env.Path ?? "", env, {
|
||||
fallbackToLoginShell: true,
|
||||
withPathEnv: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function codexNodeTerminalCapability(node: {
|
||||
@@ -173,16 +182,17 @@ export async function openCodexCatalogTerminal(params: {
|
||||
const title = `codex resume ${params.threadId.slice(0, 8)}…`;
|
||||
if (params.hostId === CODEX_LOCAL_SESSION_HOST_ID) {
|
||||
const record = await requireCatalogEligibleThread(params.control, params.threadId);
|
||||
const executable = resolveLocalCodexTerminalExecutable();
|
||||
const resolution = resolveLocalCodexTerminalExecutableWithPathEnv();
|
||||
// A managed app-server may exist without a local CLI. Fail closed so
|
||||
// terminal resume never targets a different machine or missing binary.
|
||||
if (!executable) {
|
||||
if (!resolution) {
|
||||
throw new CatalogParamsError("Codex CLI is unavailable");
|
||||
}
|
||||
return {
|
||||
kind: "local",
|
||||
argv: [executable, "resume", params.threadId],
|
||||
argv: [resolution.executable, "resume", params.threadId],
|
||||
...(record.cwd ? { cwd: record.cwd } : {}),
|
||||
...(resolution.pathEnv ? { pathEnv: resolution.pathEnv } : {}),
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,10 +78,22 @@ vi.mock("./app-server/shared-client.js", () => ({
|
||||
vi.mock("./app-server/transcript-mirror.js", () => ({
|
||||
importCodexThreadHistoryToTranscript: transcriptMirrorMocks.importCodexThreadHistoryToTranscript,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/node-host", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("openclaw/plugin-sdk/node-host")>()),
|
||||
runNodePtyCommand: nodeHostMocks.runNodePtyCommand,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/node-host", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/node-host")>();
|
||||
return {
|
||||
...actual,
|
||||
runNodePtyCommand: nodeHostMocks.runNodePtyCommand,
|
||||
resolveExecutableFromPathEnv: (
|
||||
command: string,
|
||||
pathEnv: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: { withPathEnv?: boolean },
|
||||
) =>
|
||||
options?.withPathEnv
|
||||
? actual.resolveExecutableFromPathEnv(command, pathEnv, env, { withPathEnv: true })
|
||||
: actual.resolveExecutableFromPathEnv(command, pathEnv, env),
|
||||
};
|
||||
});
|
||||
|
||||
type CreateSessionEntryParams = Parameters<
|
||||
PluginRuntime["agent"]["session"]["createSessionEntry"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { accessSync, constants, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { resolveExecutableFromPathEnv } from "openclaw/plugin-sdk/node-host";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
OpenClawPluginNodeHostCommand,
|
||||
@@ -317,7 +318,12 @@ async function listOpenCodeHosts(
|
||||
const requested = query.hostIds ? new Set(query.hostIds) : undefined;
|
||||
const searchTerm = query.search?.trim().slice(0, MAX_SEARCH_LENGTH) || undefined;
|
||||
const hosts: SessionCatalogHost[] = [];
|
||||
if ((!requested || requested.has(LOCAL_HOST_ID)) && executableOnPath("opencode", process.env)) {
|
||||
if (
|
||||
(!requested || requested.has(LOCAL_HOST_ID)) &&
|
||||
resolveExecutableFromPathEnv("opencode", process.env.PATH ?? "", process.env, {
|
||||
fallbackToLoginShell: true,
|
||||
})
|
||||
) {
|
||||
try {
|
||||
hosts.push({
|
||||
hostId: LOCAL_HOST_ID,
|
||||
|
||||
@@ -112,14 +112,20 @@ export async function openOpenCodeCatalogTerminal(
|
||||
const title = `opencode --session ${params.threadId.slice(0, 12)}…`;
|
||||
if (params.hostId === OPENCODE_LOCAL_SESSION_HOST_ID) {
|
||||
const record = await requireLocalOpenCodeSession(params.threadId);
|
||||
const executable = resolveExecutableFromPathEnv("opencode", process.env.PATH ?? "");
|
||||
if (!executable) {
|
||||
const resolution = resolveExecutableFromPathEnv(
|
||||
"opencode",
|
||||
process.env.PATH ?? "",
|
||||
process.env,
|
||||
{ fallbackToLoginShell: true, withPathEnv: true },
|
||||
);
|
||||
if (!resolution) {
|
||||
throw new Error("OpenCode CLI is unavailable");
|
||||
}
|
||||
return {
|
||||
kind: "local",
|
||||
argv: [executable, "--session", params.threadId],
|
||||
argv: [resolution.executable, "--session", params.threadId],
|
||||
...(record.cwd ? { cwd: record.cwd } : {}),
|
||||
...(resolution.pathEnv ? { pathEnv: resolution.pathEnv } : {}),
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,19 @@ const nodeHostMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/node-host", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/node-host")>();
|
||||
return { ...actual, runNodePtyCommand: nodeHostMocks.runNodePtyCommand };
|
||||
return {
|
||||
...actual,
|
||||
runNodePtyCommand: nodeHostMocks.runNodePtyCommand,
|
||||
resolveExecutableFromPathEnv: (
|
||||
command: string,
|
||||
pathEnv: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: { withPathEnv?: boolean },
|
||||
) =>
|
||||
options?.withPathEnv
|
||||
? actual.resolveExecutableFromPathEnv(command, pathEnv, env, { withPathEnv: true })
|
||||
: actual.resolveExecutableFromPathEnv(command, pathEnv, env),
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
|
||||
@@ -55,6 +55,8 @@ export function resolveTerminalOpenSpawnPlan(
|
||||
return resolveTerminalSpawnPlan(launchPlan);
|
||||
}
|
||||
if (catalogPlan.kind === "local") {
|
||||
// Keep catalog commands behind the policy-selected login shell. Its profile restores
|
||||
// CLI dependencies omitted from a sparse Gateway daemon PATH before the shebang runs.
|
||||
return resolveTerminalSpawnPlan({
|
||||
...launchPlan,
|
||||
initialCommand: catalogPlan.argv,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import type { SessionCatalogProvider } from "../../plugins/session-catalog.js";
|
||||
import { createTerminalLaunchPolicy } from "../terminal/launch.js";
|
||||
import { terminalHandlers } from "./terminal.js";
|
||||
import { terminalHandlers, TERMINAL_OPEN_DEADLINE_MS } from "./terminal.js";
|
||||
|
||||
const policyMocks = vi.hoisted(() => ({
|
||||
resolveNodeCommandAllowlist: vi.fn(() => new Set<string>()),
|
||||
@@ -160,6 +160,7 @@ describe("terminal gateway policy", () => {
|
||||
const openTerminal = vi.fn(async () => ({
|
||||
kind: "local" as const,
|
||||
argv: ["codex", "resume", "thread"],
|
||||
pathEnv: "/login-shell/bin:/usr/bin",
|
||||
title: "codex resume thread",
|
||||
}));
|
||||
installCatalog({
|
||||
@@ -188,6 +189,7 @@ describe("terminal gateway policy", () => {
|
||||
expect.objectContaining({
|
||||
shell: expect.any(String),
|
||||
args: ["-il", "-c", "'codex' 'resume' 'thread'"],
|
||||
env: expect.objectContaining({ PATH: "/login-shell/bin:/usr/bin" }),
|
||||
}),
|
||||
);
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
@@ -196,6 +198,77 @@ describe("terminal gateway policy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a catalog plan that finishes after the absolute open deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
try {
|
||||
installCatalog({
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
list: async () => [],
|
||||
read: async (request) => ({ ...request, items: [] }),
|
||||
openTerminal: async () => {
|
||||
vi.setSystemTime(TERMINAL_OPEN_DEADLINE_MS);
|
||||
return { kind: "local", argv: ["codex", "resume", "thread"] };
|
||||
},
|
||||
});
|
||||
const { opts, sessions, respond } = makeOpts(
|
||||
{
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
catalog: { catalogId: "codex", hostId: "gateway:local", threadId: "thread" },
|
||||
},
|
||||
{ enabled: true },
|
||||
);
|
||||
|
||||
await expectDefined(terminalHandlers["terminal.open"], "terminal.open")(opts);
|
||||
|
||||
expect(sessions.open).not.toHaveBeenCalled();
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "terminal open timed out" }),
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("maps a catalog rejection after the absolute deadline to a timeout", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
try {
|
||||
installCatalog({
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
list: async () => [],
|
||||
read: async (request) => ({ ...request, items: [] }),
|
||||
openTerminal: async () => {
|
||||
vi.setSystemTime(TERMINAL_OPEN_DEADLINE_MS);
|
||||
throw new Error("late catalog failure");
|
||||
},
|
||||
});
|
||||
const { opts, respond } = makeOpts(
|
||||
{
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
catalog: { catalogId: "codex", hostId: "gateway:local", threadId: "thread" },
|
||||
},
|
||||
{ enabled: true },
|
||||
);
|
||||
|
||||
await expectDefined(terminalHandlers["terminal.open"], "terminal.open")(opts);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "terminal open timed out" }),
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not create a terminal after the owning connection closes during catalog lookup", async () => {
|
||||
const plan = deferred<{ kind: "local"; argv: string[] }>();
|
||||
const openTerminal = vi.fn(() => plan.promise);
|
||||
@@ -229,6 +302,83 @@ describe("terminal gateway policy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("closes a terminal whose owning connection disappears during PTY creation", async () => {
|
||||
const created = deferred<{
|
||||
ok: true;
|
||||
sessionId: string;
|
||||
agentId: string;
|
||||
shell: string;
|
||||
cwd: string;
|
||||
}>();
|
||||
const { opts, sessions, respond, isConnectionActive } = makeOpts(
|
||||
{ cols: 80, rows: 24 },
|
||||
{ enabled: true },
|
||||
);
|
||||
sessions.open.mockImplementationOnce(async () => await created.promise);
|
||||
|
||||
const opening = expectDefined(terminalHandlers["terminal.open"], "terminal.open")(opts);
|
||||
await vi.waitFor(() => expect(sessions.open).toHaveBeenCalledOnce());
|
||||
isConnectionActive.mockReturnValue(false);
|
||||
created.resolve({
|
||||
ok: true,
|
||||
sessionId: "terminal-raced",
|
||||
agentId: "main",
|
||||
shell: "/bin/zsh",
|
||||
cwd: "/work",
|
||||
});
|
||||
await opening;
|
||||
|
||||
expect(sessions.close).toHaveBeenCalledWith("conn-1", "terminal-raced");
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "terminal connection closed" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("times out one terminal open with request-scoped cancellation", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const created = deferred<{
|
||||
ok: true;
|
||||
sessionId: string;
|
||||
agentId: string;
|
||||
shell: string;
|
||||
cwd: string;
|
||||
}>();
|
||||
let openSignal: AbortSignal | undefined;
|
||||
const { opts, sessions, respond } = makeOpts({ cols: 80, rows: 24 }, { enabled: true });
|
||||
sessions.open.mockImplementationOnce(async (request: unknown) => {
|
||||
openSignal = (request as { signal?: AbortSignal }).signal;
|
||||
return await created.promise;
|
||||
});
|
||||
|
||||
const opening = expectDefined(terminalHandlers["terminal.open"], "terminal.open")(opts);
|
||||
await vi.waitFor(() => expect(openSignal).toBeDefined());
|
||||
await vi.advanceTimersByTimeAsync(TERMINAL_OPEN_DEADLINE_MS);
|
||||
await opening;
|
||||
|
||||
expect(openSignal?.aborted).toBe(true);
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "terminal open timed out" }),
|
||||
);
|
||||
created.resolve({
|
||||
ok: true,
|
||||
sessionId: "terminal-late",
|
||||
agentId: "main",
|
||||
shell: "/bin/zsh",
|
||||
cwd: "/work",
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(sessions.close).toHaveBeenCalledWith("conn-1", "terminal-late"),
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not create a terminal when disabled during catalog lookup", async () => {
|
||||
const plan = deferred<{ kind: "local"; argv: string[] }>();
|
||||
const openTerminal = vi.fn(() => plan.promise);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
validateTerminalTextParams,
|
||||
validateTerminalUploadResult,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { toErrorObject } from "../../infra/errors.js";
|
||||
import { NODE_TERMINAL_UPLOAD_COMMAND } from "../../infra/node-commands.js";
|
||||
import type { TerminalUploadFile } from "../../infra/terminal-file-upload.js";
|
||||
import type { SessionCatalogTerminalPlan } from "../../plugins/session-catalog.js";
|
||||
@@ -53,6 +54,91 @@ function terminalEnabled(context: GatewayRequestHandlerOptions["context"]): bool
|
||||
return context.isTerminalEnabled();
|
||||
}
|
||||
|
||||
export const TERMINAL_OPEN_DEADLINE_MS = 30_000;
|
||||
|
||||
class TerminalOpenDeadlineError extends Error {
|
||||
constructor() {
|
||||
super("terminal open timed out");
|
||||
this.name = "TerminalOpenDeadlineError";
|
||||
}
|
||||
}
|
||||
|
||||
type TerminalOpenDeadline = {
|
||||
expiresAtMs: number;
|
||||
controller: AbortController;
|
||||
};
|
||||
|
||||
function createTerminalOpenDeadline(): TerminalOpenDeadline {
|
||||
return {
|
||||
expiresAtMs: Date.now() + TERMINAL_OPEN_DEADLINE_MS,
|
||||
controller: new AbortController(),
|
||||
};
|
||||
}
|
||||
|
||||
function expireTerminalOpenDeadline(deadline: TerminalOpenDeadline): Error {
|
||||
if (!deadline.controller.signal.aborted) {
|
||||
deadline.controller.abort(new TerminalOpenDeadlineError());
|
||||
}
|
||||
return toErrorObject(deadline.controller.signal.reason, "Terminal open timed out");
|
||||
}
|
||||
|
||||
async function waitForTerminalOpenDeadline<T>(
|
||||
run: () => Promise<T>,
|
||||
deadline: TerminalOpenDeadline,
|
||||
): Promise<T> {
|
||||
if (deadline.controller.signal.aborted || Date.now() >= deadline.expiresAtMs) {
|
||||
throw expireTerminalOpenDeadline(deadline);
|
||||
}
|
||||
return await new Promise<T>((resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer);
|
||||
reject(expireTerminalOpenDeadline(deadline));
|
||||
};
|
||||
const timer = setTimeout(
|
||||
() => expireTerminalOpenDeadline(deadline),
|
||||
Math.max(0, deadline.expiresAtMs - Date.now()),
|
||||
);
|
||||
deadline.controller.signal.addEventListener("abort", onAbort, { once: true });
|
||||
let promise: Promise<T>;
|
||||
try {
|
||||
promise = run();
|
||||
} catch (error) {
|
||||
if (deadline.controller.signal.aborted || Date.now() >= deadline.expiresAtMs) {
|
||||
expireTerminalOpenDeadline(deadline);
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
deadline.controller.signal.removeEventListener("abort", onAbort);
|
||||
reject(toErrorObject(error, "Terminal open failed"));
|
||||
return;
|
||||
}
|
||||
void promise.then(
|
||||
(value) => {
|
||||
if (deadline.controller.signal.aborted || Date.now() >= deadline.expiresAtMs) {
|
||||
expireTerminalOpenDeadline(deadline);
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
deadline.controller.signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
},
|
||||
(error: unknown) => {
|
||||
if (deadline.controller.signal.aborted || Date.now() >= deadline.expiresAtMs) {
|
||||
expireTerminalOpenDeadline(deadline);
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
deadline.controller.signal.removeEventListener("abort", onAbort);
|
||||
reject(toErrorObject(error, "Terminal open failed"));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function respondTerminalOpenTimeout(respond: GatewayRequestHandlerOptions["respond"]): void {
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "terminal open timed out"));
|
||||
}
|
||||
|
||||
function parseNodePayload(payload: unknown, payloadJSON?: string | null): unknown {
|
||||
if (!payloadJSON) {
|
||||
return payload;
|
||||
@@ -144,6 +230,7 @@ export const terminalHandlers: GatewayRequestHandlers = {
|
||||
respondLaunchBlocked(respond, launch.block);
|
||||
return;
|
||||
}
|
||||
const deadline = createTerminalOpenDeadline();
|
||||
|
||||
let catalogPlan: SessionCatalogTerminalPlan | undefined;
|
||||
let title: string | undefined;
|
||||
@@ -173,12 +260,22 @@ export const terminalHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const openTerminal = provider.openTerminal;
|
||||
const catalog = p.catalog;
|
||||
try {
|
||||
catalogPlan = await provider.openTerminal({
|
||||
hostId: p.catalog.hostId,
|
||||
threadId: p.catalog.threadId,
|
||||
});
|
||||
catalogPlan = await waitForTerminalOpenDeadline(
|
||||
() =>
|
||||
openTerminal.call(provider, {
|
||||
hostId: catalog.hostId,
|
||||
threadId: catalog.threadId,
|
||||
}),
|
||||
deadline,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof TerminalOpenDeadlineError) {
|
||||
respondTerminalOpenTimeout(respond);
|
||||
return;
|
||||
}
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
@@ -213,13 +310,26 @@ export const terminalHandlers: GatewayRequestHandlers = {
|
||||
invalid(respond, "catalog terminal plan has invalid params");
|
||||
return;
|
||||
}
|
||||
const policyResult = await applyPluginNodeInvokePolicy({
|
||||
context,
|
||||
client: opts.client,
|
||||
nodeSession: access.node,
|
||||
command: nodeCatalogPlan.command,
|
||||
params: nodeParams,
|
||||
});
|
||||
let policyResult: Awaited<ReturnType<typeof applyPluginNodeInvokePolicy>>;
|
||||
try {
|
||||
policyResult = await waitForTerminalOpenDeadline(
|
||||
() =>
|
||||
applyPluginNodeInvokePolicy({
|
||||
context,
|
||||
client: opts.client,
|
||||
nodeSession: access.node,
|
||||
command: nodeCatalogPlan.command,
|
||||
params: nodeParams,
|
||||
}),
|
||||
deadline,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof TerminalOpenDeadlineError) {
|
||||
respondTerminalOpenTimeout(respond);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (policyResult && !policyResult.ok) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, policyResult.message));
|
||||
return;
|
||||
@@ -260,23 +370,62 @@ export const terminalHandlers: GatewayRequestHandlers = {
|
||||
});
|
||||
}
|
||||
const spawnPlan = resolveTerminalOpenSpawnPlan(refreshedLaunch.plan, catalogPlan);
|
||||
const outcome = await manager.open({
|
||||
connId,
|
||||
agentId: spawnPlan.agentId,
|
||||
cwd: spawnPlan.cwd,
|
||||
shell: spawnPlan.shell,
|
||||
args: spawnPlan.args,
|
||||
cols: p.cols,
|
||||
rows: p.rows,
|
||||
env: buildTerminalEnv(process.env),
|
||||
...(createBackend ? { createBackend } : {}),
|
||||
...(stageUpload ? { stageUpload } : {}),
|
||||
});
|
||||
const terminalEnv = buildTerminalEnv(process.env);
|
||||
if (catalogPlan?.kind === "local" && catalogPlan.pathEnv) {
|
||||
// Preserve the PATH that found a login-shell CLI so env-based shebangs
|
||||
// can resolve their interpreter inside the spawned terminal process.
|
||||
terminalEnv.PATH = catalogPlan.pathEnv;
|
||||
}
|
||||
let openingTerminal: ReturnType<typeof manager.open> | undefined;
|
||||
let outcome: Awaited<ReturnType<typeof manager.open>>;
|
||||
try {
|
||||
outcome = await waitForTerminalOpenDeadline(() => {
|
||||
openingTerminal = manager.open({
|
||||
connId,
|
||||
agentId: spawnPlan.agentId,
|
||||
cwd: spawnPlan.cwd,
|
||||
shell: spawnPlan.shell,
|
||||
args: spawnPlan.args,
|
||||
cols: p.cols,
|
||||
rows: p.rows,
|
||||
env: terminalEnv,
|
||||
signal: deadline.controller.signal,
|
||||
...(createBackend ? { createBackend } : {}),
|
||||
...(stageUpload ? { stageUpload } : {}),
|
||||
});
|
||||
return openingTerminal;
|
||||
}, deadline);
|
||||
} catch (error) {
|
||||
if (error instanceof TerminalOpenDeadlineError) {
|
||||
// The backend can register immediately before deadline arbitration.
|
||||
// Close a late success by id so timeout never leaves an unreachable PTY.
|
||||
if (openingTerminal) {
|
||||
void openingTerminal.then(
|
||||
(lateOutcome) => {
|
||||
if (lateOutcome.ok) {
|
||||
manager.close(connId, lateOutcome.sessionId);
|
||||
}
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
respondTerminalOpenTimeout(respond);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!outcome.ok) {
|
||||
const code = outcome.code === "limit" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE;
|
||||
respond(false, undefined, errorShape(code, outcome.message));
|
||||
return;
|
||||
}
|
||||
if (context.isConnectionActive?.(connId) === false) {
|
||||
// A browser deadline can close the socket while PTY creation is still
|
||||
// finishing. Release the raced session instead of leaving an orphan.
|
||||
manager.close(connId, outcome.sessionId);
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "terminal connection closed"));
|
||||
return;
|
||||
}
|
||||
context.logGateway.info(
|
||||
`terminal opened session=${outcome.sessionId} agent=${outcome.agentId} conn=${connId} shell=${outcome.shell}`,
|
||||
);
|
||||
|
||||
@@ -9,6 +9,14 @@ type TerminalPtyHandle = Awaited<ReturnType<typeof spawnTerminalPty>>;
|
||||
const TERMINAL_EVENT_DATA = "terminal.data";
|
||||
const TERMINAL_EVENT_EXIT = "terminal.exit";
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((next) => {
|
||||
resolve = next;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
/** A controllable fake PTY that records writes and lets tests drive data/exit. */
|
||||
function makeFakePty() {
|
||||
let dataListener: ((chunk: string) => void) | undefined;
|
||||
@@ -77,6 +85,73 @@ function baseRequest(overrides?: Partial<TerminalOpenRequest>): TerminalOpenRequ
|
||||
}
|
||||
|
||||
describe("TerminalSessionManager", () => {
|
||||
it("kills a backend that finishes after its open request is cancelled", async () => {
|
||||
const spawned = deferred<TerminalPtyHandle>();
|
||||
const controller = new AbortController();
|
||||
const first = makeFakePty();
|
||||
const second = makeFakePty();
|
||||
let spawnCount = 0;
|
||||
const manager = new TerminalSessionManager({
|
||||
emit: vi.fn(),
|
||||
maxSessions: 1,
|
||||
spawn: () => (spawnCount++ === 0 ? spawned.promise : Promise.resolve(second)),
|
||||
});
|
||||
const opening = manager.open(baseRequest({ signal: controller.signal }));
|
||||
|
||||
controller.abort(new Error("terminal open timed out"));
|
||||
const next = await manager.open(baseRequest({ connId: "conn-2" }));
|
||||
expect(next.ok).toBe(true);
|
||||
spawned.resolve(first);
|
||||
|
||||
await expect(opening).resolves.toEqual({
|
||||
ok: false,
|
||||
code: "closed",
|
||||
message: "terminal open timed out",
|
||||
});
|
||||
expect(first.killed).toBe(true);
|
||||
expect(manager.size).toBe(1);
|
||||
if (next.ok) {
|
||||
expect(manager.close("conn-2", next.sessionId)).toBe(true);
|
||||
}
|
||||
expect(second.killed).toBe(true);
|
||||
expect(manager.size).toBe(0);
|
||||
});
|
||||
|
||||
it("bounds cancelled backend operations until they settle", async () => {
|
||||
const firstSpawn = deferred<TerminalPtyHandle>();
|
||||
const secondSpawn = deferred<TerminalPtyHandle>();
|
||||
const firstController = new AbortController();
|
||||
const secondController = new AbortController();
|
||||
let spawnCount = 0;
|
||||
const manager = new TerminalSessionManager({
|
||||
emit: vi.fn(),
|
||||
maxSessions: 1,
|
||||
spawn: () => (spawnCount++ === 0 ? firstSpawn.promise : secondSpawn.promise),
|
||||
});
|
||||
|
||||
const firstOpening = manager.open(baseRequest({ signal: firstController.signal }));
|
||||
firstController.abort(new Error("first cancelled"));
|
||||
const secondOpening = manager.open(
|
||||
baseRequest({ connId: "conn-2", signal: secondController.signal }),
|
||||
);
|
||||
secondController.abort(new Error("second cancelled"));
|
||||
|
||||
await expect(manager.open(baseRequest({ connId: "conn-3" }))).resolves.toEqual({
|
||||
ok: false,
|
||||
code: "limit",
|
||||
message: "terminal spawn limit reached (2)",
|
||||
});
|
||||
|
||||
const first = makeFakePty();
|
||||
const second = makeFakePty();
|
||||
firstSpawn.resolve(first);
|
||||
secondSpawn.resolve(second);
|
||||
await expect(firstOpening).resolves.toMatchObject({ ok: false, code: "closed" });
|
||||
await expect(secondOpening).resolves.toMatchObject({ ok: false, code: "closed" });
|
||||
expect(first.killed).toBe(true);
|
||||
expect(second.killed).toBe(true);
|
||||
});
|
||||
|
||||
it("runs relay backends through the same stream, input, resize, and close lifecycle", async () => {
|
||||
let onData: ((data: string) => void) | undefined;
|
||||
let onExit:
|
||||
|
||||
@@ -52,6 +52,10 @@ export class TerminalSessionManager {
|
||||
// Slots reserved by opens that are still awaiting spawn. Counted against the
|
||||
// cap so concurrent opens cannot all pass the check and exceed maxSessions.
|
||||
private opening = 0;
|
||||
// Cancellation frees a session slot, but cannot stop every backend factory.
|
||||
// Bound those physical operations until they settle so disconnect churn
|
||||
// cannot create an unbounded number of native/node spawn attempts.
|
||||
private spawning = 0;
|
||||
|
||||
constructor(options: TerminalSessionManagerOptions) {
|
||||
void ensureTerminalUploadCleanup();
|
||||
@@ -71,6 +75,16 @@ export class TerminalSessionManager {
|
||||
|
||||
/** Spawns a shell and wires its output/exit to the owning connection. */
|
||||
async open(request: TerminalOpenRequest): Promise<TerminalOpenOutcome> {
|
||||
if (request.signal?.aborted) {
|
||||
return { ok: false, code: "closed", message: this.openAbortMessage(request.signal) };
|
||||
}
|
||||
if (this.spawning >= this.maxSessions * 2) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "limit",
|
||||
message: `terminal spawn limit reached (${this.maxSessions * 2})`,
|
||||
};
|
||||
}
|
||||
if (this.sessions.size + this.opening >= this.maxSessions) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -80,7 +94,29 @@ export class TerminalSessionManager {
|
||||
}
|
||||
// Reserve the slot before the async spawn so it is visible to concurrent opens.
|
||||
this.opening += 1;
|
||||
const pending: TerminalPendingOpen = { agentId: request.agentId };
|
||||
this.spawning += 1;
|
||||
let reservationActive = true;
|
||||
const releaseReservation = () => {
|
||||
if (!reservationActive) {
|
||||
return;
|
||||
}
|
||||
reservationActive = false;
|
||||
this.opening -= 1;
|
||||
this.untrackPendingOpen(request.connId, pending);
|
||||
};
|
||||
const pending: TerminalPendingOpen = {
|
||||
agentId: request.agentId,
|
||||
abort: (message) => {
|
||||
pending.abortMessage ??= message;
|
||||
// A hung spawn must not consume capacity after its owner is gone.
|
||||
// Its eventual backend is still killed by the abortMessage check below.
|
||||
releaseReservation();
|
||||
},
|
||||
};
|
||||
const abortPending = () => {
|
||||
pending.abort(this.openAbortMessage(request.signal));
|
||||
};
|
||||
request.signal?.addEventListener("abort", abortPending, { once: true });
|
||||
this.trackPendingOpen(request.connId, pending);
|
||||
let backend: TerminalBackend;
|
||||
try {
|
||||
@@ -98,15 +134,17 @@ export class TerminalSessionManager {
|
||||
this.spawn,
|
||||
);
|
||||
} catch (err) {
|
||||
this.opening -= 1;
|
||||
this.untrackPendingOpen(request.connId, pending);
|
||||
this.spawning -= 1;
|
||||
releaseReservation();
|
||||
request.signal?.removeEventListener("abort", abortPending);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { ok: false, code: "spawn_failed", message };
|
||||
}
|
||||
// Hand the reservation over to the live session (synchronous from here — no
|
||||
// await — so the counts never both drop).
|
||||
this.opening -= 1;
|
||||
this.untrackPendingOpen(request.connId, pending);
|
||||
this.spawning -= 1;
|
||||
releaseReservation();
|
||||
request.signal?.removeEventListener("abort", abortPending);
|
||||
if (pending.abortMessage) {
|
||||
// The owning connection disconnected while the shell was spawning; kill it
|
||||
// now rather than register an orphan no one can reach or close.
|
||||
@@ -310,6 +348,10 @@ export class TerminalSessionManager {
|
||||
set.add(pending);
|
||||
}
|
||||
|
||||
private openAbortMessage(signal: AbortSignal | undefined): string {
|
||||
return signal?.reason instanceof Error ? signal.reason.message : "terminal open cancelled";
|
||||
}
|
||||
|
||||
private untrackPendingOpen(connId: string, pending: TerminalPendingOpen): void {
|
||||
const set = this.pendingOpens.get(connId);
|
||||
if (set) {
|
||||
@@ -332,7 +374,7 @@ export class TerminalSessionManager {
|
||||
const opens = this.pendingOpens.get(connId);
|
||||
if (opens) {
|
||||
for (const pending of opens) {
|
||||
pending.abortMessage = "connection closed during open";
|
||||
pending.abort("connection closed during open");
|
||||
}
|
||||
}
|
||||
const ids = this.byConn.get(connId);
|
||||
@@ -361,7 +403,7 @@ export class TerminalSessionManager {
|
||||
for (const opens of this.pendingOpens.values()) {
|
||||
for (const pending of opens) {
|
||||
if (!isAllowed(pending.agentId)) {
|
||||
pending.abortMessage = "terminal closed because the agent policy changed";
|
||||
pending.abort("terminal closed because the agent policy changed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,7 +455,7 @@ export class TerminalSessionManager {
|
||||
// Abort any opens still spawning so they don't register after shutdown.
|
||||
for (const opens of this.pendingOpens.values()) {
|
||||
for (const pending of opens) {
|
||||
pending.abortMessage = "gateway closed during terminal open";
|
||||
pending.abort("gateway closed during terminal open");
|
||||
}
|
||||
}
|
||||
// Snapshot first: finalize() deletes from this.sessions during iteration.
|
||||
|
||||
@@ -46,6 +46,8 @@ export type TerminalOpenRequest = {
|
||||
cols: number;
|
||||
rows: number;
|
||||
env: Record<string, string>;
|
||||
/** Request-scoped cancellation; a late backend is killed before registration. */
|
||||
signal?: AbortSignal;
|
||||
createBackend?: () => Promise<TerminalBackend>;
|
||||
stageUpload?: (file: TerminalUploadFile) => Promise<TerminalUploadResult>;
|
||||
};
|
||||
@@ -55,4 +57,8 @@ export type TerminalOpenOutcome =
|
||||
| { ok: false; code: "limit" | "spawn_failed" | "closed"; message: string };
|
||||
|
||||
/** Abort state shared between a pending open and lifecycle/policy teardown. */
|
||||
export type TerminalPendingOpen = { agentId: string; abortMessage?: string };
|
||||
export type TerminalPendingOpen = {
|
||||
agentId: string;
|
||||
abortMessage?: string;
|
||||
abort(message: string): void;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ let clearShellEnvAppliedKeys: ShellEnvModule["clearShellEnvAppliedKeys"];
|
||||
let getShellEnvAppliedKeys: ShellEnvModule["getShellEnvAppliedKeys"];
|
||||
let getShellPathFromLoginShell: ShellEnvModule["getShellPathFromLoginShell"];
|
||||
let loadShellEnvFallback: ShellEnvModule["loadShellEnvFallback"];
|
||||
let resolveExecutableFromUserShellPathWithPathEnv: ShellEnvModule["resolveExecutableFromUserShellPathWithPathEnv"];
|
||||
let resolveShellEnvFallbackTimeoutMs: ShellEnvModule["resolveShellEnvFallbackTimeoutMs"];
|
||||
let shouldDeferShellEnvFallback: ShellEnvModule["shouldDeferShellEnvFallback"];
|
||||
let shouldEnableShellEnvFallback: ShellEnvModule["shouldEnableShellEnvFallback"];
|
||||
@@ -21,6 +22,7 @@ beforeEach(async () => {
|
||||
getShellEnvAppliedKeys,
|
||||
getShellPathFromLoginShell,
|
||||
loadShellEnvFallback,
|
||||
resolveExecutableFromUserShellPathWithPathEnv,
|
||||
resolveShellEnvFallbackTimeoutMs,
|
||||
shouldDeferShellEnvFallback,
|
||||
shouldEnableShellEnvFallback,
|
||||
@@ -563,6 +565,47 @@ describe("shell env fallback", () => {
|
||||
expectSanitizedStartupEnv(receivedEnv);
|
||||
});
|
||||
|
||||
it("resolves from the daemon PATH without probing the login shell", () => {
|
||||
const exec = vi.fn(() => Buffer.from("PATH=/bin\0"));
|
||||
|
||||
const result = resolveExecutableFromUserShellPathWithPathEnv("sh", {
|
||||
env: { PATH: "/bin" },
|
||||
exec: exec as unknown as Parameters<
|
||||
typeof resolveExecutableFromUserShellPathWithPathEnv
|
||||
>[1]["exec"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ executable: "/bin/sh" });
|
||||
expect(exec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves from the login-shell PATH when the daemon PATH misses the executable", () => {
|
||||
const exec = vi.fn(() => Buffer.from("PATH=/bin\0"));
|
||||
|
||||
const result = resolveExecutableFromUserShellPathWithPathEnv("sh", {
|
||||
env: { PATH: "/missing", SHELL: "/bin/sh" },
|
||||
exec: exec as unknown as Parameters<
|
||||
typeof resolveExecutableFromUserShellPathWithPathEnv
|
||||
>[1]["exec"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ executable: "/bin/sh", pathEnv: "/bin" });
|
||||
expect(exec).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("returns the login-shell PATH needed by env-based executable launchers", () => {
|
||||
const exec = vi.fn(() => Buffer.from("PATH=/bin\0"));
|
||||
|
||||
const result = resolveExecutableFromUserShellPathWithPathEnv("sh", {
|
||||
env: { PATH: "/missing", SHELL: "/bin/sh" },
|
||||
exec: exec as unknown as Parameters<
|
||||
typeof resolveExecutableFromUserShellPathWithPathEnv
|
||||
>[1]["exec"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ executable: "/bin/sh", pathEnv: "/bin" });
|
||||
});
|
||||
|
||||
it("returns null without invoking shell on win32", () => {
|
||||
const exec = vi.fn(() => Buffer.from("PATH=/usr/local/bin:/usr/bin\0HOME=/tmp\0"));
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from "node:path";
|
||||
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
|
||||
import { isTruthyEnvValue } from "./env.js";
|
||||
import { formatErrorMessage } from "./errors.js";
|
||||
import { resolveExecutableFromPathEnv } from "./executable-path.js";
|
||||
import { sanitizeHostExecEnv } from "./host-env-security.js";
|
||||
import { parseStrictNonNegativeInteger } from "./parse-finite-number.js";
|
||||
|
||||
@@ -317,6 +318,48 @@ export function getShellPathFromLoginShell(opts: {
|
||||
cachedShellPath = shellPath && shellPath.length > 0 ? shellPath : null;
|
||||
return cachedShellPath;
|
||||
}
|
||||
|
||||
type UserShellExecutableResolution = {
|
||||
executable: string;
|
||||
/** Present only when resolution required the login-shell PATH fallback. */
|
||||
pathEnv?: string;
|
||||
};
|
||||
|
||||
export function resolveExecutableFromUserShellPathWithPathEnv(
|
||||
executable: string,
|
||||
opts: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
pathEnv?: string;
|
||||
includeExtensionless?: boolean;
|
||||
timeoutMs?: number;
|
||||
exec?: typeof execFileSync;
|
||||
platform?: NodeJS.Platform;
|
||||
},
|
||||
): UserShellExecutableResolution | undefined {
|
||||
const direct = resolveExecutableFromPathEnv(
|
||||
executable,
|
||||
opts.pathEnv ?? opts.env.PATH ?? opts.env.Path ?? "",
|
||||
opts.env,
|
||||
{ includeExtensionless: opts.includeExtensionless },
|
||||
);
|
||||
if (direct) {
|
||||
return { executable: direct };
|
||||
}
|
||||
const shellPath = getShellPathFromLoginShell({
|
||||
env: opts.env,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
exec: opts.exec,
|
||||
platform: opts.platform,
|
||||
});
|
||||
if (!shellPath) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = resolveExecutableFromPathEnv(executable, shellPath, opts.env, {
|
||||
includeExtensionless: opts.includeExtensionless,
|
||||
});
|
||||
return resolved ? { executable: resolved, pathEnv: shellPath } : undefined;
|
||||
}
|
||||
|
||||
export function getShellEnvAppliedKeys(): string[] {
|
||||
return [...lastAppliedKeys];
|
||||
}
|
||||
|
||||
@@ -88,12 +88,6 @@ export class NodeHostWorkerBridgeClient implements NodeHostClient {
|
||||
this.writeMessage({ type: "invoke-result", result: params ?? {} });
|
||||
return {} as T;
|
||||
}
|
||||
if (method === "node.invoke.progress") {
|
||||
// Unreachable while the embedded app worker never enables agent runs
|
||||
// (the only progress emitter); the Mac host bridge has no case for it.
|
||||
this.writeMessage({ type: "invoke-progress", progress: params ?? {} });
|
||||
return {} as T;
|
||||
}
|
||||
if (method === "node.event") {
|
||||
this.writeMessage({ type: "node-event", event: params ?? {} });
|
||||
return {} as T;
|
||||
|
||||
@@ -35,19 +35,55 @@ describe("NodeHostWorkerBridgeClient", () => {
|
||||
const client = new NodeHostWorkerBridgeClient((message) => messages.push(message));
|
||||
|
||||
await client.request("node.invoke.result", { id: "invoke-1", ok: true });
|
||||
await client.request("node.invoke.progress", { invokeId: "invoke-1", seq: 0, chunk: "a" });
|
||||
await client.request("node.event", { event: "exec.started", payloadJSON: "{}" });
|
||||
|
||||
expect(messages).toEqual([
|
||||
{ type: "invoke-result", result: { id: "invoke-1", ok: true } },
|
||||
{
|
||||
type: "invoke-progress",
|
||||
progress: { invokeId: "invoke-1", seq: 0, chunk: "a" },
|
||||
},
|
||||
{ type: "node-event", event: { event: "exec.started", payloadJSON: "{}" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("tunnels invoke progress and waits for gateway acceptance", async () => {
|
||||
const messages: Array<Record<string, unknown>> = [];
|
||||
const client = new NodeHostWorkerBridgeClient((message) => {
|
||||
messages.push(message as Record<string, unknown>);
|
||||
});
|
||||
|
||||
let settled = false;
|
||||
const response = client
|
||||
.request("node.invoke.progress", {
|
||||
invokeId: "invoke-1",
|
||||
nodeId: "node-1",
|
||||
seq: 0,
|
||||
chunk: "a",
|
||||
})
|
||||
.then(() => {
|
||||
settled = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(settled).toBe(false);
|
||||
expect(messages).toEqual([
|
||||
{
|
||||
type: "gateway-request",
|
||||
id: "gateway-1",
|
||||
method: "node.invoke.progress",
|
||||
params: { invokeId: "invoke-1", nodeId: "node-1", seq: 0, chunk: "a" },
|
||||
timeoutMs: 15_000,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
client.handleResponse({
|
||||
type: "gateway-response",
|
||||
id: "gateway-1",
|
||||
ok: true,
|
||||
result: { ok: true },
|
||||
}),
|
||||
).toBe(true);
|
||||
await response;
|
||||
expect(settled).toBe(true);
|
||||
});
|
||||
|
||||
it("tunnels runtime gateway requests and resolves their matching response", async () => {
|
||||
const messages: Array<Record<string, unknown>> = [];
|
||||
const client = new NodeHostWorkerBridgeClient((message) => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { resolveExecutableFromPathEnv as resolveExecutableFromPathEnvDirect } from "../infra/executable-path.js";
|
||||
import { resolveExecutableFromUserShellPathWithPathEnv } from "../infra/shell-env.js";
|
||||
|
||||
export {
|
||||
decodeNodePtyResumeParams,
|
||||
runNodePtyCommand,
|
||||
@@ -5,5 +8,51 @@ export {
|
||||
type NodePtyResumeParams,
|
||||
} from "../node-host/pty-command.js";
|
||||
export { validateClaudeSessionId } from "../node-host/invoke-agent-cli-claude-params.js";
|
||||
export { resolveExecutableFromPathEnv } from "../infra/executable-path.js";
|
||||
export type { OpenClawPluginNodeHostCommandIo } from "../plugins/types.js";
|
||||
|
||||
export function resolveExecutableFromPathEnv(
|
||||
executable: string,
|
||||
pathEnv: string,
|
||||
env: NodeJS.ProcessEnv | undefined,
|
||||
options: {
|
||||
includeExtensionless?: boolean;
|
||||
fallbackToLoginShell?: boolean;
|
||||
withPathEnv: true;
|
||||
},
|
||||
): { executable: string; pathEnv?: string } | undefined;
|
||||
export function resolveExecutableFromPathEnv(
|
||||
executable: string,
|
||||
pathEnv: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: {
|
||||
includeExtensionless?: boolean;
|
||||
fallbackToLoginShell?: boolean;
|
||||
withPathEnv?: false;
|
||||
},
|
||||
): string | undefined;
|
||||
export function resolveExecutableFromPathEnv(
|
||||
executable: string,
|
||||
pathEnv: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: {
|
||||
includeExtensionless?: boolean;
|
||||
fallbackToLoginShell?: boolean;
|
||||
withPathEnv?: boolean;
|
||||
},
|
||||
): string | { executable: string; pathEnv?: string } | undefined {
|
||||
let resolution: { executable: string; pathEnv?: string } | undefined;
|
||||
if (!options?.fallbackToLoginShell) {
|
||||
const resolved = resolveExecutableFromPathEnvDirect(executable, pathEnv, env, options);
|
||||
resolution = resolved ? { executable: resolved } : undefined;
|
||||
} else {
|
||||
// Local catalog terminals launch with this same login-shell PATH. Carry it
|
||||
// forward because npm-style launchers may use `#!/usr/bin/env node`.
|
||||
const shellEnv = env ?? process.env;
|
||||
resolution = resolveExecutableFromUserShellPathWithPathEnv(executable, {
|
||||
env: shellEnv,
|
||||
pathEnv,
|
||||
includeExtensionless: options.includeExtensionless,
|
||||
});
|
||||
}
|
||||
return options?.withPathEnv ? resolution : resolution?.executable;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,14 @@ export type SessionCatalogContinueProviderParams = Omit<
|
||||
export type SessionCatalogArchiveProviderParams = Omit<SessionsCatalogArchiveParams, "catalogId">;
|
||||
|
||||
export type SessionCatalogTerminalPlan =
|
||||
| { kind: "local"; argv: string[]; cwd?: string; title?: string }
|
||||
| {
|
||||
kind: "local";
|
||||
argv: string[];
|
||||
cwd?: string;
|
||||
title?: string;
|
||||
/** PATH that resolved argv[0], needed by env-based script interpreters. */
|
||||
pathEnv?: string;
|
||||
}
|
||||
| {
|
||||
kind: "node";
|
||||
nodeId: string;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { TerminalConnection, type TerminalGatewayClient } from "./terminal-connection.ts";
|
||||
import {
|
||||
TerminalConnection,
|
||||
type TerminalGatewayClient,
|
||||
TerminalOpenTimeoutError,
|
||||
} from "./terminal-connection.ts";
|
||||
|
||||
const TERMINAL_LIVENESS_IDLE_MS = 20_000;
|
||||
const TERMINAL_LIVENESS_PROBE_TIMEOUT_MS = 5_000;
|
||||
const TERMINAL_OPEN_WATCHDOG_MS = 35_000;
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
@@ -75,7 +80,11 @@ describe("TerminalConnection", () => {
|
||||
);
|
||||
|
||||
expect(result.sessionId).toBe("s1");
|
||||
expect(client.requests[0]).toEqual({ method: "terminal.open", params: { cols: 80, rows: 24 } });
|
||||
expect(client.requests[0]).toEqual({
|
||||
method: "terminal.open",
|
||||
params: { cols: 80, rows: 24 },
|
||||
options: { timeoutMs: TERMINAL_OPEN_WATCHDOG_MS },
|
||||
});
|
||||
|
||||
client.emit("terminal.data", { sessionId: "s1", seq: 5, data: "hello" });
|
||||
client.emit("terminal.data", { sessionId: "s1", seq: 6, data: "!" });
|
||||
@@ -476,9 +485,49 @@ describe("TerminalConnection", () => {
|
||||
expect(client.requests[0]).toEqual({
|
||||
method: "terminal.open",
|
||||
params: { agentId: "ops", cols: 100, rows: 30 },
|
||||
options: { timeoutMs: TERMINAL_OPEN_WATCHDOG_MS },
|
||||
});
|
||||
});
|
||||
|
||||
it("maps the Gateway's request-scoped terminal open deadline", async () => {
|
||||
const client = makeFakeClient();
|
||||
client.request = <T>(
|
||||
method: string,
|
||||
params?: unknown,
|
||||
options?: { timeoutMs?: number | null },
|
||||
) => {
|
||||
client.requests.push({ method, params, ...(options ? { options } : {}) });
|
||||
return Promise.reject(new Error("terminal open timed out")) as Promise<T>;
|
||||
};
|
||||
const conn = new TerminalConnection(client);
|
||||
|
||||
await expect(
|
||||
conn.open({ cols: 80, rows: 24 }, { onData: () => {}, onExit: () => {} }),
|
||||
).rejects.toBeInstanceOf(TerminalOpenTimeoutError);
|
||||
expect(client.requests[0]?.options).toEqual({ timeoutMs: TERMINAL_OPEN_WATCHDOG_MS });
|
||||
expect(client.forceReconnects).toEqual([]);
|
||||
});
|
||||
|
||||
it("reconnects when the browser watchdog cannot receive the Gateway deadline", async () => {
|
||||
const client = makeFakeClient();
|
||||
client.request = <T>(
|
||||
method: string,
|
||||
params?: unknown,
|
||||
options?: { timeoutMs?: number | null },
|
||||
) => {
|
||||
client.requests.push({ method, params, ...(options ? { options } : {}) });
|
||||
return Promise.reject(
|
||||
new Error(`gateway request timed out after ${options?.timeoutMs}ms: ${method}`),
|
||||
) as Promise<T>;
|
||||
};
|
||||
const conn = new TerminalConnection(client);
|
||||
|
||||
await expect(
|
||||
conn.open({ cols: 80, rows: 24 }, { onData: () => {}, onExit: () => {} }),
|
||||
).rejects.toBeInstanceOf(TerminalOpenTimeoutError);
|
||||
expect(client.forceReconnects).toEqual(["terminal open watchdog timeout"]);
|
||||
});
|
||||
|
||||
it("forwards a typed catalog reference and preserves the returned title", async () => {
|
||||
const client = makeFakeClient();
|
||||
client.nextResponse = {
|
||||
@@ -499,6 +548,7 @@ describe("TerminalConnection", () => {
|
||||
expect(client.requests[0]).toEqual({
|
||||
method: "terminal.open",
|
||||
params: { cols: 100, rows: 30, catalog },
|
||||
options: { timeoutMs: TERMINAL_OPEN_WATCHDOG_MS },
|
||||
});
|
||||
expect(result.title).toBe("codex resume 0d5c…");
|
||||
});
|
||||
|
||||
@@ -13,7 +13,8 @@ export interface TerminalGatewayClient {
|
||||
): Promise<T>;
|
||||
addEventListener(listener: (evt: { event: string; payload: unknown }) => void): () => void;
|
||||
inboundActivitySeq?: number;
|
||||
forceReconnect?(reason: string): void;
|
||||
/** Recovers unreplayable output gaps and half-open terminal streams. */
|
||||
forceReconnect(reason: string): void;
|
||||
}
|
||||
|
||||
type TerminalOpenResult = {
|
||||
@@ -75,6 +76,29 @@ type PendingEvent =
|
||||
|
||||
const TERMINAL_LIVENESS_IDLE_MS = 20_000;
|
||||
const TERMINAL_LIVENESS_PROBE_TIMEOUT_MS = 5_000;
|
||||
// The Gateway owns the 30s open deadline. This longer browser watchdog only
|
||||
// recovers a half-open socket when the Gateway's response cannot arrive.
|
||||
const TERMINAL_OPEN_WATCHDOG_MS = 35_000;
|
||||
export class TerminalOpenTimeoutError extends Error {
|
||||
constructor(cause: unknown) {
|
||||
super("terminal open timed out", { cause });
|
||||
this.name = "TerminalOpenTimeoutError";
|
||||
}
|
||||
}
|
||||
|
||||
function isTerminalOpenRequestTimeout(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
/^gateway request timed out after \d+ms: terminal\.open$/u.test(error.message)
|
||||
);
|
||||
}
|
||||
|
||||
function isTerminalOpenTimeout(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
(error.message === "terminal open timed out" || isTerminalOpenRequestTimeout(error))
|
||||
);
|
||||
}
|
||||
|
||||
/** Routes the shared terminal event stream to the session that owns each id. */
|
||||
export class TerminalConnection {
|
||||
@@ -161,9 +185,24 @@ export class TerminalConnection {
|
||||
params: { agentId?: string; cols: number; rows: number; catalog?: TerminalCatalogReference },
|
||||
sink: SessionSink,
|
||||
): Promise<TerminalOpenResult> {
|
||||
const result = await this.requestWhileHoldingStream(() =>
|
||||
this.client.request<TerminalOpenResult>("terminal.open", params),
|
||||
);
|
||||
let result: TerminalOpenResult;
|
||||
try {
|
||||
result = await this.requestWhileHoldingStream(() =>
|
||||
this.client.request<TerminalOpenResult>("terminal.open", params, {
|
||||
timeoutMs: TERMINAL_OPEN_WATCHDOG_MS,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isTerminalOpenTimeout(error)) {
|
||||
throw error;
|
||||
}
|
||||
if (isTerminalOpenRequestTimeout(error)) {
|
||||
// The server should answer first. A later browser timeout means this
|
||||
// socket cannot carry the response, so disconnect to cancel ownership.
|
||||
this.client.forceReconnect("terminal open watchdog timeout");
|
||||
}
|
||||
throw new TerminalOpenTimeoutError(error);
|
||||
}
|
||||
this.adoptSession(result.sessionId, sink, { seqMode: "unknown", expectedSeq: 0 });
|
||||
return result;
|
||||
}
|
||||
@@ -301,7 +340,7 @@ export class TerminalConnection {
|
||||
// would duplicate bytes already rendered before the detected gap.
|
||||
stream.recovering = false;
|
||||
this.pending.delete(sessionId);
|
||||
this.client.forceReconnect?.("terminal replay reset unavailable");
|
||||
this.client.forceReconnect("terminal replay reset unavailable");
|
||||
return;
|
||||
}
|
||||
stream.sink.onReplay(result.buffer);
|
||||
@@ -331,7 +370,7 @@ export class TerminalConnection {
|
||||
}
|
||||
stream.recovering = false;
|
||||
this.pending.delete(sessionId);
|
||||
this.client.forceReconnect?.("terminal replay failed");
|
||||
this.client.forceReconnect("terminal replay failed");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -437,7 +476,7 @@ export class TerminalConnection {
|
||||
const activityNow = this.client.inboundActivitySeq ?? this.inboundActivityVersion;
|
||||
if (activityNow === activityBefore) {
|
||||
this.lastTerminalActivityAtMs = Date.now();
|
||||
this.client.forceReconnect?.("terminal liveness timeout");
|
||||
this.client.forceReconnect("terminal liveness timeout");
|
||||
} else {
|
||||
// Any valid inbound frame proves the socket is not half-open.
|
||||
this.lastTerminalActivityAtMs = Date.now();
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "../../i18n/index.ts";
|
||||
import { createStorageMock } from "../../test-helpers/storage.ts";
|
||||
import type { TerminalGatewayClient } from "./terminal-connection.ts";
|
||||
import { OpenClawTerminalPanel } from "./terminal-panel.ts";
|
||||
import type { createIsolatedGhosttyTerminal } from "./terminal-runtime.ts";
|
||||
|
||||
function createTerminalController() {
|
||||
return {
|
||||
readOnly: false,
|
||||
terminal: {
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
viewportY: 0,
|
||||
write: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
paste: vi.fn(),
|
||||
},
|
||||
write: vi.fn(),
|
||||
fit: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
setReadOnly: vi.fn(),
|
||||
attach: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
const createTerminal = vi.fn(async () => createTerminalController());
|
||||
|
||||
class ReadinessTestTerminalPanel extends OpenClawTerminalPanel {
|
||||
protected override createTerminal =
|
||||
createTerminal as unknown as typeof createIsolatedGhosttyTerminal;
|
||||
}
|
||||
|
||||
const TERMINAL_PANEL_ELEMENT_NAME = `test-terminal-panel-readiness-${crypto.randomUUID()}`;
|
||||
customElements.define(TERMINAL_PANEL_ELEMENT_NAME, ReadinessTestTerminalPanel);
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((next) => {
|
||||
resolve = next;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function terminalOpenResult(sessionId: string) {
|
||||
return {
|
||||
sessionId,
|
||||
agentId: "main",
|
||||
shell: "/bin/zsh",
|
||||
cwd: "/work",
|
||||
confined: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("terminal panel readiness", () => {
|
||||
beforeEach(async () => {
|
||||
vi.stubGlobal("localStorage", createStorageMock());
|
||||
vi.stubGlobal("sessionStorage", createStorageMock());
|
||||
await i18n.setLocale("en");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
document.body.replaceChildren();
|
||||
createTerminal.mockClear();
|
||||
vi.unstubAllGlobals();
|
||||
await i18n.setLocale("en");
|
||||
});
|
||||
|
||||
it("shows a connecting animation while a terminal open is in flight", async () => {
|
||||
const open = deferred<{
|
||||
sessionId: string;
|
||||
agentId: string;
|
||||
shell: string;
|
||||
cwd: string;
|
||||
confined: boolean;
|
||||
}>();
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: <T>(method: string) =>
|
||||
(method === "terminal.open" ? open.promise : Promise.resolve({})) as Promise<T>,
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
panel.toggle();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector(".tp-connecting")?.textContent).toContain(
|
||||
"Connecting to session",
|
||||
);
|
||||
});
|
||||
expect(panel.renderRoot.querySelector(".tp-tab")?.classList.contains("is-connecting")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
open.resolve(terminalOpenResult("session-1"));
|
||||
await vi.waitFor(() => expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull());
|
||||
expect(panel.renderRoot.querySelector(".tp-tab")?.classList.contains("is-live")).toBe(true);
|
||||
});
|
||||
|
||||
it("persists a catalog tab after its first output arrives", async () => {
|
||||
const controller = createTerminalController();
|
||||
createTerminal.mockResolvedValue(controller);
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
let listener: ((event: { event: string; payload: unknown }) => void) | undefined;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
return {
|
||||
...terminalOpenResult("catalog-terminal-1"),
|
||||
title: "codex resume 0d5c…",
|
||||
} as T;
|
||||
},
|
||||
addEventListener: (nextListener) => {
|
||||
listener = nextListener;
|
||||
return () => {
|
||||
listener = undefined;
|
||||
};
|
||||
},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
const catalog = { catalogId: "codex", hostId: "node:mac", threadId: "thread" };
|
||||
|
||||
panel.handleToggleRequest(new CustomEvent("openclaw:terminal-toggle", { detail: { catalog } }));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(requests).toContainEqual({
|
||||
method: "terminal.open",
|
||||
params: { agentId: undefined, cols: 100, rows: 30, catalog },
|
||||
});
|
||||
});
|
||||
expect(panel.renderRoot.querySelector(".tp-tab")?.textContent).toContain("codex resume 0d5c…");
|
||||
expect(panel.renderRoot.querySelector(".tp-connecting")?.textContent).toContain(
|
||||
"Connecting to session",
|
||||
);
|
||||
|
||||
listener?.({
|
||||
event: "terminal.data",
|
||||
payload: { sessionId: "catalog-terminal-1", seq: 5, data: "ready" },
|
||||
});
|
||||
await vi.waitFor(() => expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull());
|
||||
expect(new TextDecoder().decode(controller.write.mock.calls[0]?.[0])).toBe("ready");
|
||||
expect(sessionStorage.getItem("openclaw.terminal.sessions.v1")).toBe(
|
||||
JSON.stringify(["catalog-terminal-1"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("marks a catalog terminal ready when its first visible output is a replay", async () => {
|
||||
const controller = createTerminalController();
|
||||
createTerminal.mockResolvedValue(controller);
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
let listener: ((event: { event: string; payload: unknown }) => void) | undefined;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
if (method === "terminal.attach") {
|
||||
return {
|
||||
...terminalOpenResult("catalog-terminal-1"),
|
||||
buffer: "recovered output",
|
||||
seq: 12,
|
||||
} as T;
|
||||
}
|
||||
return {
|
||||
...terminalOpenResult("catalog-terminal-1"),
|
||||
title: "claude --resume 1234…",
|
||||
} as T;
|
||||
},
|
||||
addEventListener: (nextListener) => {
|
||||
listener = nextListener;
|
||||
return () => {
|
||||
listener = undefined;
|
||||
};
|
||||
},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
|
||||
panel.handleToggleRequest(
|
||||
new CustomEvent("openclaw:terminal-toggle", {
|
||||
detail: { catalog: { catalogId: "anthropic", hostId: "node:mac", threadId: "thread" } },
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => expect(panel.renderRoot.querySelector(".tp-connecting")).not.toBeNull());
|
||||
|
||||
listener?.({
|
||||
event: "terminal.data",
|
||||
payload: { sessionId: "catalog-terminal-1", seq: 12, data: "gap" },
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull());
|
||||
expect(requests).toContainEqual({
|
||||
method: "terminal.attach",
|
||||
params: { sessionId: "catalog-terminal-1" },
|
||||
});
|
||||
expect(controller.terminal.reset).toHaveBeenCalledOnce();
|
||||
expect(new TextDecoder().decode(controller.write.mock.calls[0]?.[0])).toBe("recovered output");
|
||||
});
|
||||
|
||||
it("closes a catalog terminal and shows an error when no output arrives", async () => {
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
return (
|
||||
method === "terminal.open"
|
||||
? { ...terminalOpenResult("catalog-terminal-1"), title: "claude --resume 1234…" }
|
||||
: {}
|
||||
) as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
(panel as unknown as { catalogReadyTimeoutMs: number }).catalogReadyTimeoutMs = 5;
|
||||
document.body.append(panel);
|
||||
|
||||
panel.handleToggleRequest(
|
||||
new CustomEvent("openclaw:terminal-toggle", {
|
||||
detail: { catalog: { catalogId: "anthropic", hostId: "node:mac", threadId: "thread" } },
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector(".tp-error")?.textContent).toContain(
|
||||
"Session did not connect within 30 seconds",
|
||||
);
|
||||
});
|
||||
expect(requests).toContainEqual({
|
||||
method: "terminal.close",
|
||||
params: { sessionId: "catalog-terminal-1" },
|
||||
});
|
||||
expect(panel.renderRoot.querySelector(".tp-tab")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -106,6 +106,9 @@ export const terminalPanelStyles = css`
|
||||
.tp-tab.is-exited::part(base) {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.tp-tab.is-connecting .tp-tab__icon {
|
||||
animation: tp-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
.tp-tab__icon {
|
||||
display: inline-flex;
|
||||
color: var(--accent, #4ec9a8);
|
||||
@@ -267,9 +270,31 @@ export const terminalPanelStyles = css`
|
||||
.tp-host {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
padding: 6px 8px;
|
||||
caret-color: transparent;
|
||||
}
|
||||
.tp-connecting {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: var(--muted, #8a919e);
|
||||
background: color-mix(in srgb, var(--bg, #0e1015) 88%, transparent);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tp-connecting__spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid color-mix(in srgb, var(--accent, #ff5c5c) 24%, transparent);
|
||||
border-top-color: var(--accent, #ff5c5c);
|
||||
border-radius: 50%;
|
||||
animation: tp-spin 0.8s linear infinite;
|
||||
}
|
||||
.tp-empty,
|
||||
.tp-error {
|
||||
padding: 10px 12px;
|
||||
@@ -279,4 +304,20 @@ export const terminalPanelStyles = css`
|
||||
.tp-error {
|
||||
color: var(--danger, #ff6b6b);
|
||||
}
|
||||
@keyframes tp-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes tp-pulse {
|
||||
50% {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tp-connecting__spinner,
|
||||
.tp-tab.is-connecting .tp-tab__icon {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -8,7 +8,7 @@ export type TerminalPanelTab = {
|
||||
shellName: string | null;
|
||||
agentId: string | null;
|
||||
cwd: string | null;
|
||||
status: "live" | "exited";
|
||||
status: "connecting" | "live" | "exited";
|
||||
exitReason?: string;
|
||||
exitCode?: number | null;
|
||||
};
|
||||
@@ -28,6 +28,9 @@ function terminalTabHint(tab: TerminalPanelTab): string | null {
|
||||
}
|
||||
|
||||
function terminalTabStatusLabel(tab: TerminalPanelTab): string | null {
|
||||
if (tab.status === "connecting") {
|
||||
return t("terminal.connecting");
|
||||
}
|
||||
if (tab.status !== "exited") {
|
||||
return null;
|
||||
}
|
||||
@@ -60,7 +63,7 @@ export function renderTerminalPanelTabs(params: {
|
||||
return html`
|
||||
<wa-tab
|
||||
id=${`terminal-tab-${tab.id}`}
|
||||
class="tp-tab ${tab.status === "exited" ? "is-exited" : ""}"
|
||||
class="tp-tab is-${tab.status}"
|
||||
panel=${tab.id}
|
||||
aria-controls="terminal-tab-panel"
|
||||
title=${terminalTabHint(tab) || nothing}
|
||||
|
||||
@@ -56,6 +56,16 @@ function terminalUploadFile(name: string, content: string): File {
|
||||
return file;
|
||||
}
|
||||
|
||||
function terminalOpenResult(sessionId: string) {
|
||||
return {
|
||||
sessionId,
|
||||
agentId: "ops",
|
||||
shell: "/bin/zsh",
|
||||
cwd: "/work/ops",
|
||||
confined: false,
|
||||
};
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
@@ -80,12 +90,208 @@ describe("OpenClawTerminalPanel upload lifecycle", () => {
|
||||
await i18n.setLocale("en");
|
||||
});
|
||||
|
||||
it("uploads dropped files and pastes shell-safe paths without executing", async () => {
|
||||
const controller = createTerminalController();
|
||||
createGhosttyTerminalMock.mockResolvedValue(controller);
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
if (method === "terminal.open") {
|
||||
return terminalOpenResult("session-1") as T;
|
||||
}
|
||||
if (method === "terminal.upload") {
|
||||
return { path: "/tmp/openclaw upload/scan final.pdf", size: 3 } as T;
|
||||
}
|
||||
return {} as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
panel.toggle();
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
});
|
||||
expect(panel.renderRoot.querySelector<HTMLInputElement>(".tp-file-input")?.multiple).toBe(true);
|
||||
|
||||
const file = new File(["pdf"], "scan final.pdf", { type: "application/pdf" });
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: async () => new TextEncoder().encode("pdf").buffer,
|
||||
});
|
||||
const drop = new Event("drop", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(drop, "dataTransfer", {
|
||||
value: { types: ["Files"], files: [file], dropEffect: "none" },
|
||||
});
|
||||
panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(requests).toContainEqual({
|
||||
method: "terminal.upload",
|
||||
params: {
|
||||
sessionId: "session-1",
|
||||
name: "scan final.pdf",
|
||||
contentBase64: "cGRm",
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(controller.terminal.paste).toHaveBeenCalledWith("'/tmp/openclaw upload/scan final.pdf'");
|
||||
expect(controller.terminal.paste).not.toHaveBeenCalledWith(expect.stringContaining("\n"));
|
||||
});
|
||||
|
||||
it("shows file progress and retries only the failed remainder", async () => {
|
||||
const controller = createTerminalController();
|
||||
createGhosttyTerminalMock.mockResolvedValue(controller);
|
||||
const requests: Array<{ method: string; params: unknown; signal?: AbortSignal }> = [];
|
||||
const failedUpload = deferred<{ path: string; size: number }>();
|
||||
let notesAttempts = 0;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown, options?: { signal?: AbortSignal }) => {
|
||||
requests.push({ method, params, signal: options?.signal });
|
||||
if (method === "terminal.open") {
|
||||
return terminalOpenResult("session-1") as T;
|
||||
}
|
||||
if (method === "terminal.upload") {
|
||||
const name = (params as { name: string }).name;
|
||||
if (name === "scan final.pdf") {
|
||||
return { path: "/tmp/openclaw upload/scan final.pdf", size: 3 } as T;
|
||||
}
|
||||
notesAttempts += 1;
|
||||
if (notesAttempts === 1) {
|
||||
return (await failedUpload.promise) as T;
|
||||
}
|
||||
return { path: "/tmp/openclaw upload/notes.txt", size: 4 } as T;
|
||||
}
|
||||
return {} as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
panel.toggle();
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
});
|
||||
|
||||
const drop = new Event("drop", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(drop, "dataTransfer", {
|
||||
value: {
|
||||
types: ["Files"],
|
||||
files: [
|
||||
terminalUploadFile("scan final.pdf", "pdf"),
|
||||
terminalUploadFile("notes.txt", "note"),
|
||||
],
|
||||
dropEffect: "none",
|
||||
},
|
||||
});
|
||||
panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const progress = panel.renderRoot.querySelector(".tp-upload-progress");
|
||||
expect(progress?.getAttribute("aria-valuenow")).toBe("1");
|
||||
expect(progress?.getAttribute("aria-valuemax")).toBe("2");
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain(
|
||||
"Uploading 2 of 2",
|
||||
);
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain("notes.txt");
|
||||
});
|
||||
expect(controller.terminal.paste).not.toHaveBeenCalled();
|
||||
|
||||
failedUpload.reject(
|
||||
Object.assign(new Error("paired node went offline"), {
|
||||
gatewayCode: "UNAVAILABLE",
|
||||
retryable: false,
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
const failed = panel.renderRoot.querySelector(".tp-upload-card--failed");
|
||||
expect(failed?.textContent).toContain("Upload failed");
|
||||
expect(failed?.textContent).toContain("paired node went offline");
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload-retry")).not.toBeNull();
|
||||
});
|
||||
panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload-retry")?.click();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(controller.terminal.paste).toHaveBeenCalledWith(
|
||||
"'/tmp/openclaw upload/scan final.pdf' '/tmp/openclaw upload/notes.txt'",
|
||||
);
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")).toBeNull();
|
||||
});
|
||||
expect(
|
||||
requests
|
||||
.filter(({ method }) => method === "terminal.upload")
|
||||
.map(({ params }) => (params as { name: string }).name),
|
||||
).toEqual(["scan final.pdf", "notes.txt", "notes.txt"]);
|
||||
});
|
||||
|
||||
it("cancels an active batch without pasting staged paths", async () => {
|
||||
const controller = createTerminalController();
|
||||
createGhosttyTerminalMock.mockResolvedValue(controller);
|
||||
const pendingUpload = deferred<{ path: string; size: number }>();
|
||||
let uploadSignal: AbortSignal | undefined;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, _params?: unknown, options?: { signal?: AbortSignal }) => {
|
||||
if (method === "terminal.open") {
|
||||
return terminalOpenResult("session-1") as T;
|
||||
}
|
||||
if (method === "terminal.upload") {
|
||||
uploadSignal = options?.signal;
|
||||
return (await pendingUpload.promise) as T;
|
||||
}
|
||||
return {} as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
panel.toggle();
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
});
|
||||
|
||||
const drop = new Event("drop", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(drop, "dataTransfer", {
|
||||
value: {
|
||||
types: ["Files"],
|
||||
files: [terminalUploadFile("archive.zip", "zip")],
|
||||
dropEffect: "none",
|
||||
},
|
||||
});
|
||||
panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop);
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain(
|
||||
"Uploading 1 of 1",
|
||||
);
|
||||
});
|
||||
|
||||
panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload-cancel")?.click();
|
||||
await panel.updateComplete;
|
||||
expect(uploadSignal?.aborted).toBe(true);
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")).toBeNull();
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
|
||||
pendingUpload.resolve({ path: "/tmp/openclaw upload/archive.zip", size: 3 });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(controller.terminal.paste).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels a pending upload when its terminal tab closes", async () => {
|
||||
const controller = createTerminalController();
|
||||
createGhosttyTerminalMock.mockResolvedValue(controller);
|
||||
const pendingUpload = deferred<{ path: string; size: number }>();
|
||||
let uploadSignal: AbortSignal | undefined;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, _params?: unknown, options?: { signal?: AbortSignal }) => {
|
||||
if (method === "terminal.open") {
|
||||
return {
|
||||
|
||||
@@ -50,14 +50,6 @@ function terminalOpenResult(sessionId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function terminalUploadFile(name: string, content: string): File {
|
||||
const file = new File([content], name);
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: async () => new TextEncoder().encode(content).buffer,
|
||||
});
|
||||
return file;
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
@@ -89,6 +81,7 @@ async function startPanelWithPendingOpen() {
|
||||
const open = deferred<ReturnType<typeof terminalOpenResult>>();
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
return (method === "terminal.open" ? open.promise : Promise.resolve({})) as Promise<T>;
|
||||
@@ -147,6 +140,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
});
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
return {
|
||||
@@ -253,37 +247,6 @@ describe("OpenClawTerminalPanel", () => {
|
||||
expect(requests.some(({ method }) => method === "terminal.input")).toBe(false);
|
||||
});
|
||||
|
||||
it("opens a new titled tab for a catalog toggle request", async () => {
|
||||
createGhosttyTerminalMock.mockImplementation(async () => createTerminalController());
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
return {
|
||||
...terminalOpenResult("catalog-terminal-1"),
|
||||
title: "codex resume 0d5c…",
|
||||
} as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
const catalog = { catalogId: "codex", hostId: "node:mac", threadId: "thread" };
|
||||
|
||||
panel.handleToggleRequest(new CustomEvent("openclaw:terminal-toggle", { detail: { catalog } }));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(requests).toContainEqual({
|
||||
method: "terminal.open",
|
||||
params: { agentId: undefined, cols: 100, rows: 30, catalog },
|
||||
});
|
||||
});
|
||||
await panel.updateComplete;
|
||||
expect(panel.renderRoot.querySelector(".tp-tab")?.textContent).toContain("codex resume 0d5c…");
|
||||
});
|
||||
|
||||
it("reattaches persisted sessions before opening a catalog tab", async () => {
|
||||
sessionStorage.setItem("openclaw.terminal.sessions.v1", JSON.stringify(["persisted-1"]));
|
||||
createGhosttyTerminalMock
|
||||
@@ -291,6 +254,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
.mockResolvedValueOnce(createTerminalController());
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
if (method === "terminal.list") {
|
||||
@@ -337,7 +301,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
requests.findIndex((entry) => entry.method === "terminal.open"),
|
||||
);
|
||||
expect(sessionStorage.getItem("openclaw.terminal.sessions.v1")).toBe(
|
||||
JSON.stringify(["persisted-1", "catalog-terminal-1"]),
|
||||
JSON.stringify(["persisted-1"]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -353,6 +317,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
.mockResolvedValueOnce(controllers[2]);
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
if (method === "terminal.open") {
|
||||
@@ -459,6 +424,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
const secondList = deferred<{ sessions: ListedSession[] }>();
|
||||
let listCount = 0;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: <T>(method: string) => {
|
||||
if (method === "terminal.open") {
|
||||
return Promise.resolve(terminalOpenResult("current-1")) as Promise<T>;
|
||||
@@ -523,6 +489,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
.mockResolvedValueOnce(createTerminalController())
|
||||
.mockResolvedValueOnce(createTerminalController());
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string) => {
|
||||
if (method === "terminal.open") {
|
||||
return terminalOpenResult("current-1") as T;
|
||||
@@ -565,6 +532,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
let openCount = 0;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
if (method === "terminal.open") {
|
||||
@@ -599,6 +567,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
createGhosttyTerminalMock.mockImplementation(async () => createTerminalController());
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
return {
|
||||
@@ -647,6 +616,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
let listener: ((event: { event: string; payload: unknown }) => void) | undefined;
|
||||
let openCount = 0;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
if (method === "terminal.open") {
|
||||
@@ -716,6 +686,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
const oldRequests: string[] = [];
|
||||
const oldUnsubscribe = vi.fn();
|
||||
const oldClient: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string) => {
|
||||
oldRequests.push(method);
|
||||
return (method === "terminal.open" ? terminalOpenResult("old-session") : {}) as T;
|
||||
@@ -724,6 +695,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
};
|
||||
const newRequests: string[] = [];
|
||||
const newClient: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string) => {
|
||||
newRequests.push(method);
|
||||
if (method === "terminal.list") {
|
||||
@@ -763,6 +735,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
.mockResolvedValueOnce(currentController);
|
||||
const requests: string[] = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string) => {
|
||||
requests.push(method);
|
||||
return (method === "terminal.open" ? terminalOpenResult("current-session") : {}) as T;
|
||||
@@ -804,6 +777,7 @@ describe("OpenClawTerminalPanel", () => {
|
||||
it("removes resize listeners when disconnected mid-drag", async () => {
|
||||
createGhosttyTerminalMock.mockResolvedValue(createTerminalController());
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string) =>
|
||||
(method === "terminal.open" ? terminalOpenResult("session-1") : {}) as T,
|
||||
addEventListener: () => () => {},
|
||||
@@ -847,202 +821,11 @@ describe("OpenClawTerminalPanel", () => {
|
||||
expect(host.isConnected).toBe(false);
|
||||
});
|
||||
|
||||
it("uploads dropped files and pastes shell-safe paths without executing", async () => {
|
||||
const controller = createTerminalController();
|
||||
createGhosttyTerminalMock.mockResolvedValue(controller);
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const client: TerminalGatewayClient = {
|
||||
request: async <T>(method: string, params?: unknown) => {
|
||||
requests.push({ method, params });
|
||||
if (method === "terminal.open") {
|
||||
return terminalOpenResult("session-1") as T;
|
||||
}
|
||||
if (method === "terminal.upload") {
|
||||
return { path: "/tmp/openclaw upload/scan final.pdf", size: 3 } as T;
|
||||
}
|
||||
return {} as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
panel.toggle();
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
});
|
||||
expect(panel.renderRoot.querySelector<HTMLInputElement>(".tp-file-input")?.multiple).toBe(true);
|
||||
|
||||
const file = new File(["pdf"], "scan final.pdf", { type: "application/pdf" });
|
||||
Object.defineProperty(file, "arrayBuffer", {
|
||||
value: async () => new TextEncoder().encode("pdf").buffer,
|
||||
});
|
||||
const drop = new Event("drop", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(drop, "dataTransfer", {
|
||||
value: { types: ["Files"], files: [file], dropEffect: "none" },
|
||||
});
|
||||
panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(requests).toContainEqual({
|
||||
method: "terminal.upload",
|
||||
params: {
|
||||
sessionId: "session-1",
|
||||
name: "scan final.pdf",
|
||||
contentBase64: "cGRm",
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(controller.terminal.paste).toHaveBeenCalledWith("'/tmp/openclaw upload/scan final.pdf'");
|
||||
expect(controller.terminal.paste).not.toHaveBeenCalledWith(expect.stringContaining("\n"));
|
||||
});
|
||||
|
||||
it("shows file progress and retries only the failed remainder", async () => {
|
||||
const controller = createTerminalController();
|
||||
createGhosttyTerminalMock.mockResolvedValue(controller);
|
||||
const requests: Array<{ method: string; params: unknown; signal?: AbortSignal }> = [];
|
||||
const failedUpload = deferred<{ path: string; size: number }>();
|
||||
let notesAttempts = 0;
|
||||
const client: TerminalGatewayClient = {
|
||||
request: async <T>(method: string, params?: unknown, options?: { signal?: AbortSignal }) => {
|
||||
requests.push({ method, params, signal: options?.signal });
|
||||
if (method === "terminal.open") {
|
||||
return terminalOpenResult("session-1") as T;
|
||||
}
|
||||
if (method === "terminal.upload") {
|
||||
const name = (params as { name: string }).name;
|
||||
if (name === "scan final.pdf") {
|
||||
return { path: "/tmp/openclaw upload/scan final.pdf", size: 3 } as T;
|
||||
}
|
||||
notesAttempts += 1;
|
||||
if (notesAttempts === 1) {
|
||||
return (await failedUpload.promise) as T;
|
||||
}
|
||||
return { path: "/tmp/openclaw upload/notes.txt", size: 4 } as T;
|
||||
}
|
||||
return {} as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
panel.toggle();
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
});
|
||||
|
||||
const drop = new Event("drop", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(drop, "dataTransfer", {
|
||||
value: {
|
||||
types: ["Files"],
|
||||
files: [
|
||||
terminalUploadFile("scan final.pdf", "pdf"),
|
||||
terminalUploadFile("notes.txt", "note"),
|
||||
],
|
||||
dropEffect: "none",
|
||||
},
|
||||
});
|
||||
panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const progress = panel.renderRoot.querySelector(".tp-upload-progress");
|
||||
expect(progress?.getAttribute("aria-valuenow")).toBe("1");
|
||||
expect(progress?.getAttribute("aria-valuemax")).toBe("2");
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain(
|
||||
"Uploading 2 of 2",
|
||||
);
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain("notes.txt");
|
||||
});
|
||||
expect(controller.terminal.paste).not.toHaveBeenCalled();
|
||||
|
||||
failedUpload.reject(
|
||||
Object.assign(new Error("paired node went offline"), {
|
||||
gatewayCode: "UNAVAILABLE",
|
||||
retryable: false,
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
const failed = panel.renderRoot.querySelector(".tp-upload-card--failed");
|
||||
expect(failed?.textContent).toContain("Upload failed");
|
||||
expect(failed?.textContent).toContain("paired node went offline");
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload-retry")).not.toBeNull();
|
||||
});
|
||||
panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload-retry")?.click();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(controller.terminal.paste).toHaveBeenCalledWith(
|
||||
"'/tmp/openclaw upload/scan final.pdf' '/tmp/openclaw upload/notes.txt'",
|
||||
);
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")).toBeNull();
|
||||
});
|
||||
expect(
|
||||
requests
|
||||
.filter(({ method }) => method === "terminal.upload")
|
||||
.map(({ params }) => (params as { name: string }).name),
|
||||
).toEqual(["scan final.pdf", "notes.txt", "notes.txt"]);
|
||||
});
|
||||
|
||||
it("cancels an active batch without pasting staged paths", async () => {
|
||||
const controller = createTerminalController();
|
||||
createGhosttyTerminalMock.mockResolvedValue(controller);
|
||||
const pendingUpload = deferred<{ path: string; size: number }>();
|
||||
let uploadSignal: AbortSignal | undefined;
|
||||
const client: TerminalGatewayClient = {
|
||||
request: async <T>(method: string, _params?: unknown, options?: { signal?: AbortSignal }) => {
|
||||
if (method === "terminal.open") {
|
||||
return terminalOpenResult("session-1") as T;
|
||||
}
|
||||
if (method === "terminal.upload") {
|
||||
uploadSignal = options?.signal;
|
||||
return (await pendingUpload.promise) as T;
|
||||
}
|
||||
return {} as T;
|
||||
},
|
||||
addEventListener: () => () => {},
|
||||
};
|
||||
const panel = document.createElement(TERMINAL_PANEL_ELEMENT_NAME) as OpenClawTerminalPanel;
|
||||
panel.client = client;
|
||||
panel.available = true;
|
||||
document.body.append(panel);
|
||||
panel.toggle();
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
});
|
||||
|
||||
const drop = new Event("drop", { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(drop, "dataTransfer", {
|
||||
value: {
|
||||
types: ["Files"],
|
||||
files: [terminalUploadFile("archive.zip", "zip")],
|
||||
dropEffect: "none",
|
||||
},
|
||||
});
|
||||
panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop);
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain(
|
||||
"Uploading 1 of 1",
|
||||
);
|
||||
});
|
||||
|
||||
panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload-cancel")?.click();
|
||||
await panel.updateComplete;
|
||||
expect(uploadSignal?.aborted).toBe(true);
|
||||
expect(panel.renderRoot.querySelector(".tp-upload-card")).toBeNull();
|
||||
expect(panel.renderRoot.querySelector<HTMLButtonElement>(".tp-upload")?.disabled).toBe(false);
|
||||
|
||||
pendingUpload.resolve({ path: "/tmp/openclaw upload/archive.zip", size: 3 });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(controller.terminal.paste).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retranslates cached exit state when the locale changes", async () => {
|
||||
createGhosttyTerminalMock.mockResolvedValue(createTerminalController());
|
||||
let listener: ((event: { event: string; payload: unknown }) => void) | undefined;
|
||||
const client: TerminalGatewayClient = {
|
||||
forceReconnect: () => {},
|
||||
request: async <T>(method: string) =>
|
||||
(method === "terminal.open" ? terminalOpenResult("session-1") : {}) as T,
|
||||
addEventListener: (nextListener) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import {
|
||||
TerminalConnection,
|
||||
type TerminalGatewayClient,
|
||||
TerminalOpenTimeoutError,
|
||||
type TerminalSessionInfo,
|
||||
} from "./terminal-connection.ts";
|
||||
import { terminalPanelStyles } from "./terminal-panel-styles.ts";
|
||||
@@ -35,19 +36,24 @@ import {
|
||||
persistTerminalSessionIds,
|
||||
} from "./terminal-session-storage.ts";
|
||||
import { createTerminalStartupInput, type StartupInputBuffer } from "./terminal-startup-input.ts";
|
||||
import {
|
||||
TerminalTabReadinessController,
|
||||
type TerminalTabReadinessState,
|
||||
} from "./terminal-tab-readiness.ts";
|
||||
import { TerminalTaskQueue } from "./terminal-task-queue.ts";
|
||||
import { terminalTheme } from "./terminal-theme.ts";
|
||||
|
||||
type TerminalDock = DockPanelSide;
|
||||
type TerminalTabState = TerminalPanelTab & {
|
||||
gatewaySessionId: string;
|
||||
pendingInput: StartupInputBuffer;
|
||||
controller: GhosttyTerminalController;
|
||||
shell: string;
|
||||
host: HTMLDivElement;
|
||||
/** Why an in-flight open/attach must not adopt this disposed terminal. */
|
||||
cancelled?: "close" | "lifecycle";
|
||||
};
|
||||
type TerminalTabState = TerminalPanelTab &
|
||||
TerminalTabReadinessState & {
|
||||
gatewaySessionId: string;
|
||||
pendingInput: StartupInputBuffer;
|
||||
controller: GhosttyTerminalController;
|
||||
shell: string;
|
||||
host: HTMLDivElement;
|
||||
/** Why an in-flight open/attach must not adopt this disposed terminal. */
|
||||
cancelled?: "close" | "lifecycle";
|
||||
};
|
||||
|
||||
type TerminalOperation = {
|
||||
generation: number;
|
||||
@@ -72,6 +78,7 @@ const panelLayout = createDockPanelLayout({
|
||||
const TERMINAL_FONT_FAMILY =
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Symbols Nerd Font Mono", "MesloLGLDZ Nerd Font Mono", "JetBrainsMono Nerd Font Mono", "Liberation Mono", monospace';
|
||||
const TERMINAL_OUTPUT_ENCODER = new TextEncoder();
|
||||
const CATALOG_TERMINAL_READY_TIMEOUT_MS = 30_000;
|
||||
|
||||
/** `<openclaw-terminal-panel>` — the dockable Control UI shell surface. */
|
||||
export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
@@ -123,6 +130,21 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
});
|
||||
private readonly bootQueue = new TerminalTaskQueue();
|
||||
protected createTerminal = createIsolatedGhosttyTerminal;
|
||||
protected catalogReadyTimeoutMs = CATALOG_TERMINAL_READY_TIMEOUT_MS;
|
||||
private readonly readiness = new TerminalTabReadinessController<TerminalTabState>({
|
||||
timeoutMs: () => this.catalogReadyTimeoutMs,
|
||||
isCurrent: (tab) => this.tabs.includes(tab),
|
||||
onReady: () => {
|
||||
this.tabs = [...this.tabs];
|
||||
this.persistLiveSessions();
|
||||
},
|
||||
onTimeout: (tab) => {
|
||||
this.errorText = t("terminal.connectionTimedOut");
|
||||
void this.connection?.close(tab.gatewaySessionId);
|
||||
this.dropFailedTab(tab);
|
||||
this.persistLiveSessions();
|
||||
},
|
||||
});
|
||||
private readonly onGlobalKeyDown = (event: KeyboardEvent) => this.handleGlobalKey(event);
|
||||
private readonly onToggleRequest = (event: Event) => this.handleToggleRequest(event);
|
||||
// Re-clamp a dock sized on a larger window so the header/resizer never end
|
||||
@@ -461,7 +483,10 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
}
|
||||
|
||||
/** Boots a tab with a libterminal controller, ready for an open or attach RPC. */
|
||||
private async bootTab(operation: TerminalOperation): Promise<{
|
||||
private async bootTab(
|
||||
operation: TerminalOperation,
|
||||
options: { awaitFirstOutput?: boolean } = {},
|
||||
): Promise<{
|
||||
tab: TerminalTabState;
|
||||
connection: TerminalConnection;
|
||||
cols: number;
|
||||
@@ -528,7 +553,9 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
cwd: null,
|
||||
controller,
|
||||
host,
|
||||
status: "live",
|
||||
status: "connecting",
|
||||
awaitFirstOutput: options.awaitFirstOutput === true,
|
||||
readyTimer: null,
|
||||
};
|
||||
tabRef.current = tab;
|
||||
this.tabs = [...this.tabs, tab];
|
||||
@@ -545,6 +572,9 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
onData: (data: string) => {
|
||||
if (!tab.cancelled) {
|
||||
tab.controller.write(TERMINAL_OUTPUT_ENCODER.encode(data));
|
||||
if (data.length > 0) {
|
||||
this.readiness.markReady(tab);
|
||||
}
|
||||
}
|
||||
},
|
||||
// A replay is authoritative. Reset parser, screen, and scrollback so a
|
||||
@@ -554,10 +584,12 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
tab.controller.terminal.reset();
|
||||
if (data) {
|
||||
tab.controller.write(TERMINAL_OUTPUT_ENCODER.encode(data));
|
||||
this.readiness.markReady(tab);
|
||||
}
|
||||
}
|
||||
},
|
||||
onExit: (info: { reason?: string; exitCode: number | null }) => this.handleExit(tab.id, info),
|
||||
onExit: (info: { reason?: string; exitCode: number | null; error?: string }) =>
|
||||
this.handleExit(tab.id, info),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -579,6 +611,14 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
void this.connection?.input(result.sessionId, data);
|
||||
}
|
||||
|
||||
if (tab.status === "connecting") {
|
||||
if (tab.awaitFirstOutput) {
|
||||
this.readiness.arm(tab);
|
||||
} else {
|
||||
this.readiness.markReady(tab);
|
||||
}
|
||||
}
|
||||
|
||||
this.tabs = [...this.tabs];
|
||||
this.persistLiveSessions();
|
||||
}
|
||||
@@ -608,7 +648,7 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
// Tracked outside the try so the catch can dispose a tab whose open failed.
|
||||
let createdTab: TerminalTabState | undefined;
|
||||
try {
|
||||
const boot = await this.bootTab(operation);
|
||||
const boot = await this.bootTab(operation, { awaitFirstOutput: Boolean(catalog) });
|
||||
createdTab = boot.tab;
|
||||
const result = await boot.connection.open(
|
||||
{ agentId, cols: boot.cols, rows: boot.rows, ...(catalog ? { catalog } : {}) },
|
||||
@@ -637,7 +677,12 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
if (!this.isTerminalOperationCurrent(operation)) {
|
||||
return;
|
||||
}
|
||||
this.errorText = err instanceof Error ? err.message : String(err);
|
||||
this.errorText =
|
||||
err instanceof TerminalOpenTimeoutError
|
||||
? t("terminal.connectionTimedOut")
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: String(err);
|
||||
} finally {
|
||||
if (this.isTerminalOperationCurrent(operation)) {
|
||||
this.booting = false;
|
||||
@@ -677,14 +722,21 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private handleExit(tabId: string, info: { reason?: string; exitCode: number | null }): void {
|
||||
private handleExit(
|
||||
tabId: string,
|
||||
info: { reason?: string; exitCode: number | null; error?: string },
|
||||
): void {
|
||||
const tab = this.tabs.find((entry) => entry.id === tabId);
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
this.readiness.stop(tab);
|
||||
tab.status = "exited";
|
||||
tab.exitReason = info.reason;
|
||||
tab.exitCode = info.exitCode;
|
||||
if (info.error?.trim()) {
|
||||
this.errorText = info.error.trim();
|
||||
}
|
||||
// The connection drops its own sink on exit delivery, so no release() here —
|
||||
// the session id may not be recorded yet when an early exit is replayed.
|
||||
this.tabs = [...this.tabs];
|
||||
@@ -697,9 +749,9 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
return;
|
||||
}
|
||||
this.upload.cancelForTab(tab);
|
||||
if (tab.gatewaySessionId && tab.status === "live") {
|
||||
if (tab.gatewaySessionId && tab.status !== "exited") {
|
||||
void this.connection?.close(tab.gatewaySessionId);
|
||||
} else if (!tab.gatewaySessionId && tab.status === "live") {
|
||||
} else if (!tab.gatewaySessionId && tab.status !== "exited") {
|
||||
// Open still in flight: no session id to close yet. Flag it so the open
|
||||
// continuation closes the server session as soon as the RPC resolves.
|
||||
tab.cancelled = "close";
|
||||
@@ -760,6 +812,7 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
}
|
||||
|
||||
private disposeTab(tab: TerminalTabState): void {
|
||||
this.readiness.stop(tab);
|
||||
try {
|
||||
tab.controller.dispose();
|
||||
} catch {
|
||||
@@ -893,6 +946,9 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
: this.dock === "bottom"
|
||||
? `height:${this.height}px;--tp-panel-height:${this.height}px`
|
||||
: `width:${this.width}px`;
|
||||
const activeTab = this.tabs.find((tab) => tab.id === this.activeId);
|
||||
const connecting =
|
||||
(this.booting && this.tabs.length === 0) || activeTab?.status === "connecting";
|
||||
return html`
|
||||
<section class="tp tp--${mode}" style=${style} aria-label=${t("terminal.title")}>
|
||||
${this.fullscreen
|
||||
@@ -950,8 +1006,11 @@ export class OpenClawTerminalPanel extends OpenClawLitElement {
|
||||
@dragleave=${this.upload.handleDragLeave}
|
||||
@drop=${this.upload.handleDrop}
|
||||
>
|
||||
${this.booting && this.tabs.length === 0
|
||||
? html`<div class="tp-empty">${t("terminal.starting")}</div>`
|
||||
${connecting
|
||||
? html`<div class="tp-connecting" role="status">
|
||||
<span class="tp-connecting__spinner" aria-hidden="true"></span>
|
||||
<span>${t("terminal.connecting")}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
${renderTerminalUploadLayer(this.upload)}
|
||||
</wa-tab-panel>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { TerminalPanelTab } from "./terminal-panel-tabs.ts";
|
||||
|
||||
export type TerminalTabReadinessState = {
|
||||
awaitFirstOutput: boolean;
|
||||
readyTimer: ReturnType<typeof setTimeout> | null;
|
||||
};
|
||||
|
||||
type ReadyTab = TerminalPanelTab & TerminalTabReadinessState;
|
||||
|
||||
export class TerminalTabReadinessController<T extends ReadyTab> {
|
||||
constructor(
|
||||
private readonly options: {
|
||||
timeoutMs: () => number;
|
||||
isCurrent: (tab: T) => boolean;
|
||||
onReady: (tab: T) => void;
|
||||
onTimeout: (tab: T) => void;
|
||||
},
|
||||
) {}
|
||||
|
||||
markReady(tab: T): void {
|
||||
this.stop(tab);
|
||||
if (tab.status !== "connecting") {
|
||||
return;
|
||||
}
|
||||
tab.status = "live";
|
||||
this.options.onReady(tab);
|
||||
}
|
||||
|
||||
arm(tab: T): void {
|
||||
if (tab.readyTimer || tab.status !== "connecting" || !tab.awaitFirstOutput) {
|
||||
return;
|
||||
}
|
||||
// PTY creation only proves transport setup. Native CLIs are ready once they
|
||||
// emit their first byte; otherwise a broken relay looks like a blank shell.
|
||||
tab.readyTimer = setTimeout(() => {
|
||||
tab.readyTimer = null;
|
||||
if (!this.options.isCurrent(tab) || tab.status !== "connecting" || !tab.awaitFirstOutput) {
|
||||
return;
|
||||
}
|
||||
tab.awaitFirstOutput = false;
|
||||
this.options.onTimeout(tab);
|
||||
}, this.options.timeoutMs());
|
||||
}
|
||||
|
||||
stop(tab: T): void {
|
||||
if (tab.readyTimer) {
|
||||
clearTimeout(tab.readyTimer);
|
||||
tab.readyTimer = null;
|
||||
}
|
||||
tab.awaitFirstOutput = false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type Locator } from "playwright";
|
||||
import { chromium, type Browser, type Locator, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
@@ -131,6 +131,47 @@ function expectStableVirtualRowPrepend(
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
function resumableClaudeCatalog() {
|
||||
return {
|
||||
catalogs: [
|
||||
{
|
||||
id: "claude",
|
||||
label: "Claude Code",
|
||||
capabilities: { continueSession: true, archive: false },
|
||||
hosts: [
|
||||
{
|
||||
hostId: "gateway:local",
|
||||
label: "Local Mac",
|
||||
kind: "local",
|
||||
connected: true,
|
||||
sessions: [
|
||||
{
|
||||
threadId: "claude-terminal-session",
|
||||
name: "Native Claude terminal",
|
||||
status: "stored",
|
||||
source: "claude-cli",
|
||||
archived: false,
|
||||
canContinue: true,
|
||||
canArchive: false,
|
||||
canOpenTerminal: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function openClaudeCatalogTerminal(page: Page) {
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
const row = page.locator('[data-session-key^="catalog:"]').filter({
|
||||
hasText: "Native Claude terminal",
|
||||
});
|
||||
await row.click({ button: "right" });
|
||||
await page.locator('wa-dropdown-item[value="terminal"]').click();
|
||||
}
|
||||
|
||||
suite("Claude native session catalog", () => {
|
||||
beforeAll(async () => {
|
||||
if (!available) {
|
||||
@@ -145,6 +186,118 @@ suite("Claude native session catalog", () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("shows catalog connection progress until the first terminal output", async () => {
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
deferredMethods: ["terminal.open"],
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"sessions.catalog.list",
|
||||
"sessions.catalog.read",
|
||||
"terminal.open",
|
||||
],
|
||||
methodResponses: {
|
||||
"sessions.catalog.list": resumableClaudeCatalog(),
|
||||
"sessions.catalog.read": {
|
||||
hostId: "gateway:local",
|
||||
threadId: "claude-terminal-session",
|
||||
items: [{ type: "userMessage", text: "Continue the native session" }],
|
||||
},
|
||||
"terminal.list": { sessions: [] },
|
||||
},
|
||||
terminalEnabled: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await openClaudeCatalogTerminal(page);
|
||||
const open = await gateway.waitForRequest("terminal.open");
|
||||
expect(open.params).toMatchObject({
|
||||
catalog: {
|
||||
catalogId: "claude",
|
||||
hostId: "gateway:local",
|
||||
threadId: "claude-terminal-session",
|
||||
},
|
||||
});
|
||||
const connecting = page.getByRole("status").filter({ hasText: "Connecting to session" });
|
||||
await connecting.waitFor();
|
||||
expect(await page.locator(".tp-tab.is-connecting").count()).toBe(1);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(artifactDir, "claude-terminal-connecting.png") });
|
||||
}
|
||||
|
||||
await gateway.resolveDeferred("terminal.open", {
|
||||
agentId: "main",
|
||||
confined: false,
|
||||
cwd: "/workspace",
|
||||
sessionId: "claude-terminal-e2e",
|
||||
shell: "/bin/zsh",
|
||||
title: "claude --resume claude-termi…",
|
||||
});
|
||||
await expect.poll(() => connecting.count()).toBe(1);
|
||||
await gateway.emitGatewayEvent("terminal.data", {
|
||||
sessionId: "claude-terminal-e2e",
|
||||
seq: 17,
|
||||
data: "Claude Code ready\r\n",
|
||||
});
|
||||
await expect.poll(() => connecting.count()).toBe(0);
|
||||
expect(await page.locator(".tp-tab.is-live").count()).toBe(1);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("closes a catalog terminal that produces no output before the deadline", async () => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"sessions.catalog.list",
|
||||
"sessions.catalog.read",
|
||||
"terminal.open",
|
||||
],
|
||||
methodResponses: {
|
||||
"sessions.catalog.list": resumableClaudeCatalog(),
|
||||
"sessions.catalog.read": {
|
||||
hostId: "gateway:local",
|
||||
threadId: "claude-terminal-session",
|
||||
items: [],
|
||||
},
|
||||
"terminal.list": { sessions: [] },
|
||||
"terminal.open": {
|
||||
agentId: "main",
|
||||
confined: false,
|
||||
cwd: "/workspace",
|
||||
sessionId: "claude-terminal-timeout",
|
||||
shell: "/bin/zsh",
|
||||
title: "claude --resume claude-termi…",
|
||||
},
|
||||
},
|
||||
terminalEnabled: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await page.clock.install();
|
||||
await openClaudeCatalogTerminal(page);
|
||||
await gateway.waitForRequest("terminal.open");
|
||||
await page.getByRole("status").filter({ hasText: "Connecting to session" }).waitFor();
|
||||
await page.clock.runFor(30_001);
|
||||
|
||||
await page.getByText("Session did not connect within 30 seconds.", { exact: true }).waitFor();
|
||||
const close = await gateway.waitForRequest("terminal.close");
|
||||
expect(close.params).toEqual({ sessionId: "claude-terminal-timeout" });
|
||||
expect(await page.locator(".tp-tab").count()).toBe(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-loads older chat without moving the viewport and disables paired-node continuation", async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.clock.install();
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.115Z",
|
||||
"generatedAt": "2026-07-15T00:45:23.615Z",
|
||||
"locale": "ar",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -1549,7 +1549,9 @@
|
||||
{"cache_key":"dac4aa199d3b6e8b364499cd91adc775def2c7b3f9572aa1bef60e352abd0898","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.loadingMicrophones","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Loading microphones…","text_hash":"042a481c407f31b97b0cc8ff4e6c8b8f3f6e85d798cba8549c348d6d33a9945c","tgt_lang":"ar","translated":"جارٍ تحميل الميكروفونات…","updated_at":"2026-07-06T17:33:50.709Z"}
|
||||
{"cache_key":"db35b841b8920c0afee334b5565e2cba2f8a016fe1e3d56a0acf650971bdc2e5","model":"gpt-5.6-sol","provider":"openai","segment_id":"dreaming.wiki.id","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Id:","text_hash":"68d036759794566ce5d3e8d118a575d6b481ceeb79b324e938a3e06614164d32","tgt_lang":"ar","translated":"المعرّف:","updated_at":"2026-07-12T07:00:40.742Z"}
|
||||
{"cache_key":"db54f3edea7af3d6db8e66ee8a77ac0b99aecaa81310f10c3b98d594bd8f4ee5","model":"gpt-5.6-sol","provider":"openai","segment_id":"agentTools.overrideOn","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Override On","text_hash":"3740d84158209fb42847c00fb88816dc86fa62e9083bc56953fe2fcf0ee2942c","tgt_lang":"ar","translated":"التجاوز مفعّل","updated_at":"2026-07-12T06:59:12.313Z"}
|
||||
{"cache_key":"db72f08123aba0fc51c9f06d6e2bf4eea7bac689b61029070977f68e0e6aefac","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"ar","translated":"تعذّر الاتصال بالجلسة خلال 30 ثانية.","updated_at":"2026-07-15T00:45:23.615Z"}
|
||||
{"cache_key":"db848099464ca02f0d6941a55267884c2a626bff5fadd98e3688e7847ed6de8b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"ar","translated":"معاينة","updated_at":"2026-06-16T14:15:44.085Z"}
|
||||
{"cache_key":"dba7161818a3dc2d88ad414eaf92786d70fc9b59ebd8f0672930cf6172325d65","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"ar","translated":"جارٍ الاتصال بالجلسة…","updated_at":"2026-07-15T00:45:23.615Z"}
|
||||
{"cache_key":"dbce10b79cd0a126c40ee72de1b4951d75ea0eca3d0d8e0227a99a5211621b10","model":"gpt-5.6-sol","provider":"openai","segment_id":"dreaming.wiki.openConfig","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Open Config","text_hash":"63697308e9be76a975649e826a0640b5badf2e94f21a82af5e2c66c4a75889c4","tgt_lang":"ar","translated":"فتح الإعدادات","updated_at":"2026-07-12T07:00:40.742Z"}
|
||||
{"cache_key":"dbf577649c253dae4464d601a9313a88d8c0e1e22ba8fe93064588429eb9b75c","model":"gpt-5.6-sol","provider":"openai","segment_id":"configForm.customEntries","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Custom entries","text_hash":"1d5eb91b086b17275aa0d539482ac5c845807edb5d2b96ceaf7cf5cc4a70d5da","tgt_lang":"ar","translated":"إدخالات مخصّصة","updated_at":"2026-07-12T06:58:08.154Z"}
|
||||
{"cache_key":"dc1e1e1c65a0df066efbeb2104e02b5332d73e24727ea67165fc0ea5d2fa5a44","model":"gpt-5.6-sol","provider":"openai","segment_id":"skillStatus.disabled","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"disabled","text_hash":"17eb3c0168d0d7b21ede5481150f17233427d89833ec121b4dbc4fb96cfab71e","tgt_lang":"ar","translated":"معطّل","updated_at":"2026-07-12T06:59:37.065Z"}
|
||||
|
||||
Generated
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"fallbacks": {},
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"version": 1
|
||||
}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.466Z",
|
||||
"generatedAt": "2026-07-15T00:45:07.547Z",
|
||||
"locale": "de",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -240,6 +240,7 @@
|
||||
{"cache_key":"21c3baf040c532b1dbfc3412b7b07aa2ff4d5733a9964ee5f3887ec894439cc0","model":"gpt-5","provider":"openai","segment_id":"claudeSessions.searchPlaceholder","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Search session titles","text_hash":"9a6bdede84234fa08d6294e1636a34d26cbdd8eca425248f8d3c6cf8844a0cf9","tgt_lang":"de","translated":"Sitzungstitel durchsuchen","updated_at":"2026-07-09T10:01:43.726Z"}
|
||||
{"cache_key":"222981f3dabff8f006dd110acf52ee80162d3e8d00d35cc3937c86baa8fab05f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"de","translated":"Nicht zugewiesen (verwendet {agent})","updated_at":"2026-06-17T14:13:07.390Z"}
|
||||
{"cache_key":"2282d02251bb51133ef9d333372392209eca239eb0d6456ff5bf33664269c0bf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agentTools.title","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Tool Access","text_hash":"cf8084fcb3ba5158b83bed00ade2acfcba14311d9da2e7f7891f321addcd1c6c","tgt_lang":"de","translated":"Tool-Zugriff","updated_at":"2026-07-12T06:27:35.086Z"}
|
||||
{"cache_key":"228a07205cd33a6b462b44416dbc27cf74b17930d063a656ad5c0cf8244e9878","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"de","translated":"Verbindung zur Sitzung wird hergestellt…","updated_at":"2026-07-15T00:45:07.547Z"}
|
||||
{"cache_key":"229e511cd57fbe3e3bd052510b240e9455817fe54a871b3c68ec58b36cab3f91","model":"gpt-5","provider":"openai","segment_id":"modelProviders.probe.status.ok","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Connected","text_hash":"22965568d22a14ee17af055d2870b50afcfe9fd94a83eec3196e266932297bb2","tgt_lang":"de","translated":"Verbunden","updated_at":"2026-07-09T10:01:43.726Z"}
|
||||
{"cache_key":"22c09ac355f646e893761f9ae2c071946cf7e4f0f060eb37d76b33604c344013","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.inventory.roleUpgrade","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"role upgrade requires approval","text_hash":"c2c2a021e6839c8bb89fcf5d387a4a7c577bf4a042a110220ee1ffb78f238966","tgt_lang":"de","translated":"Rollen-Erweiterung erfordert Genehmigung","updated_at":"2026-07-12T06:25:24.484Z"}
|
||||
{"cache_key":"22dca2f6ae545073b14cb1f44d2352ddd0e046a4b9983c82403c3fd5fc7a66ee","model":"gpt-5.6-sol","provider":"openai","segment_id":"cron.form.summaryCron","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Cron schedule {expr}","text_hash":"953e6a80deb2a7c7fd3a8a29e4c363baa92bc4df48ae58ac0c7dafee554fd510","tgt_lang":"de","translated":"Cron-Zeitplan {expr}","updated_at":"2026-07-12T09:21:49.502Z"}
|
||||
@@ -1359,6 +1360,7 @@
|
||||
{"cache_key":"bc745191d77ded6eb1fc717e44b1f0dd0d9a5e86e6d6e484612a7f2edc3f5a24","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.customEntries","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Custom entries","text_hash":"1d5eb91b086b17275aa0d539482ac5c845807edb5d2b96ceaf7cf5cc4a70d5da","tgt_lang":"de","translated":"Benutzerdefinierte Einträge","updated_at":"2026-07-12T06:26:04.418Z"}
|
||||
{"cache_key":"bc8d533ff5bdecd6eb0d3cc8f3959e0883abfc31c4015a40d760048329d6b6a4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.noItems","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"No items yet. Click \"Add\" to create one.","text_hash":"7911888656dc431af458b7521f85f45aeeb702b8060d63a49d52520aa96cd9e8","tgt_lang":"de","translated":"Noch keine Elemente. Klicken Sie auf „Hinzufügen“, um eines zu erstellen.","updated_at":"2026-07-12T06:26:04.418Z"}
|
||||
{"cache_key":"bc8f51f2daa9f811e422f883cce65998a0072fd03163531457e5e8a011583136","model":"gpt-5.5","provider":"openai","segment_id":"chat.pullRequests.ciMonitoring","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"CI monitoring","text_hash":"b729ae0c12be4bfdccc13ca31d0ee718cfc6a324b564daa239a5bf2e147b3398","tgt_lang":"de","translated":"CI-Überwachung","updated_at":"2026-07-10T23:12:26.926Z"}
|
||||
{"cache_key":"bca9f570f1aba136ac16df762ca4a74bbcf033e85a2700cd9e85d7c8ce35263a","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"de","translated":"Die Verbindung zur Sitzung wurde nicht innerhalb von 30 Sekunden hergestellt.","updated_at":"2026-07-15T00:45:07.547Z"}
|
||||
{"cache_key":"bcc72747c637cb37cbd61a2031e294497bde9b3268d2382c72a1dd3eb840d386","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agentTools.enableNamed","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Enable {name}","text_hash":"dd01fc045da3bbf286494b66561c74d4e5e7ea1295a99bbd69dafb34d647928b","tgt_lang":"de","translated":"{name} aktivieren","updated_at":"2026-07-12T06:27:47.285Z"}
|
||||
{"cache_key":"bd69dd219b34a86ad13ed2d47d897c45139f72401d20a9d02a521c8284368128","model":"gpt-5.5","provider":"openai","segment_id":"newSession.title","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"New session","text_hash":"cffdba22adf299e8fc6f937ef63ca4352ca9db3022d4634004c0a9bd10aa0dfe","tgt_lang":"de","translated":"Neue Sitzung","updated_at":"2026-07-10T15:20:45.759Z"}
|
||||
{"cache_key":"bdbd69d4d05e013329248fdf282ba894f41e5a92268171705cb01e7c1f73c53c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.form.failureAlertInherit","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Inherit global setting","text_hash":"d4a23b45ca8a97a420a2716e0c2b9db41f0a89adc259a43f8c842cea1fcd1491","tgt_lang":"de","translated":"Globale Einstellung übernehmen","updated_at":"2026-07-12T06:30:02.971Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.574Z",
|
||||
"generatedAt": "2026-07-15T00:45:09.673Z",
|
||||
"locale": "es",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -627,6 +627,7 @@
|
||||
{"cache_key":"5374b6991e3457597aa34c1ac94740da287f12cb2241f61c42acbfee762a8997","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"es","translated":"Artefactos","updated_at":"2026-06-16T14:14:23.912Z"}
|
||||
{"cache_key":"537569c7d6b0e66bda3ea379226e9a01302c5a0b725f20e0bc93344ff2d5e5f8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"githubPreview.ariaLabel","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{state} {kind} {repo} #{number}: {title}, by {author}","text_hash":"e18c624550814e4318a45e1e0f8782913409763d046aa703519e91c887be205b","tgt_lang":"es","translated":"{state} {kind} {repo} #{number}: {title}, por {author}","updated_at":"2026-07-12T06:31:12.973Z"}
|
||||
{"cache_key":"53a39119b041c8b526ef9cf2fae89094f1299c8f5e3772df4dab61042c3f4518","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"es","translated":"Comentarios","updated_at":"2026-07-01T01:06:39.912Z"}
|
||||
{"cache_key":"53be9b8d5cb43e2f3ff71cdeca7fc995fc8cf6184d28ca691d24067dd34eaa87","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"es","translated":"Conectando con la sesión…","updated_at":"2026-07-15T00:45:09.673Z"}
|
||||
{"cache_key":"53e5bda60a120f4bcabf15af0b94dd1dfef943f58e03809378ab07ca66632bb5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.detail.historyTitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Run history","text_hash":"addf321bfa5b8346b1699c837e7658a4c646025227efada351113b4cbd649181","tgt_lang":"es","translated":"Historial de ejecuciones","updated_at":"2026-07-12T06:35:32.739Z"}
|
||||
{"cache_key":"54265c742b13510e3bac93963ad6ca4b1dd9cb78ca6b88cc58b1a39d0cb32024","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.inventory.rolesAndScopes","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"roles: {roles} · scopes: {scopes}","text_hash":"035200095981fd169e4ff5855efc8233dafe1de451cccd054074b791f1f10e17","tgt_lang":"es","translated":"roles: {roles} · ámbitos: {scopes}","updated_at":"2026-07-12T06:31:31.117Z"}
|
||||
{"cache_key":"542f049cede42d6cded59b38679c20156241f6fbad27c3ec19f3e07e7a799f01","model":"gpt-5.6-sol","provider":"openai","segment_id":"newSession.nodeOffline","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Device is offline","text_hash":"3571104eb98b4de871b743f3f2993d1aea3f6bf9636730c858201c902464fa44","tgt_lang":"es","translated":"El dispositivo está sin conexión","updated_at":"2026-07-12T18:40:02.321Z"}
|
||||
@@ -693,6 +694,7 @@
|
||||
{"cache_key":"5c970759f652f97f481d8c6dbb9e01d2752a2095dbab3d749f85e19e2a1ade89","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.sections.skills.description","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Skill packs and capabilities","text_hash":"16929f911a9b43cb09809e2088aa93241e7da0ffea4630ca251dc6825b7587b4","tgt_lang":"es","translated":"Paquetes de Skills y capacidades","updated_at":"2026-07-12T06:32:21.509Z"}
|
||||
{"cache_key":"5c9c0c4d6b02004b5062fee3e6f30ef48fb7a52f2c04c39a0338662a62e8acbb","model":"gpt-5.5","provider":"openai","segment_id":"nativeLinkMenu.copy","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Copy Link","text_hash":"724e78a3254c899d16ef6324a7abc9a8f5240ffce8bff74976df397c68ce9d78","tgt_lang":"es","translated":"Copiar enlace","updated_at":"2026-07-09T11:02:44.131Z"}
|
||||
{"cache_key":"5ca3eb3ab1cd3c888a4ca21ec8add873e170b8f2aac4082db3983b5b1f62af4d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.actions.revise","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Revise","text_hash":"f1323f09f63c867d520b3b267f6e314e4eaf92fb314f916b1c50b67076dfcab7","tgt_lang":"es","translated":"Revisar","updated_at":"2026-07-12T06:34:21.942Z"}
|
||||
{"cache_key":"5cb7042c36c5cb48e3bedfabd7727b789a7bf1f57e0f76b81d781b0e15f468c0","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"es","translated":"La sesión no se conectó en un plazo de 30 segundos.","updated_at":"2026-07-15T00:45:09.673Z"}
|
||||
{"cache_key":"5cb84d880d8ccc775012caa552980ad9a96e40a81cfdce70852082b7492a2dfc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agents.toolCatalog.descriptions.webSearch","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Search the web","text_hash":"0d3d9dd6d2ebd697f7068a644d1b72767a7b04309e333fa432fbadc536c46491","tgt_lang":"es","translated":"Buscar en la web","updated_at":"2026-07-12T06:32:02.628Z"}
|
||||
{"cache_key":"5ccaa2d7e9e92dc8710fa7c7c259d5f01c796218348ec0e90e48f30d53f40826","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.empty.appliedTitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Nothing applied yet","text_hash":"32697e30c8206968d4e025e8055ac5555a79c518aef4da12fabaa25280c2459a","tgt_lang":"es","translated":"Aún no se ha aplicado nada","updated_at":"2026-07-12T06:34:29.611Z"}
|
||||
{"cache_key":"5ce401cf073b34992749bdefbcecf375c9e9687124c9e90b296713ad9f96954a","model":"gpt-5.6-sol","provider":"openai","segment_id":"skillsPage.title","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Skills","text_hash":"66d0f523a379b2de6f8d5fba3a817ebc395f7bcaa54cc132ca9dfa665d1e9378","tgt_lang":"es","translated":"Skills","updated_at":"2026-07-12T00:08:26.918Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:52.082Z",
|
||||
"generatedAt": "2026-07-15T00:45:47.920Z",
|
||||
"locale": "fa",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -149,6 +149,7 @@
|
||||
{"cache_key":"149119bd2cf79c38b902f80b62de12cb57eaa09688329a070438fbdc44eb961c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agents.overview.title","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Overview","text_hash":"d4b1ea5708dd532930a85188b45aff6f0a3ed458500c7577e0127a538eb0d100","tgt_lang":"fa","translated":"نمای کلی","updated_at":"2026-07-12T06:53:17.312Z"}
|
||||
{"cache_key":"14a622b02a51860b3c883e0b565061b7ca5dc023cd354925937564743f6a3967","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.nostr.title","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Nostr","text_hash":"cd27eb85bbbcde66be45dd832281190c0002b58be5c867e4f095d59b110c0c04","tgt_lang":"fa","translated":"Nostr","updated_at":"2026-07-12T06:52:24.346Z"}
|
||||
{"cache_key":"14d08fb31b7428e1328002e00999ae27383f642933f78110b988accf33809924","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.notifications.unavailable","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Unavailable","text_hash":"ca184496974204a0bd3fd4925e0540bc4b0df6fcc232994d29754272b7183812","tgt_lang":"fa","translated":"در دسترس نیست","updated_at":"2026-07-12T06:55:07.729Z"}
|
||||
{"cache_key":"1515c06d19273982ffa12c73c3598bbf4444d0eab106d355e633c0655b578a3a","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"fa","translated":"اتصال به نشست ظرف ۳۰ ثانیه برقرار نشد.","updated_at":"2026-07-15T00:45:47.920Z"}
|
||||
{"cache_key":"153016aa6f387314e4cc4662a949c92c2952a816171461282bb406ddf04dd0ca","model":"claude-opus-4-8","provider":"anthropic","segment_id":"pluginsPage.connectorDescriptions.travel","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Flight and hotel search with fare watching and trip memory.","text_hash":"674641dedb84777fbda9257f030592dd29e4a7403b9a342eb28a9fe581f37286","tgt_lang":"fa","translated":"جستجوی پرواز و هتل با رصد کرایه و حافظه سفر.","updated_at":"2026-07-12T06:57:30.982Z"}
|
||||
{"cache_key":"1559d2502c8a68602b514e23d6e4e320cf9f7f31721643a13b5ee33c3c5cb84b","model":"gpt-5.6-sol","provider":"openai","segment_id":"agentChip.noAgentMatches","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"No matching agents","text_hash":"38b006752ec7a0a6e18631050994899431708df2707bbc0792315deaf8e3a933","tgt_lang":"fa","translated":"هیچ عامل منطبقی یافت نشد","updated_at":"2026-07-13T05:31:20.131Z"}
|
||||
{"cache_key":"1572ba82025e96e3bf0387d0ac68167d85bb9b109d6434340c79d736e5f4629f","model":"claude-sonnet-4-6","provider":"anthropic","segment_id":"cron.suggestions.ideas.nightWatch.prompt","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Check that my services and gateway are healthy: scan recent logs for new errors, restarts, or unusual load. Stay quiet if everything is fine; if something looks broken, message me what failed and where to start looking.","text_hash":"ff691fc0bcd98cc27aea75758d323fc926a74252f5007eb43b49073d1a5716b7","tgt_lang":"fa","translated":"بررسی کن که سرویسها و Gateway من سالم باشند: لاگهای اخیر را برای خطاهای جدید، راهاندازی مجدد یا بار غیرعادی اسکن کن. اگر همه چیز خوب است، ساکت بمان؛ اگر چیزی خراب به نظر میرسد، به من پیام بده چه چیزی خراب شده و از کجا شروع به بررسی کنم.","updated_at":"2026-07-11T22:49:36.745Z"}
|
||||
@@ -1765,6 +1766,7 @@
|
||||
{"cache_key":"faac3e3875308802f7caedd0e2725842459b138d922b3deea34b67b881656bd4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.execApprovals.override","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Override ({value}).","text_hash":"b561858a9c97ebe63ee4ea9608da6af30ed0710bd5f18e12f1d35025ebe08f79","tgt_lang":"fa","translated":"بازنویسی ({value}).","updated_at":"2026-07-12T06:53:09.066Z"}
|
||||
{"cache_key":"fad84c869f4c72c6781d4c7b345c5e63a8782ad3fe5ffd6a43ccb67e98d122da","model":"claude-opus-4-8","provider":"anthropic","segment_id":"quickSettings.security.deviceAuth","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Device auth","text_hash":"6fc25a19873431c090a39a9ea327265e37e2201237e8f1d06045426d4341b92a","tgt_lang":"fa","translated":"احراز هویت دستگاه","updated_at":"2026-07-12T06:54:30.738Z"}
|
||||
{"cache_key":"fae540f92ca9e65f917d524e2bf3640acbb9845c0013211e81664628a9da36d3","model":"gpt-5.5","provider":"openai","segment_id":"pluginsPage.mcp","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"fa","translated":"MCP","updated_at":"2026-05-31T05:36:59.400Z"}
|
||||
{"cache_key":"fb368b042dd2dd4cc6c9e5683591a3be141cedc9f5971e8efa763cb7e2c8dc6b","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"fa","translated":"در حال اتصال به نشست…","updated_at":"2026-07-15T00:45:47.920Z"}
|
||||
{"cache_key":"fb67c83b1bb46517f66b8c0f3dd7884ec600b88c3ebdce640031324320d16c02","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillsPage.noClawHubResults","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"No skills found on ClawHub.","text_hash":"a3b2387163dc439615c00ab811a92f533225c5c02a529b68074ef56fac75821f","tgt_lang":"fa","translated":"هیچ Skill در ClawHub یافت نشد.","updated_at":"2026-07-12T06:56:35.267Z"}
|
||||
{"cache_key":"fb736bf01b588cfee6d256f787421041bfeb552fcf2f93863b0ce27cd9e40a17","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agents.toolCatalog.descriptions.write","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Create or overwrite files","text_hash":"b29a2552b08fc6afff15e7d9e6276beeaa33e59f3fa298a999c6e8115b487d7a","tgt_lang":"fa","translated":"ایجاد یا بازنویسی فایلها","updated_at":"2026-07-12T06:53:26.782Z"}
|
||||
{"cache_key":"fb7d1fa78a10d7f85914aee2870bb81ca931e2a87ca266184c8b380391dadfc2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"fa","translated":"برداشتن سنجاق نشست","updated_at":"2026-07-02T14:31:11.668Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.895Z",
|
||||
"generatedAt": "2026-07-15T00:45:16.828Z",
|
||||
"locale": "fr",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -967,6 +967,7 @@
|
||||
{"cache_key":"83a3f830b91efce5c1261f2de88ea64eaa055e3d61452ba8fcd923226823c3ae","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.queue.search","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Search proposals…","text_hash":"920a1bd7a15443b762e0d8f9f5aac701346342c3cd5ce3583b42a0e63e43ec0c","tgt_lang":"fr","translated":"Rechercher des propositions…","updated_at":"2026-07-12T06:35:13.975Z"}
|
||||
{"cache_key":"83a3fb48f64340235cc516ca8d88ca305f08dc0cdccea062af22e5badd71090b","model":"gpt-5.5","provider":"openai","segment_id":"browser.start","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Start browser","text_hash":"4af2fc76dfa1bfc8b4b30cefd65fb5ca171d792bedc4cab90f082df7c9b961ff","tgt_lang":"fr","translated":"Démarrer le navigateur","updated_at":"2026-07-11T02:18:20.344Z"}
|
||||
{"cache_key":"83b8bbdf993208bb03a0f25732561aaa378bdecf9b585b73a474cb93cf9f57b5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillGroups.extra","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Extra Skills","text_hash":"1999b1716503a67377522c9416f5d2b2a424668ec761c5007655f0c8d284d686","tgt_lang":"fr","translated":"Skills supplémentaires","updated_at":"2026-07-12T06:34:31.353Z"}
|
||||
{"cache_key":"83e1fcffcfdf9a74a9d72caf82b1c7a3e05f3c78bea3172c2e9cb8c000720a1f","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"fr","translated":"Connexion à la session…","updated_at":"2026-07-15T00:45:16.828Z"}
|
||||
{"cache_key":"83fa83338c609d9ce0d5a0fd044202ca93c5debd047857b6e76b4c543a80d072","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.queue.removeQueuedMessage","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Remove queued message","text_hash":"1c99e5283577df5340915a16859019f651a3d20dd9928c78d0e5ec14464d9b74","tgt_lang":"fr","translated":"Supprimer le message en file d'attente","updated_at":"2026-07-12T06:36:02.496Z"}
|
||||
{"cache_key":"8426336867446f1b7bb80a177852d24ab0e269be467bfa71e633bb7b29145292","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.empty.rejectedTitle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"No rejected proposals","text_hash":"c94fa71bf6645cbf1d245fd8849cb25281262df6c7e7d186cefabd7c846c9380","tgt_lang":"fr","translated":"Aucune proposition rejetée","updated_at":"2026-07-12T06:35:23.166Z"}
|
||||
{"cache_key":"8437bbde31f98a8212d5402ac1a1677eb444423a6a622da8c17624d0af769b0c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.actions.exitFocusMode","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Exit focus mode","text_hash":"cf1f3e5858222364ee1dbaad49d0a246996a578e39d0a46aff1283dd3fd1d9b7","tgt_lang":"fr","translated":"Quitter le mode focus","updated_at":"2026-07-12T06:35:57.306Z"}
|
||||
@@ -1006,6 +1007,7 @@
|
||||
{"cache_key":"8a07c4150358cf2d4e04f54e9783795cba937ac092e114ae6ad87f30369dba62","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.relative.secondsAgo","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count}s ago","text_hash":"244073ecb2be8fe875a37bcf7023ff32fb21f7c64e5d29e0ae62931a84c98a6a","tgt_lang":"fr","translated":"il y a {count} s","updated_at":"2026-07-12T06:35:36.282Z"}
|
||||
{"cache_key":"8a3bf6a7710c3d99cc75528d25a79ae7405c8a17cba773c5fbb52b22193820fd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.talkVoice","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Voice","text_hash":"87bf2bc08589f0bd4a078db145c34ad5e14b8fda53c3ae65b78601294913df95","tgt_lang":"fr","translated":"Voix","updated_at":"2026-07-06T20:20:02.809Z"}
|
||||
{"cache_key":"8a407cc06ec5c609e1eee29c0d973582c258dcb68fe007a4f57ed5e71351be57","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.contextUsage.contextWindow","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Context window","text_hash":"7696d0855331622dc12438057f5509348f9d6f0ec2eb3580e18a99d31eba86db","tgt_lang":"fr","translated":"Fenêtre de contexte","updated_at":"2026-07-05T10:16:09.189Z"}
|
||||
{"cache_key":"8a5482f2664f7787b17ac8d556fc2c49cf3f95249139470b26dd5f40e2bfbfec","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"fr","translated":"La connexion à la session n’a pas été établie dans les 30 secondes.","updated_at":"2026-07-15T00:45:16.828Z"}
|
||||
{"cache_key":"8a9af3a254d52b7eee14f5f63650af64abe3323807045edf76145af7c9965ee3","model":"gpt-5.6-sol","provider":"openai","segment_id":"modelProviders.defaults.saved","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Default models saved.","text_hash":"bcfc1802a87c6f284158e3d9b881d5b50ef50c24a7cb3b1a8878766784caf907","tgt_lang":"fr","translated":"Modèles par défaut enregistrés.","updated_at":"2026-07-13T16:31:59.089Z"}
|
||||
{"cache_key":"8ad358d7f59dc9144ea68e02b5ad6c733b04e1bf67e6a3ce4a1fe46fd36f877b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.nostr.placeholders.website","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"https://example.com","text_hash":"100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9","tgt_lang":"fr","translated":"https://example.com","updated_at":"2026-07-12T06:32:00.816Z"}
|
||||
{"cache_key":"8ae836ba79430ff9534233893652805eee3f30b2dba903571d2bde4ba4996c4d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.empty.staleBody","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Proposals that can no longer apply cleanly will appear here.","text_hash":"cc140b15a8c376fc139829453cab08c9280f277f0796e9673ea723621772d877","tgt_lang":"fr","translated":"Les propositions qui ne peuvent plus s'appliquer proprement apparaîtront ici.","updated_at":"2026-07-12T06:35:23.166Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.001Z",
|
||||
"generatedAt": "2026-07-15T00:45:21.158Z",
|
||||
"locale": "hi",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -202,6 +202,7 @@
|
||||
{"cache_key":"0fba2d5dead215b9bf8cf4dbde67a0a18686ec09a3e598efe086bd06b627f0e4","model":"gpt-5.5","provider":"openai","segment_id":"profilePage.insightModel","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Most used model","text_hash":"a13c86cd4f835d2c2b5af940171ede2a27e0f805cef070d87628f1bd333bb706","tgt_lang":"hi","translated":"सबसे अधिक उपयोग किया गया model","updated_at":"2026-07-09T11:27:28.426Z"}
|
||||
{"cache_key":"0fc99e5b34dab73253294ca22f274b1d4dd60a2300c24c261c0b5168a290f306","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.sections.mcp.description","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Model Context Protocol server definitions","text_hash":"bcafe6b826750f5565395b53cb77493f44b5cb0c9809d6b97e0df95407b91b9e","tgt_lang":"hi","translated":"Model Context Protocol सर्वर परिभाषाएँ","updated_at":"2026-07-12T06:39:07.768Z"}
|
||||
{"cache_key":"0fcb2e1d7604ccf474d8d53b5675d3de499d2420ed8928d79868762a4b5c3cc7","model":"gpt-5.6-sol","provider":"openai","segment_id":"chat.workRun.workedFor","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Worked for {duration}","text_hash":"c8e2dac0ee966bbad30c620b4049b9cb92879e3e4f1967dc8161e3937a73cc2c","tgt_lang":"hi","translated":"{duration} तक काम किया","updated_at":"2026-07-12T17:49:35.699Z"}
|
||||
{"cache_key":"0fe289379373c585c2cee6c26b166c607a8054782d15c47b6d2bcfd56e8b305a","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"hi","translated":"सत्र से कनेक्ट किया जा रहा है…","updated_at":"2026-07-15T00:45:21.158Z"}
|
||||
{"cache_key":"0fe85a1bf4b86ecdc3a2d90e92bc495f67df949b23bf5e5c36aea457a49c7863","model":"gpt-5.5","provider":"openai","segment_id":"chat.refreshTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Refresh chat data","text_hash":"40b8edfd9a1326939cf9db6cca2b31d4af4e815606fe6d86f7982f4f9534e268","tgt_lang":"hi","translated":"चैट डेटा रीफ़्रेश करें","updated_at":"2026-06-26T21:36:16.800Z"}
|
||||
{"cache_key":"0ff9313f900028f3097a954f640efdb603a7a0b256f33332270ee6f08b6b6aed","model":"gpt-5.5","provider":"openai","segment_id":"execApproval.labels.cwd","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"hi","translated":"CWD","updated_at":"2026-06-26T21:31:18.653Z"}
|
||||
{"cache_key":"1001c2d65a3bd7607d746129578d6be34c919b352b3b7cea41fddeb0bb6c1b91","model":"gpt-5.5","provider":"openai","segment_id":"quickSettings.model.fastModes.auto","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Auto","text_hash":"0286249762f7c94349cdc0ba3bb2255baf9a80036e2193ead1d77696f888582f","tgt_lang":"hi","translated":"स्वचालित","updated_at":"2026-07-06T20:20:02.809Z"}
|
||||
@@ -1274,6 +1275,7 @@
|
||||
{"cache_key":"630bc2b30a8210742eb7e82f792b6c3aa28bccc85e9094ec3ee3e4b65d3d58f6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.execApprovals.security","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Security","text_hash":"8f6fb4eb7f42c0e245e29e63f5b82cc3ba19852681d1ed9aed291f59cf75ec0e","tgt_lang":"hi","translated":"सुरक्षा","updated_at":"2026-06-26T21:31:18.653Z"}
|
||||
{"cache_key":"631d2edc2138392209d7036ad0528e07df06030ae288b6eb66e5b4a358abb98e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"quickSettings.automation.configure","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Configure →","text_hash":"3853cdcbe571c3cc86f127a799dfbcc2540b79f092761008c94f70976f8d9e7c","tgt_lang":"hi","translated":"कॉन्फ़िगर करें →","updated_at":"2026-07-12T06:39:13.839Z"}
|
||||
{"cache_key":"631ff04d5745f46ef01d373b06a0edab2d601a25230f34e42f843a29f703c866","model":"gpt-5.5","provider":"openai","segment_id":"browser.annotate","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Annotate page","text_hash":"a195c16075cc573ed53b608e832e1656b59bd30481eea941f6f264dc10294c65","tgt_lang":"hi","translated":"पेज पर टिप्पणी करें","updated_at":"2026-07-11T02:18:28.211Z"}
|
||||
{"cache_key":"6337b7b6601061147ba01da53c548a982b46234216d95bd2f1bb557c0731a9e4","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"hi","translated":"सत्र 30 सेकंड के भीतर कनेक्ट नहीं हुआ।","updated_at":"2026-07-15T00:45:21.158Z"}
|
||||
{"cache_key":"635e3b572a7f32c609358147cf17a9902f3e01071d29e733f8e4e4f90bcd66d2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"pluginsPage.connectorDescriptions.todoist","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Read, add, and complete tasks and projects in Todoist.","text_hash":"de752d3f3270b42889ff4f1fa5ded19d459cb653d13deafa4359e2c960b9976a","tgt_lang":"hi","translated":"Todoist में कार्य और प्रोजेक्ट पढ़ें, जोड़ें, और पूर्ण करें।","updated_at":"2026-07-12T06:41:11.385Z"}
|
||||
{"cache_key":"636076b1547d2584ec19524ec4686640df542a157916a5a07fa76f9435a70c6a","model":"claude-sonnet-4-6","provider":"anthropic","segment_id":"cron.suggestions.ideas.hackerNewsScout.prompt","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Scan today's Hacker News front page for posts about AI agents, developer tooling, and TypeScript. Send me the three most interesting links, each with a one-line hot take.","text_hash":"11c42596963d19c50c7108c9f5ad1001f72a55028e816eeca0208f55d5713de8","tgt_lang":"hi","translated":"AI agents, developer tooling और TypeScript पर आज के Hacker News front page को स्कैन करें। मुझे तीन सबसे दिलचस्प लिंक भेजें, प्रत्येक के साथ एक पंक्ति का hot take।","updated_at":"2026-07-11T22:46:11.446Z"}
|
||||
{"cache_key":"6362d51a03302c48fa00184207b14f6e7d3edb987a3b79f64c13139e4d3d9718","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.toolCards.openDetails","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Open tool details in side panel","text_hash":"c6056b4228d222d66008f2ddf0d39980d4d451f1590b08a15999b629a32ff5db","tgt_lang":"hi","translated":"साइड पैनल में टूल विवरण खोलें","updated_at":"2026-07-12T06:43:10.661Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.543Z",
|
||||
"generatedAt": "2026-07-15T00:45:34.312Z",
|
||||
"locale": "id",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -831,6 +831,7 @@
|
||||
{"cache_key":"77fd51cb53dd11dbbe1962e4a402a56557a00f442b0cbbd5f97995cb18d5ede1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.nostr.subtitle","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Decentralized DMs via Nostr relays (NIP-04).","text_hash":"f910815433f5df92e08b45c501380a6607a3bb86a66dd51bfc5ea32720f918d3","tgt_lang":"id","translated":"DM terdesentralisasi melalui relay Nostr (NIP-04).","updated_at":"2026-07-12T06:44:48.803Z"}
|
||||
{"cache_key":"780fd2544a24cd758296900de5e055bed5b5e0a2e9a2c72ecce6b6af00c63f6b","model":"gpt-5.5","provider":"openai","segment_id":"chat.pullRequests.checks","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"CI","text_hash":"fe8ee15bb86d27a77f2a62bd71bc65936156c99fe5b58537b347cbad1761fd95","tgt_lang":"id","translated":"CI","updated_at":"2026-07-10T17:04:17.152Z"}
|
||||
{"cache_key":"781981308f67efff0c5ade16e91efa5f13004fcb8d532008e842d617b29f30f2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"id","translated":"Salin kode penyiapan","updated_at":"2026-07-04T16:48:34.014Z"}
|
||||
{"cache_key":"782f14ae3fbfeb942896e6469a76713458b5bba62f01a34845544a998f7e1292","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"id","translated":"Sesi tidak terhubung dalam waktu 30 detik.","updated_at":"2026-07-15T00:45:34.312Z"}
|
||||
{"cache_key":"7836ee7b362527aac3bce43f3c550eb423f2e2df68e019daad8a236950de582b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.detail.detailsSection","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Details","text_hash":"45989de49fb7f66dfe17a8fc26f3a02c7abcd74f7e8e6cf9f39fa0e3775780df","tgt_lang":"id","translated":"Detail","updated_at":"2026-07-12T06:49:32.707Z"}
|
||||
{"cache_key":"785c116ca15b349bb785e46909a5c8ffb51193836fdc53779404d11f1ff83256","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.execApprovals.hostNativeHint","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Read-only here. Edit from the companion app or CLI.","text_hash":"f40b6f5e3ead3a1a83b8c2cb0ffe80b8d105b35497ec464651dc8be98b7fe8cb","tgt_lang":"id","translated":"Hanya-baca di sini. Edit dari aplikasi pendamping atau CLI.","updated_at":"2026-07-12T06:45:09.776Z"}
|
||||
{"cache_key":"786790754bba3400c2aa1d4adb4d7c339878258807060a8c7ab9a4a7bc9e8b1a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.sections.models.label","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Models","text_hash":"d17d2d78d76e6a6cd13048de225e107ac73de4aa5f8914e58a0cfaa9698c373e","tgt_lang":"id","translated":"Model","updated_at":"2026-07-12T06:46:00.053Z"}
|
||||
@@ -1715,6 +1716,7 @@
|
||||
{"cache_key":"f47516abdcee50dd1d144f2981b9b9542d0e934a971c553dd5a9c5cd18c6498c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.detailPanel.overwrite","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Overwrite","text_hash":"b24963ea2cbc74b97321a347754137aa207b126695a95c860406098528966b55","tgt_lang":"id","translated":"Timpa","updated_at":"2026-07-12T06:49:15.040Z"}
|
||||
{"cache_key":"f4bab9579700a67ef35249bc12292bb928fb05af2d8b7cd7761560a721d57474","model":"claude-opus-4-8","provider":"anthropic","segment_id":"pluginsPage.connectorDescriptions.spotify","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Search, queue, and soundtrack your day with mood-based playlists.","text_hash":"ba29daebd2737cc8a802f208e9eb5c270570ec1e2a3fd936ed6bd0f73553bfd8","tgt_lang":"id","translated":"Cari, antrekan, dan iringi hari Anda dengan playlist berbasis suasana hati.","updated_at":"2026-07-12T06:48:01.260Z"}
|
||||
{"cache_key":"f4c91a2aa66246787035abe4cb42ecbe1d9b37f7a3b9ab21a6c9cc029918d2ad","model":"gpt-5.5","provider":"openai","segment_id":"browser.empty","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"No open tabs. Enter a URL above to browse.","text_hash":"a9e25255aa326aac375865aac61e932f80d6030997ad0d8d2fe04b6d30772edd","tgt_lang":"id","translated":"Tidak ada tab terbuka. Masukkan URL di atas untuk menjelajah.","updated_at":"2026-07-11T02:19:28.776Z"}
|
||||
{"cache_key":"f5012d5bcba7cb4337e9e1cc669178517280f17db02a0c252efc21f664574667","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"id","translated":"Menghubungkan ke sesi…","updated_at":"2026-07-15T00:45:34.312Z"}
|
||||
{"cache_key":"f50c8765057719fb1a3b5bedb5ad86b5a899cbe3d28902c8acadba821029bebc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"quickSettings.appearance.themes.claw","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Claw","text_hash":"587cfbbbcd42a71ba7f33f8051d5c354707fa1ff227cab11452c3ba2147a8682","tgt_lang":"id","translated":"Claw","updated_at":"2026-07-12T06:46:17.106Z"}
|
||||
{"cache_key":"f50fa2715128b7c94a18e2fa5fc6a4c84e261e1fa3d0790dc89d56a4fce7bb23","model":"gpt-5.6-sol","provider":"openai","segment_id":"chat.progressLabels.clawing","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Clawing","text_hash":"f4ce53e24f86c82ea592e29402eb72daf6f56f1e10185b9c73229a1c10d853a0","tgt_lang":"id","translated":"Mencapit","updated_at":"2026-07-14T04:54:35.208Z"}
|
||||
{"cache_key":"f526899381a459c119e1b2c00dec965341de010401082d6c00a2a9c1faae7ad0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.execApprovals.scope","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Scope","text_hash":"b073f6c68ef8721107fd9815b19b2c35ec111d526b75c2123d1111ba64424000","tgt_lang":"id","translated":"Cakupan","updated_at":"2026-07-12T06:45:17.163Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.218Z",
|
||||
"generatedAt": "2026-07-15T00:45:25.876Z",
|
||||
"locale": "it",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -293,6 +293,7 @@
|
||||
{"cache_key":"2921e861dc586f4f070f2fab2089c4681abae68d3480bdcd4574c42d17f04ea8","model":"gpt-5.5","provider":"openai","segment_id":"chat.workspaceFiles.showFiles","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Show session files","text_hash":"88e60963b00018033f164b496f29e784fddfdb400cd19baf3311e04645bab27a","tgt_lang":"it","translated":"Mostra i file della sessione","updated_at":"2026-07-10T06:08:23.266Z"}
|
||||
{"cache_key":"2926df7d5f4c4b3fd6029c5b7bac0df9a590aa60622d7ddb1e567943014c0471","model":"gpt-5.5","provider":"openai","segment_id":"codexSessions.transcript.loadMore","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Load older transcript items","text_hash":"2dd9c742f15de60d977597a17faf62db2cd79cd58caf87f349dac75899b3788e","tgt_lang":"it","translated":"Carica elementi precedenti della trascrizione","updated_at":"2026-07-11T10:25:06.585Z"}
|
||||
{"cache_key":"293cd5413295620b18d22090f49b4ef40d91519dd921086dcbe0004a6336cf19","model":"gpt-5.5","provider":"openai","segment_id":"newSession.hint","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Pick where this session works, then say what to do.","text_hash":"fcab2bf24a4a66dfbdf0589e5e1474d3b5cf952720f9f9f619d11f564283e698","tgt_lang":"it","translated":"Scegli dove lavora questa sessione, poi indica cosa fare.","updated_at":"2026-07-10T17:59:26.804Z"}
|
||||
{"cache_key":"295c5f03400a6afb65294925bf1f81188941d3da9f29484dfdd002ae67549d09","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"it","translated":"La sessione non si è connessa entro 30 secondi.","updated_at":"2026-07-15T00:45:25.876Z"}
|
||||
{"cache_key":"2967e9aea81e98f4c54fc315111bac0e865f34b9beda20f34e8050540310e6e8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.sections.update.description","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Auto-update settings and release channel","text_hash":"e61b824d77d8e34d4e3ede3d018533f02848cc6f2071b15a7682beed13a315e7","tgt_lang":"it","translated":"Impostazioni di aggiornamento automatico e canale di rilascio","updated_at":"2026-07-12T06:38:56.155Z"}
|
||||
{"cache_key":"299d1a2add22162789625cc7196d9dce21ca2363a0b64229047f1c6850a1127a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.viewPendingChangesRaw","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"View pending changes","text_hash":"dfde31545cd2686bfc6834a69fdc8e56c5dbc9206557d0016cb861f02025b51c","tgt_lang":"it","translated":"Visualizza modifiche in sospeso","updated_at":"2026-07-12T06:40:05.679Z"}
|
||||
{"cache_key":"29c83567ecda1cd615ad78ef42b4f0bc863f798d2bf64e9e9e2a164eacc32749","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.signal.title","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Signal","text_hash":"1e9806e4227ba3b9a986732f1b09a21fd6b96043d12e5a4334a326ec5ad39842","tgt_lang":"it","translated":"Signal","updated_at":"2026-07-12T06:37:45.624Z"}
|
||||
@@ -1783,6 +1784,7 @@
|
||||
{"cache_key":"fd624f84075dc14e7c13a3ef798716202cd0422b553f427fab1d8cc7ad19f148","model":"gpt-5.5","provider":"openai","segment_id":"claudeSessions.sidebar.title","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Claude sessions","text_hash":"45d8dde95eda96ef87a6371eefdef22b667880c6e55ef402313b25f589bd983b","tgt_lang":"it","translated":"Sessioni Claude","updated_at":"2026-07-11T13:50:58.465Z"}
|
||||
{"cache_key":"fd90ff517fe38f8f425f638c15307c2e4e1a23a0915baa0f8e3396e74ef2fc41","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"it","translated":"Completate di recente","updated_at":"2026-06-17T14:19:01.168Z"}
|
||||
{"cache_key":"fdac4608aa308a8bd3d55f66564ff26ffa29d123acab3516a2f9c91a52ce1555","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.form.promptPlaceholder","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Describe what OpenClaw should do...","text_hash":"81afbe360be2d62d23ea39306a351c9b95b2d9a5a653c7469343454dc2ac706b","tgt_lang":"it","translated":"Descrivi cosa deve fare OpenClaw...","updated_at":"2026-07-12T06:42:36.545Z"}
|
||||
{"cache_key":"fdb362ac04715d85992c17140b5c34de0b154d6331ae046e0207b5ec999d2d85","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"it","translated":"Connessione alla sessione…","updated_at":"2026-07-15T00:45:25.876Z"}
|
||||
{"cache_key":"fdedb51f0df618cd58829d6441986fe05e18a11317d336484f92021fbd7a4400","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"it","translated":"Workspace: {workspace}","updated_at":"2026-06-16T14:15:39.914Z"}
|
||||
{"cache_key":"fdfdd72696cb4003f558a5d5d7b81991cf5e62377c1ddb18c4305eadd437bce1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.notifications.unavailable","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Unavailable","text_hash":"ca184496974204a0bd3fd4925e0540bc4b0df6fcc232994d29754272b7183812","tgt_lang":"it","translated":"Non disponibile","updated_at":"2026-07-12T06:39:47.853Z"}
|
||||
{"cache_key":"fe2dd36bd9ffc987d984a2d7bfa18da5e5fff1c20c34d1d6446f9314b42acf36","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"it","translated":"Aggiungi nota","updated_at":"2026-06-16T14:15:46.111Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.693Z",
|
||||
"generatedAt": "2026-07-15T00:45:11.997Z",
|
||||
"locale": "ja-JP",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -1177,6 +1177,7 @@
|
||||
{"cache_key":"a8d755d21f56af8a78796a70a5b4702c34255df2ff75699bbacc81d376dbe0c0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.actions.reject","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Reject","text_hash":"ab604a360777735fe2993aec7d4ffae415f1515b0a4c989269603fd3d5c07b61","tgt_lang":"ja-JP","translated":"拒否","updated_at":"2026-07-12T06:34:56.740Z"}
|
||||
{"cache_key":"a8e2ae138d39cdce280859f816898618e76290b2bc9b57f926dde5c4c150f893","model":"gpt-5.5","provider":"openai","segment_id":"claudeSessions.transcript.toolResult","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Tool result","text_hash":"9bb620efa692f707a302a5f42464015a54c20843e2f76f18a1542626b886bb91","tgt_lang":"ja-JP","translated":"ツールの結果","updated_at":"2026-07-11T13:50:31.844Z"}
|
||||
{"cache_key":"a910809f88a20b6501c54dd454679bdcb761b3da05281e0cf733bfa258a7d0b0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.execApprovals.on","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"on","text_hash":"b8d31e852725afb1e26d53bab6095b2bff1749c9275be13ed1c05a56ed31ec09","tgt_lang":"ja-JP","translated":"オン","updated_at":"2026-07-12T06:31:51.627Z"}
|
||||
{"cache_key":"a9890403d41a02984f8df18075a3c2fe4dce04b70d980279e838429f25622f81","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"ja-JP","translated":"セッションに接続しています…","updated_at":"2026-07-15T00:45:11.997Z"}
|
||||
{"cache_key":"a98d042f52a274515ae8b61ab23a7c03467d924f4e9d9e5399c7033b75ce6f38","model":"gpt-5.5","provider":"openai","segment_id":"browser.reload","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Reload","text_hash":"bdc090ec61e3fcfc65f469951dfe00f3f2ecfc6003c44deac8e05b7237092de6","tgt_lang":"ja-JP","translated":"再読み込み","updated_at":"2026-07-11T02:17:57.654Z"}
|
||||
{"cache_key":"a9a3fd54b08bbe0382dab2c8e5f07c346dbf83de9303341cae3c109bb24162b5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.nostr.subtitle","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Decentralized DMs via Nostr relays (NIP-04).","text_hash":"f910815433f5df92e08b45c501380a6607a3bb86a66dd51bfc5ea32720f918d3","tgt_lang":"ja-JP","translated":"Nostr リレー経由の分散型 DM(NIP-04)。","updated_at":"2026-07-12T06:31:25.246Z"}
|
||||
{"cache_key":"a9ab379f6f3edb6a98e5d1eb2046e2b733cb7be57d9d0ff54b8f8854e530efc0","model":"gpt-5.6-sol","provider":"openai","segment_id":"agents.identity.name","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Display name","text_hash":"2b7f6a84de917e387539dbe441ca22056e793d738e6c50db30c3f569e4448df3","tgt_lang":"ja-JP","translated":"表示名","updated_at":"2026-07-13T05:29:43.148Z"}
|
||||
@@ -1192,6 +1193,7 @@
|
||||
{"cache_key":"ab549fa4fb01c4471d13b3fefd1d408d3039a20a2f666c49973bd4512f20f0f8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"githubPreview.ariaLabel","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{state} {kind} {repo} #{number}: {title}, by {author}","text_hash":"e18c624550814e4318a45e1e0f8782913409763d046aa703519e91c887be205b","tgt_lang":"ja-JP","translated":"{state} {kind} {repo} #{number}: {title}、作成者 {author}","updated_at":"2026-07-12T06:31:19.391Z"}
|
||||
{"cache_key":"ab59ca701851244c4776ae00dea97ebb790841296d301b61c5bde750690eb171","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.sections.talk.label","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Talk","text_hash":"449f5a775762cdc561ee231300af1b02be67c56b81e503058a1c777ea1dc8b55","tgt_lang":"ja-JP","translated":"Talk","updated_at":"2026-07-12T06:32:47.571Z"}
|
||||
{"cache_key":"ab9ee088c32c48932a47f1df2584997d5042b2eec59c70b562de0f079b71fbf5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillsPage.refreshing","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Refreshing…","text_hash":"1c0def7be0607b966b89e4974da38090472d8ada625f5b4c89f25b09d39683bd","tgt_lang":"ja-JP","translated":"更新中…","updated_at":"2026-07-12T06:34:27.939Z"}
|
||||
{"cache_key":"abc210f438b105429b48544f027a713be4aa4b294521935c6b46626ce8420a94","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"ja-JP","translated":"30秒以内にセッションに接続できませんでした。","updated_at":"2026-07-15T00:45:11.997Z"}
|
||||
{"cache_key":"ac161ebbba0207c1eee4d54140004b06269f533b765e00816f872711ea6a4a78","model":"claude-opus-4-6","provider":"anthropic","segment_id":"configView.autoSaveSaved","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Saved","text_hash":"b5c120b316c237a0deab3140267aebedba61947d3ae268d708a53d5be119e9fb","tgt_lang":"ja-JP","translated":"保存済み","updated_at":"2026-07-14T12:52:40.223Z"}
|
||||
{"cache_key":"ac368e893d2db8f865eac84435d4ba801a31552756358a6a64b533261b0a1c44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.today.upNext","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Up next · {count} more waiting","text_hash":"9f2a67f22d9ca908c4251c43b81269329e118fd9c8dfc42dea3bb006924b8ad2","tgt_lang":"ja-JP","translated":"次へ · あと {count} 件が待機中","updated_at":"2026-07-12T06:35:20.102Z"}
|
||||
{"cache_key":"ac3b2e583623039a3990a982b7bf0d5a7afc0c7e44521518d466e6e530cde28f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"ja-JP","translated":"ブロック中","updated_at":"2026-06-17T14:14:09.255Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.793Z",
|
||||
"generatedAt": "2026-07-15T00:45:14.871Z",
|
||||
"locale": "ko",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -1150,6 +1150,7 @@
|
||||
{"cache_key":"a5f1ea8004c1818dd7c0ce0b970f49a275f0579437bc0470a1c52a5591a7f602","model":"claude-sonnet-4-6","provider":"anthropic","segment_id":"cron.suggestions.ideas.nightWatch.prompt","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Check that my services and gateway are healthy: scan recent logs for new errors, restarts, or unusual load. Stay quiet if everything is fine; if something looks broken, message me what failed and where to start looking.","text_hash":"ff691fc0bcd98cc27aea75758d323fc926a74252f5007eb43b49073d1a5716b7","tgt_lang":"ko","translated":"내 서비스와 Gateway가 정상인지 확인해 주세요: 최근 로그에서 새로운 오류, 재시작, 또는 비정상적인 부하를 스캔하세요. 모든 것이 정상이면 조용히 있어도 됩니다; 문제가 보이면 무엇이 실패했고 어디서부터 확인해야 할지 알려주세요.","updated_at":"2026-07-11T22:45:43.800Z"}
|
||||
{"cache_key":"a60533aa79fa5e103f52b3712766e13309241f3e3eec56c0e52086783e112a4d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillsPage.clawHub","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"ClawHub","text_hash":"a2019fe71279ebb59b7876298299699524a6eb7885a04484409a1b556c8548f3","tgt_lang":"ko","translated":"ClawHub","updated_at":"2026-07-12T06:34:39.346Z"}
|
||||
{"cache_key":"a63f8e6abe07b9dc720665586dcd28dab5183f43c90dbc074bd44c6bc5533519","model":"gpt-5","provider":"openai","segment_id":"approvalPage.nodeLabel","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Node","text_hash":"e93372533f323b2f12783aa3a586135cf421486439c2cdcde47411b78f9839ec","tgt_lang":"ko","translated":"노드","updated_at":"2026-07-09T10:01:43.732Z"}
|
||||
{"cache_key":"a65aaf5bcb5862a72eac0d3f7d04b5cde40c016c9c6227f13c9ff6acf1288892","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"ko","translated":"30초 이내에 세션에 연결하지 못했습니다.","updated_at":"2026-07-15T00:45:14.871Z"}
|
||||
{"cache_key":"a67b1892e0c6b7bf13dcece3156d2b99cd74845998c94e22562d9a50e3521644","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.inventory.gateway","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"gateway","text_hash":"4ea5ee68fea05586106890ded5733820bb77d919cda27bc4b8139b7cd33b8889","tgt_lang":"ko","translated":"gateway","updated_at":"2026-07-12T06:31:59.316Z"}
|
||||
{"cache_key":"a67c411e720506873247a370e8df0d3807cabf66074e6ce3064d68282387f75a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"common.previous","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Previous","text_hash":"a57b08a480b822a0a572b993391c292ede593bf8000b406675b180bbb16260fa","tgt_lang":"ko","translated":"이전","updated_at":"2026-07-12T06:31:28.728Z"}
|
||||
{"cache_key":"a69af17552a6ee7f48b7b0e9b0e6ef8a989ee7760fc5e62e14c2bd6bcbacc64b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.notifications.hint","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Receive browser push notifications from your gateway.","text_hash":"1a90345f698ef3383b5aaef3cce80cd844e49a2d6301876816c49439dc17661e","tgt_lang":"ko","translated":"Gateway에서 브라우저 푸시 알림을 받습니다.","updated_at":"2026-07-12T06:33:43.023Z"}
|
||||
@@ -1756,6 +1757,7 @@
|
||||
{"cache_key":"f9da79be76e39c522c57e3669b99da2d00c9c51ad557689db58ea87d6152b238","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.form.lightContextHelp","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Use lightweight bootstrap context for this agent job.","text_hash":"6aebad7bcc7f52b2df758588930165ccdfce28f58fe77c2db43a2fb5e950da00","tgt_lang":"ko","translated":"이 에이전트 작업에 경량 부트스트랩 컨텍스트를 사용합니다.","updated_at":"2026-07-12T06:36:45.775Z"}
|
||||
{"cache_key":"f9db17659980eab33947cd7850f01bb822cb3a3c04b26d5307ee9e76ef22fdf4","model":"gpt-5.5","provider":"openai","segment_id":"quickSettings.appearance.lobsterVisitsOff","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Never visits","text_hash":"4892012cb692f089af106c0533e80e3ce3331a7ff839bd2a1416a286cc85c6e8","tgt_lang":"ko","translated":"절대 방문하지 않습니다","updated_at":"2026-07-09T20:51:31.206Z"}
|
||||
{"cache_key":"fa66a6052376197305c4f49ae4cbd10229bbe2376fe80c30a16e0335a1823722","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"ko","translated":"현재 채팅 사용","updated_at":"2026-06-16T14:14:21.259Z"}
|
||||
{"cache_key":"fa79f0431eb964c1740c6f63d83fe0c0636d71e13ac2ca61dce5740d6b8b5dc6","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"ko","translated":"세션에 연결하는 중…","updated_at":"2026-07-15T00:45:14.871Z"}
|
||||
{"cache_key":"fa8badc022ea990163ca945913fd3c521455cbf703b2d1cc9f0412531f719364","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillStatus.eligible","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"eligible","text_hash":"fc99c152d84713fe276da3f905807ab166666451a8736224efc2fee710658d5f","tgt_lang":"ko","translated":"사용 가능","updated_at":"2026-07-12T06:34:44.618Z"}
|
||||
{"cache_key":"fabc3f3809de6e206f6065fa8e23817c8f2c6cb0203c9f9c857627b866d7c873","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.messages.unknownDate","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Unknown date","text_hash":"ad224bb89c8a3fb6dc0b567dd13c3c4f104deb2acf64aa3679d85301f007a6ee","tgt_lang":"ko","translated":"알 수 없는 날짜","updated_at":"2026-07-12T06:36:13.954Z"}
|
||||
{"cache_key":"fabfa09f8370e02212d114ad149c6b6a57221b3752e6b220799886492d0142f4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"ko","translated":"세션","updated_at":"2026-06-16T14:14:34.760Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.963Z",
|
||||
"generatedAt": "2026-07-15T00:45:44.990Z",
|
||||
"locale": "nl",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -60,6 +60,7 @@
|
||||
{"cache_key":"072f393e1442e87be86ef8b4a54ebe37b6a104f54208c7826b862a7c233d5355","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.execApprovals.rules","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} rules","text_hash":"e838d0ef12825f802a10f914fbebc46645de9dacffffe4522eb035cd806039de","tgt_lang":"nl","translated":"{count} regels","updated_at":"2026-07-12T06:52:20.624Z"}
|
||||
{"cache_key":"074147c1fe6ae8a5c4ee3f0c15f9aac7a5c10a1b7212d529233fd2d7fd169e22","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"nl","translated":"actief","updated_at":"2026-06-17T14:17:36.035Z"}
|
||||
{"cache_key":"078f461da84360a2d859b640ae0c8881bf9fbbfaceb42c726644b27a52536199","model":"gpt-5.5","provider":"openai","segment_id":"browser.refresh","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Refresh view","text_hash":"112b4ddb82dac7080c1c85de7475cd9754b2ac7e0a89fbb38126923180d76ec2","tgt_lang":"nl","translated":"Weergave vernieuwen","updated_at":"2026-07-11T02:20:09.408Z"}
|
||||
{"cache_key":"079af6a85cba877e5745498fed553207f6a56958972c563f0449f2eae76ed21f","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"nl","translated":"Er kon niet binnen 30 seconden verbinding worden gemaakt met de sessie.","updated_at":"2026-07-15T00:45:44.990Z"}
|
||||
{"cache_key":"07ab8d07f27ba9f66160aa7a335f9dbe70a346950643509842c8ea56ee4788c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.notifications.unavailableHint","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Not available in this browser.","text_hash":"4dad76698cde08136f50bd8b789e635c4cff19e25b6a4ec62290684958b23add","tgt_lang":"nl","translated":"Niet beschikbaar in deze browser.","updated_at":"2026-07-12T06:54:02.978Z"}
|
||||
{"cache_key":"07cc3fad504faf0eb379a2458ec915d8aa5fe61dce0d006be7fe4ce9a1f4706c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.dragSessionHint","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Drag to move between groups","text_hash":"3e8692b1cd1c1f9a606a779f1037aa390c885fde224ddb841df9259e8aab03ae","tgt_lang":"nl","translated":"Sleep om tussen groepen te verplaatsen","updated_at":"2026-07-05T14:40:20.847Z"}
|
||||
{"cache_key":"07d7e135848387ad8996ffab20b817e8ee33a12368f9ead23770559b6935ee12","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"nl","translated":"Gelezen","updated_at":"2026-06-16T14:18:24.009Z"}
|
||||
@@ -1293,6 +1294,7 @@
|
||||
{"cache_key":"aab054102b0c4897f2379ca669b36ce88883888a2d0aee8e5dad82ad977ae2e3","model":"claude-sonnet-4-6","provider":"anthropic","segment_id":"cron.suggestions.ideas.polyglotMinute.prompt","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Teach me one useful phrase in Japanese: the phrase, how to pronounce it, its literal meaning, and when to use it. Keep it under five lines.","text_hash":"acaeae7dfcaab66a6b06b682970496a2cec2efcde9d564204288f71819578186","tgt_lang":"nl","translated":"Leer me één nuttige zin in het Japans: de zin, hoe je hem uitspreekt, de letterlijke betekenis en wanneer je hem gebruikt. Houd het onder de vijf regels.","updated_at":"2026-07-11T22:49:06.392Z"}
|
||||
{"cache_key":"aacc8671a3d63c2642d47534bf926bf388e3d32d8b0e46142da8a1142ca5a692","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.compaction.label","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Compacted history","text_hash":"1c066091aa0c37ad253bfe195469b0cf82b276a06dea4ccc55e246e175b2e68d","tgt_lang":"nl","translated":"Gecomprimeerde geschiedenis","updated_at":"2026-07-12T06:56:19.372Z"}
|
||||
{"cache_key":"aafacbc4613c7e1c128d420d1f56df1e2fe159561076eee60c0435811b94a129","model":"gpt-5.5","provider":"openai","segment_id":"pluginsPage.mcpServersGroup","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"MCP servers","text_hash":"22a7559f09bf8f82c510280f934bf50db4c45cc611fd4dd47d7cbf7c7d4f5b82","tgt_lang":"nl","translated":"MCP-servers","updated_at":"2026-07-10T02:28:58.148Z"}
|
||||
{"cache_key":"ab170b454009debc335b1925fb8b7a9b572de21048fadb55d09deb3caee6b480","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"nl","translated":"Verbinding maken met sessie…","updated_at":"2026-07-15T00:45:44.990Z"}
|
||||
{"cache_key":"ab2dcada6652ca028ff3f5ed792cfb9b96562fd33d8ca85b5aa867d174c17609","model":"gpt-5.5","provider":"openai","segment_id":"worktrees.actions","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Actions","text_hash":"ff8059dc6752afdd30d275932b1d5031a2ec854b387a8c57ecc6689915293a43","tgt_lang":"nl","translated":"Acties","updated_at":"2026-07-05T21:01:37.071Z"}
|
||||
{"cache_key":"ab570a6ab4088b159718e2cd9d415cf31c1c843db8f35eee542ec8798fea6ad3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.openChat","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Open chat","text_hash":"0600175af8429c3cd44ab859dcdd3cfa5c763d9b352c7b03520196d4ceb59a88","tgt_lang":"nl","translated":"Chat openen","updated_at":"2026-07-09T08:27:29.789Z"}
|
||||
{"cache_key":"abd00aa5e64818b81c3c5fecbde76865bc0a7e3472d7671f262b176fed5f7da4","model":"gpt-5.6-sol","provider":"openai","segment_id":"channels.setup.finish","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Finish","text_hash":"a6c7a84baa6750fce33f7512acd6793e53def1d228b5f2efb8074b42648424fc","tgt_lang":"nl","translated":"Voltooien","updated_at":"2026-07-13T16:53:23.812Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.639Z",
|
||||
"generatedAt": "2026-07-15T00:45:37.101Z",
|
||||
"locale": "pl",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -902,6 +902,8 @@
|
||||
{"cache_key":"7db74e90c18af539e2f75c9664fa6afc220d658a95da94c374182ac19c1b19d9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.detail.overviewTitle","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Recent activity","text_hash":"6cb44b56336af70b050861cb4617a0d729c872fe8206e1104c26edad0b79cb7a","tgt_lang":"pl","translated":"Ostatnia aktywność","updated_at":"2026-07-12T06:50:33.143Z"}
|
||||
{"cache_key":"7ddd791a0eef6e2b81bf314c3f50863a3bb695dff7cc1c092a702be8b7f29e83","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.sections.web","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Web","text_hash":"2975104784a401e3880e2215550e9490eda7e67db5fc2b35e1a244acb092ced3","tgt_lang":"pl","translated":"Sieć","updated_at":"2026-07-12T06:47:28.389Z"}
|
||||
{"cache_key":"7dec11d6123c7df613c6b38e6e7ab35d55ef6c168bac323b4b4b5eac939cb51f","model":"gpt-5.5","provider":"openai","segment_id":"claudeSessions.empty.nonArchived","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"No non-archived sessions on this host.","text_hash":"49ecb9e9967d53ce0ffb0d1d5c1f75a595973708d03a577ba9a6e3f327a428fb","tgt_lang":"pl","translated":"Brak niezarchiwizowanych sesji na tym hoście.","updated_at":"2026-07-11T13:51:15.565Z"}
|
||||
{"cache_key":"7dee7dd62cacb2101661643f0bbaea9b654b19ce195b260730a29b50a95f6bbb","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"pl","translated":"Nie udało się połączyć z sesją w ciągu 30 sekund.","updated_at":"2026-07-15T00:45:37.101Z"}
|
||||
{"cache_key":"7e074f9ab07b5fb3f64893db6ea684536a2616f572dc4a66123c46b7d39ceb2f","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"pl","translated":"Łączenie z sesją…","updated_at":"2026-07-15T00:45:37.101Z"}
|
||||
{"cache_key":"7e4f44c6bafbc6743ed1fa5f6ff990595a3a2a505068873b4a465b3cda63baec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.textSizes.xl","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"XL","text_hash":"f365705bb0612eed918b30a166bd7b5d48908d58cbfb7da0deff2a7b953199ce","tgt_lang":"pl","translated":"XL","updated_at":"2026-07-12T06:47:33.953Z"}
|
||||
{"cache_key":"7e6ba9b4c0cb06b50d82883f6cd7016194c9f93101cf520c224322751709fd94","model":"gpt-5.5","provider":"openai","segment_id":"chat.backgroundTasks.loading","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Loading background tasks…","text_hash":"b8f8eaea7ccdee15740c7daa3d290a534fd3db2b4f0b2835243a6627d9ff7ff6","tgt_lang":"pl","translated":"Ładowanie zadań w tle…","updated_at":"2026-07-11T00:45:31.838Z"}
|
||||
{"cache_key":"7e814784495586c151e4c556cb072b933df4b4f74760e5ba077fff751c577ea8","model":"gpt-5.6-sol","provider":"openai","segment_id":"connection.snapshot.lastChannelsRefresh","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Last Channels Refresh","text_hash":"97a20d4f5b29914b8a08748cfc55d704a4d52ed948180cc90b7c1e06267c692f","tgt_lang":"pl","translated":"Ostatnie odświeżenie kanałów","updated_at":"2026-07-12T00:10:05.075Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.367Z",
|
||||
"generatedAt": "2026-07-15T00:45:05.102Z",
|
||||
"locale": "pt-BR",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -630,6 +630,7 @@
|
||||
{"cache_key":"5494d598782d821e79e7a9eab1e6e942bd34909e44a5618d1ce487b2189c4804","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.actions.previous","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Previous","text_hash":"a57b08a480b822a0a572b993391c292ede593bf8000b406675b180bbb16260fa","tgt_lang":"pt-BR","translated":"Anterior","updated_at":"2026-07-12T06:28:16.888Z"}
|
||||
{"cache_key":"549a2440defaf37562a29331c04387f66a0345d9b69feab4ca72d0bd060683b5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.messages.openInCanvas","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Open in canvas","text_hash":"02597bb20cb5d93ebbb47e62c3591fcf9dec76fc42697e1dc130e1f167232e61","tgt_lang":"pt-BR","translated":"Abrir no canvas","updated_at":"2026-07-12T06:29:10.164Z"}
|
||||
{"cache_key":"549e4fe33824e518937b667adb9ece09a9f6f731e794697856236aa728257494","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillsPage.verdict.blocked","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"pt-BR","translated":"Bloqueado","updated_at":"2026-06-17T14:13:15.046Z"}
|
||||
{"cache_key":"549f9eb5bfb47109c634961ade76e170c4c4ce6b3d29f76bad18851a389818d2","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"pt-BR","translated":"A sessão não foi conectada em até 30 segundos.","updated_at":"2026-07-15T00:45:05.102Z"}
|
||||
{"cache_key":"54e413d127eabc5d41ba8916286c51228984010b95c36efea00704ed25e04c56","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.themes.claw.label","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Claw","text_hash":"587cfbbbcd42a71ba7f33f8051d5c354707fa1ff227cab11452c3ba2147a8682","tgt_lang":"pt-BR","translated":"Claw","updated_at":"2026-07-12T06:26:59.355Z"}
|
||||
{"cache_key":"54f626f2673974a4dc9ac0d723d762fb12942a16d33fabec18dc9f13ac410200","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agentTools.toolsOne","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} Tool","text_hash":"1b25a793182ec17a490365d4b12703940e9e4592bc40c4a48d66a7d9ed06adcd","tgt_lang":"pt-BR","translated":"{count} ferramenta","updated_at":"2026-07-12T06:27:35.199Z"}
|
||||
{"cache_key":"55058cc343c6f9340676eff66678c5afd5ef256ccd02c97f0dafc4a72c1787f9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.sections.channels.description","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Messaging channels (Telegram, Discord, Slack, etc.)","text_hash":"6e372c3083a4d6153c076740549e9cc20995bcb1bd0ef43950671b7845b6e07d","tgt_lang":"pt-BR","translated":"Canais de mensagens (Telegram, Discord, Slack, etc.)","updated_at":"2026-07-12T06:26:05.905Z"}
|
||||
@@ -1365,6 +1366,7 @@
|
||||
{"cache_key":"bedac72b4656686d4b258e89f738336b8cd4cbda02583c14642f64da8dccd5a4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"quickSettings.system.used","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"used","text_hash":"f839161355091fcd9e33f571e0b11c60217a5009f9973e769c13d7858db162cc","tgt_lang":"pt-BR","translated":"usado","updated_at":"2026-07-12T06:26:38.130Z"}
|
||||
{"cache_key":"bf56aab3b35d65a3dedc9a41f3b56833baa554735d2f6e8e590155eccdf868fb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.working","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{name} is working...","text_hash":"dbce69e1f37797e32879e9960125e409ed6f1e36e238cfd0898eb60ea839deca","tgt_lang":"pt-BR","translated":"{name} está trabalhando...","updated_at":"2026-07-12T06:29:21.041Z"}
|
||||
{"cache_key":"bf5d0d78c050f9e4e5347bf603a69da4e5799d7204beb7a36c67fb884b7aa61c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"pt-BR","translated":"QR de pareamento expirado","updated_at":"2026-07-01T10:31:00.782Z"}
|
||||
{"cache_key":"bf6c8276cc89c2e07cfb243ee92d7b13129242791f6367f948d0eb54fd6e2300","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"pt-BR","translated":"Conectando à sessão…","updated_at":"2026-07-15T00:45:05.102Z"}
|
||||
{"cache_key":"bfd3b08dbaf6bb1b7b282c2f6bede7b2cd75d7412036270de7692ca19d0aa0bb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.resetToDefault","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Reset to default","text_hash":"bc5b45ae7b60692ade0bb26ebe5b6830f150d97134153e3c3091882eb72e7c25","tgt_lang":"pt-BR","translated":"Redefinir para o padrão","updated_at":"2026-07-12T06:26:00.301Z"}
|
||||
{"cache_key":"c00ff84c177f8c24cf5c3ba1cdb8a383441b8b7c412206a22253281022ca4f3d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillsPage.reason","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Reason: {reasons}","text_hash":"f98758e12634814d8357b70d457d3847a9c8d641993694b536c63367075187b3","tgt_lang":"pt-BR","translated":"Motivo: {reasons}","updated_at":"2026-07-12T06:27:51.283Z"}
|
||||
{"cache_key":"c037fa2fa74af71c6693ddb13369398a2936c62e789d5e4004dbf84dbcedac19","model":"gpt-5.6-sol","provider":"openai","segment_id":"chat.progressLabels.pinching","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Pinching","text_hash":"01cfb797d96a35b63f28cf276b5c777df0f256885b7c0f5e0d2d35b418daf7d5","tgt_lang":"pt-BR","translated":"Beliscando","updated_at":"2026-07-14T04:53:03.154Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:52.213Z",
|
||||
"generatedAt": "2026-07-15T00:45:50.229Z",
|
||||
"locale": "ru",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -2618,6 +2618,7 @@
|
||||
{"cache_key":"d27d192faa982d1f1a0c3b444c5c6e5fa3d8759c79b2be176257c9ade031a3e2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.appearance.replace","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Replace {name}","text_hash":"93a0cf0f05a0f232f354d458882d45d10c60c5bc12c89a5df7f4cb6553233fcd","tgt_lang":"ru","translated":"Заменить {name}","updated_at":"2026-07-12T07:00:21.256Z"}
|
||||
{"cache_key":"d28bf8b903552be6f3384de908b08d85882961ef6f3b6fd09ba7a4219030f656","model":"gpt-5.5","provider":"openai","segment_id":"overview.palette.footer.select","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"select","text_hash":"b1a36d25d9633ed2ac04939fcb614ccb2b513243c148f18694592ae037f9d35f","tgt_lang":"ru","translated":"выбрать","updated_at":"2026-06-26T21:40:37.573Z"}
|
||||
{"cache_key":"d29b4bbb191e0c891d1b249c5b774fcf108482d7df85df83117caed8f45c31d2","model":"gpt-5.6-sol","provider":"openai","segment_id":"agents.identity.subtitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Name, emoji, and avatar shown in chats and the sidebar.","text_hash":"a7fa9a1082ea324736d132dd25b5940c0b2b2edc5117ffed30a35ff47b175d0e","tgt_lang":"ru","translated":"Имя, эмодзи и аватар, отображаемые в чатах и на боковой панели.","updated_at":"2026-07-13T05:31:27.367Z"}
|
||||
{"cache_key":"d2a3081c1e540a86a0aefcef6ca29bdfbe46051c98ba4794eed2ef89db6f0874","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"ru","translated":"Подключение к сеансу…","updated_at":"2026-07-15T00:45:50.229Z"}
|
||||
{"cache_key":"d2b87ae61cb8d7763072763db801029f2cc4017b4ee970e5d10c885937fb692d","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobList.edit","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Edit","text_hash":"464c4ffd019e1e9691dcf0537c797353ef2b1c1d4833d3d463e5b74ae4547344","tgt_lang":"ru","translated":"Изменить","updated_at":"2026-06-26T21:42:47.554Z"}
|
||||
{"cache_key":"d2cc6dc04e56f4e28ea0bc76cff98d2d741ff954b9a91d268169c3af4053277c","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.trace.emptyShortTerm","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No active short-term items.","text_hash":"e3a71c5ac02b76384ed603efc99062bf70b21092fd094fb3a7c0b3e2647ee757","tgt_lang":"ru","translated":"Нет активных краткосрочных элементов.","updated_at":"2026-06-26T21:40:48.120Z"}
|
||||
{"cache_key":"d2d592685376469426cf4d5cf32e5c2884b19942c6087bdc7e02cee567fb9d4b","model":"gpt-5.5","provider":"openai","segment_id":"worktrees.owner","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Owner","text_hash":"4b1b8aa3608a26da451ae0630d75b60ab1bc2dd229c41a80838fc7993e835c46","tgt_lang":"ru","translated":"Владелец","updated_at":"2026-07-10T18:00:13.179Z"}
|
||||
@@ -3085,6 +3086,7 @@
|
||||
{"cache_key":"f86284c2698db911ebac5d20ef9eab007ead6a773546341ea6d76e14f3c51a16","model":"gpt-5.5","provider":"openai","segment_id":"configView.sections.models","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Models","text_hash":"d17d2d78d76e6a6cd13048de225e107ac73de4aa5f8914e58a0cfaa9698c373e","tgt_lang":"ru","translated":"Модели","updated_at":"2026-06-26T21:39:17.817Z"}
|
||||
{"cache_key":"f86ce0fbee87217702d44a021e0d51c0a564fd15a0c4f1e43f2d669cc5077e35","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.defaultOption","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Default ({value})","text_hash":"a9d6571117890ef77ecc72f77ba43e9d8b05ed82c1c64ff27a352c02dff3c2bd","tgt_lang":"ru","translated":"По умолчанию ({value})","updated_at":"2026-06-26T21:38:54.164Z"}
|
||||
{"cache_key":"f86e511303df40c74db4a1e9ad9803c68716427fbdba55e442a64433d69eb5ad","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.filters","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Filters","text_hash":"546ebb8eb993ea561029d9febd84c363bdb09010bb2cb915a8287762b76b9a64","tgt_lang":"ru","translated":"Фильтры","updated_at":"2026-06-26T21:38:48.084Z"}
|
||||
{"cache_key":"f89f34ef96a6eb801473ff9d358d21c5cf563930947af394eaaa7d34cf6d266e","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"ru","translated":"Не удалось подключиться к сеансу в течение 30 секунд.","updated_at":"2026-07-15T00:45:50.229Z"}
|
||||
{"cache_key":"f8d1664c037238e7b7966b8af30a892ceffd853ee093dcda14325c2e08271995","model":"gpt-5.5","provider":"openai","segment_id":"common.reloadConfig","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Reload Config","text_hash":"48e6315352561c36be84097326fbb3558b4c2fa3fc4f833402d32040ccb640f7","tgt_lang":"ru","translated":"Перезагрузить конфигурацию","updated_at":"2026-06-26T21:38:30.975Z"}
|
||||
{"cache_key":"f8d36cbffbfdec2566a29cc23f954616dee2fc091172edc9c98a74ea674d6328","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.dateToday","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Today","text_hash":"2b065c7c9ce466e5ebcad757987d5d660ee4c9ea708bc62c43444b53334738ba","tgt_lang":"ru","translated":"Сегодня","updated_at":"2026-06-26T21:40:56.975Z"}
|
||||
{"cache_key":"f8dcfb5a4fe291ae049cdc2770276434112e2c96a769f23ad9f2df36647fc2aa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agentTools.loadConfig","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Load the gateway config to adjust tool profiles.","text_hash":"0e9291ef2bab7a6a96376087b37d281a973319e0c5b893360b67cf5681d8c863","tgt_lang":"ru","translated":"Загрузите конфигурацию gateway, чтобы настроить профили инструментов.","updated_at":"2026-07-12T07:00:42.425Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.742Z",
|
||||
"generatedAt": "2026-07-15T00:45:39.822Z",
|
||||
"locale": "th",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -84,6 +84,7 @@
|
||||
{"cache_key":"0b46aea66abe0e5da609d04faa21bcd1568c58bb3ade9b485b3f88cede82f0bd","model":"gpt-5.5","provider":"openai","segment_id":"chat.pullRequests.checksFailed","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Failed","text_hash":"031a8f0f659df890dfd53c92e45295b0f14c997185bae46e168831e403b273f7","tgt_lang":"th","translated":"ล้มเหลว","updated_at":"2026-07-10T23:12:49.455Z"}
|
||||
{"cache_key":"0b51c6aff1f66c79ba67dd53f88619f76829d952af88fbb1ffafffd080404e7a","model":"gpt-5.5","provider":"openai","segment_id":"usage.providerUsage.lastDays","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"{count} days","text_hash":"e9f0a85930cc6fa61b7ac01763893020adc4c712d1b8e8897bdd13971637d529","tgt_lang":"th","translated":"{count} วัน","updated_at":"2026-07-06T06:40:15.357Z"}
|
||||
{"cache_key":"0b576e45e59c88e6195e7ba95d23bd9db989d475a96b1eeebe56e6a9f74127e5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agentTools.enabledByOverride","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Enabled by agent override.","text_hash":"298b111d71465ff9092838988e31a5d6fbdb8557d301e99fae621ba1924e1fb6","tgt_lang":"th","translated":"เปิดใช้งานโดยการแทนที่ของเอเจนต์","updated_at":"2026-07-12T06:54:00.247Z"}
|
||||
{"cache_key":"0bacd36c88ada0c6ca89e58796a7c7837bd0d817aa8f55759eefe341e9489dc3","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"th","translated":"กำลังเชื่อมต่อกับเซสชัน…","updated_at":"2026-07-15T00:45:39.822Z"}
|
||||
{"cache_key":"0bc2280a1a058c478c10765ef0fd9ed59142e530a465dbf8147094d7453b9244","model":"gpt-5.6-sol","provider":"openai","segment_id":"skillWorkshop.selfLearning.pitchBody","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"OpenClaw reviews corrections and substantial completed runs, then drafts skill proposals for this board. It spends extra background tokens and every proposal waits for your review.","text_hash":"b06e460a44a232840a13f7cefdb87910f61da21ddafe0c9cc40eecb9be9e6757","tgt_lang":"th","translated":"OpenClaw จะตรวจสอบการแก้ไขและการดำเนินงานสำคัญที่เสร็จสมบูรณ์ จากนั้นร่างข้อเสนอ Skills สำหรับบอร์ดนี้ โดยจะใช้โทเค็นเบื้องหลังเพิ่มเติม และทุกข้อเสนอจะรอให้คุณตรวจสอบ","updated_at":"2026-07-13T06:16:22.653Z"}
|
||||
{"cache_key":"0bca8ee9c50779080f2276c69d865ed5a4369614745fd8c52b5cf88da23c0b16","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"th","translated":"เพิ่งเสร็จ","updated_at":"2026-06-17T14:16:47.316Z"}
|
||||
{"cache_key":"0bd7f0ec49e6bb6fccc86a5dc42fc6a06aed138f112e66e6edda18e9721797a3","model":"gpt-5.6-sol","provider":"openai","segment_id":"connection.access.showPassword","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Show password","text_hash":"6aeaa6a53d09dcad071fdda6280b1e7c42aa164cd0514304ff162e7da440ffaa","tgt_lang":"th","translated":"แสดงรหัสผ่าน","updated_at":"2026-07-12T00:10:17.485Z"}
|
||||
@@ -1181,6 +1182,7 @@
|
||||
{"cache_key":"a0cecd735cd38870804887fc91e904ee91769eb29f1efd33539e05ef9fcf28d2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"filePreview.readOnly","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"read-only","text_hash":"4fed3970dcc0d31dccddbf69ef55b00f32d2ca787f1757894914bc0365ea7aa5","tgt_lang":"th","translated":"อ่านอย่างเดียว","updated_at":"2026-07-12T06:49:19.074Z"}
|
||||
{"cache_key":"a0d4438f197d54754a0ecf05a40ae1be4952b538453fc661b0d8f759735ac598","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.nostr.placeholders.lightningAddress","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"you@getalby.com","text_hash":"7c433ad5c3a532bd4ab1a634c4ac4d75cbd857e41b1de75b50ac8c8b16ccf319","tgt_lang":"th","translated":"you@getalby.com","updated_at":"2026-07-12T06:49:37.673Z"}
|
||||
{"cache_key":"a0db05454e35ec33757ac8fa9e37a2bf5b813584cc87ae0ed708317e3658500e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.sideChat.title","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Side chat","text_hash":"77db106985161e4794ca08b6f32f3cec864d91f7b8107b21b30c52be19ed0236","tgt_lang":"th","translated":"แชทด้านข้าง","updated_at":"2026-07-12T06:57:50.389Z"}
|
||||
{"cache_key":"a0ec6e2a50d73f982c30b7ad4f0f03c2ec6017129ae8dbbe4d7df437cc871d6f","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"th","translated":"ไม่สามารถเชื่อมต่อเซสชันได้ภายใน 30 วินาที","updated_at":"2026-07-15T00:45:39.822Z"}
|
||||
{"cache_key":"a10d87a8e1f91b20dabe0977ab313c1e9e3ed00a447c1d5f0876e2d071d0f009","model":"gpt-5","provider":"openai","segment_id":"codexSessions.host.connected","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Connected","text_hash":"22965568d22a14ee17af055d2870b50afcfe9fd94a83eec3196e266932297bb2","tgt_lang":"th","translated":"เชื่อมต่อแล้ว","updated_at":"2026-07-09T10:01:43.768Z"}
|
||||
{"cache_key":"a1371bbf9cc3ef23b2f5f39aea49299c961f1a81bdac841c6b2e091b409968c8","model":"gpt-5.6-sol","provider":"openai","segment_id":"aboutPage.linkGitHub","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"GitHub","text_hash":"f911e414cf6bdfc595532ab166b5ba0f63d73c021452fcdacbda363dda6ad8fb","tgt_lang":"th","translated":"GitHub","updated_at":"2026-07-13T17:00:15.912Z"}
|
||||
{"cache_key":"a19793da03829e99bb156deb2638814f8ad817c4b3266bad12c066b119200694","model":"gpt-5.6-sol","provider":"openai","segment_id":"configPage.content","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Settings content","text_hash":"e4dcd3d9bff6ba5422b5216d88009d9b4be8eb0e6c65d25781cb3441c80e0597","tgt_lang":"th","translated":"เนื้อหาการตั้งค่า","updated_at":"2026-07-13T13:04:23.414Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.326Z",
|
||||
"generatedAt": "2026-07-15T00:45:28.240Z",
|
||||
"locale": "tr",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -663,6 +663,7 @@
|
||||
{"cache_key":"607fa5183ba39bbc7c00b2fbf9422837367a0c297be46a3b6688895cee8436a2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"quickSettings.automation.configure","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Configure →","text_hash":"3853cdcbe571c3cc86f127a799dfbcc2540b79f092761008c94f70976f8d9e7c","tgt_lang":"tr","translated":"Yapılandır →","updated_at":"2026-07-12T06:39:49.200Z"}
|
||||
{"cache_key":"60a81b02bbbe7ccb8d41c22387b9e72efe27aeb50f80e0734f9c3c7937195fb2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.appearance.themeLink","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Theme link or ID","text_hash":"5c6e9a2d22ee3070ff697719d1236c9381856b1737b511563084ffca7f74d797","tgt_lang":"tr","translated":"Tema bağlantısı veya kimliği","updated_at":"2026-07-12T06:40:40.562Z"}
|
||||
{"cache_key":"60a9849004064cd9509ec3e357f872ac97ae548355170af6967076900e120dbd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"pluginsPage.connectorDescriptions.email","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Mailbox triage, summaries, and drafts with send-on-approval.","text_hash":"ba48296df0613eff276edad474c32efeb8ffec2590287d7f6de51a4de26f4d0f","tgt_lang":"tr","translated":"Onay üzerine gönderme özelliğiyle posta kutusu önceliklendirme, özetler ve taslaklar.","updated_at":"2026-07-12T06:41:55.091Z"}
|
||||
{"cache_key":"60c642f6cdcd25502be33d33ba8d90121baf4efb40b57f595420f0933c9f1432","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"tr","translated":"Oturuma bağlanılıyor…","updated_at":"2026-07-15T00:45:28.240Z"}
|
||||
{"cache_key":"60db5fc671394b1fcca3c8f55e2f5ace272cc439e19f4233bee049394ab1c314","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.inventory.inputAgo","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"input {time} ago","text_hash":"cf922e918893ebac328c042f6cb71f6381c808dbdf06faa23a962bf21ac225f1","tgt_lang":"tr","translated":"girdi {time} önce","updated_at":"2026-07-12T06:38:23.899Z"}
|
||||
{"cache_key":"610e81960ab9e3620b58537c737e3060f0910ab2e2e73b599ac5bf8a55efc653","model":"gpt-5.5","provider":"openai","segment_id":"profilePage.loading","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Diving for stats…","text_hash":"81a616d9c5f179db9a23f119bd752b6c0445a3e37bcc28e76eab97fd29da7c74","tgt_lang":"tr","translated":"İstatistikler için dalınıyor…","updated_at":"2026-07-09T11:28:03.876Z"}
|
||||
{"cache_key":"61135b4bc8e62f5a047e4ec5c2b3fbb7a88e83c3e1643c1e3bb7c0ea3e058386","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.sections.theme","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Theme","text_hash":"efb52e7172b77731d996ff4f51cd7b3dcfd55fc6f07392994619418d58d170dd","tgt_lang":"tr","translated":"Tema","updated_at":"2026-07-12T06:40:24.490Z"}
|
||||
@@ -1425,6 +1426,7 @@
|
||||
{"cache_key":"ce0fee74eb15e48d1a90ac3db4907e6cb39b62575daa408cb2c9d4fe2051533e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.groupByChannel","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Channel","text_hash":"ce4683e7013a18cdf3d224bfcb4e9594ea8f559e946a837c633defe7d3c32172","tgt_lang":"tr","translated":"Kanal","updated_at":"2026-07-05T14:40:02.505Z"}
|
||||
{"cache_key":"ce20ae4a3e847496f1d8e55da89ff57ecd6742b674667e0334fe271216b6919f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commands.clearDescription","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Clear chat history","text_hash":"797a31a346b1a6256296ebe1b6d04dfdf22a2d9d67ad2d2c48370720c0c4bdae","tgt_lang":"tr","translated":"Sohbet geçmişini temizle","updated_at":"2026-07-12T06:43:13.126Z"}
|
||||
{"cache_key":"ce7fb70fbee4ffd9f71ce61b44152ea4fad646f86933576580c8faf393882259","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.nostr.placeholders.username","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"satoshi","text_hash":"da2876b3eb31edb4436fa4650673fc6f01f90de2f1793c4ec332b2387b09726f","tgt_lang":"tr","translated":"satoshi","updated_at":"2026-07-12T06:38:14.794Z"}
|
||||
{"cache_key":"ce8ae11155554d37b79de6ad693d99181f7929fb0b06ecd56305e2f87ddc1f62","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"tr","translated":"Oturum bağlantısı 30 saniye içinde kurulamadı.","updated_at":"2026-07-15T00:45:28.240Z"}
|
||||
{"cache_key":"cec1be0c36dacef8e37f132517a334985510bcd01541a58f5233a13ccbc99c2f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.detailPanel.copyPath","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"tr","translated":"Yolu kopyala","updated_at":"2026-06-16T14:15:58.007Z"}
|
||||
{"cache_key":"cec993593eeea34ecd5b372d3cf523d5914ec98322acfd7fc339d1b2f4a0152d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agents.toolCatalog.descriptions.write","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Create or overwrite files","text_hash":"b29a2552b08fc6afff15e7d9e6276beeaa33e59f3fa298a999c6e8115b487d7a","tgt_lang":"tr","translated":"Dosya oluştur veya üzerine yaz","updated_at":"2026-07-12T06:39:05.862Z"}
|
||||
{"cache_key":"ced5928326b77d658f564ce934ea46eca8b0e8151294b842fd435a92ab3596d7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.sections.nodeHost","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Node Host","text_hash":"5dead206faf634af13473cb72f84b9c9fcd99d2cc6191a0ab06f2ff026853d9f","tgt_lang":"tr","translated":"Node Host","updated_at":"2026-07-12T06:40:19.200Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.436Z",
|
||||
"generatedAt": "2026-07-15T00:45:31.680Z",
|
||||
"locale": "uk",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -611,6 +611,7 @@
|
||||
{"cache_key":"4c9fc4d357241738a36aa22670508d2dc9ebc19ae7905ab7d914f55f86091975","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"uk","translated":"Використовувати поточний чат","updated_at":"2026-06-16T14:15:40.204Z"}
|
||||
{"cache_key":"4d31c39f0ad51921b6e5a644fc60333aa826d9c64f6d792d052c97c70411964c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.notifications.sendTest","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Send test","text_hash":"22d7b11d40c98b51df5d10fdb6d5dfdf7eb83481ba75b5269aad4fab51010615","tgt_lang":"uk","translated":"Надіслати тест","updated_at":"2026-07-12T06:46:56.909Z"}
|
||||
{"cache_key":"4d4431540fcb12e4807302aab91fe2a484dc7cac8646a5dae5476718ea08fdce","model":"gpt-5.5","provider":"openai","segment_id":"browser.refresh","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Refresh view","text_hash":"112b4ddb82dac7080c1c85de7475cd9754b2ac7e0a89fbb38126923180d76ec2","tgt_lang":"uk","translated":"Оновити подання","updated_at":"2026-07-11T02:19:21.035Z"}
|
||||
{"cache_key":"4d5240441cf4c3f832e9d378b44da2433a6f1b981a00001f2dcecaa50d9f571b","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"uk","translated":"Не вдалося підключитися до сеансу протягом 30 секунд.","updated_at":"2026-07-15T00:45:31.680Z"}
|
||||
{"cache_key":"4d873333457c611d47bf6cc0a7bf0377b4e16f16d89afd46ae75f09685af8c54","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"uk","translated":"Використовувати поточний чат для запитів на перегляд","updated_at":"2026-06-16T14:15:40.204Z"}
|
||||
{"cache_key":"4dbbcfd455caf06766a2ef4a8514af8a927369a4eb7dc168205feb3f9261f9dd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"uk","translated":"Сесія","updated_at":"2026-06-16T14:16:00.845Z"}
|
||||
{"cache_key":"4dcd52b6ed8d83f04ead04c0d5689fdeebe1bb7969fb11eafb9cb74c412c8049","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agentTools.availableError","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Could not load available tools for this session.","text_hash":"9b6a953e54f271fb698a93b197ec42eec5c2861af23e0f41a2ae37bdb4b0862d","tgt_lang":"uk","translated":"Не вдалося завантажити доступні інструменти для цього сеансу.","updated_at":"2026-07-12T06:47:38.942Z"}
|
||||
@@ -1063,6 +1064,7 @@
|
||||
{"cache_key":"945e0b243cec2884dfb31c513583914446ac8191a2c473ab2c8beab04e4bc4df","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agents.toolCatalog.profiles.minimal","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Minimal","text_hash":"057b5de48d7b90f123ec28d7e15f65d99b508d6b7cc2958c39472070f0f0f6bb","tgt_lang":"uk","translated":"Мінімальний","updated_at":"2026-07-12T06:45:33.014Z"}
|
||||
{"cache_key":"94a7d93d5cb99e0093782ea249e2bcc3c73572654265b4d9f1e82dd4ce9b6135","model":"claude-opus-4-8","provider":"anthropic","segment_id":"pluginsPage.connectorDescriptions.airtable","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Query and update records, tables, and bases in Airtable.","text_hash":"d50d210d4078f40825578718ef4891ae7c0a793081413c977803486e45bf3e05","tgt_lang":"uk","translated":"Запити та оновлення записів, таблиць і баз у Airtable.","updated_at":"2026-07-12T06:48:23.928Z"}
|
||||
{"cache_key":"94adc5add559fe7aef79b930f6baa31857ba5eb0f2704f0b9bfe2d747102ec05","model":"gpt-5","provider":"openai","segment_id":"claudeSessions.loadMore","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Load more","text_hash":"ac8991ef01019cf55a2426194a05959e0cb886333f1a332ff4f442320d165400","tgt_lang":"uk","translated":"Завантажити ще","updated_at":"2026-07-09T10:01:43.750Z"}
|
||||
{"cache_key":"94b75d5553e6a9d2cc8bafb1c3c2f7fdec63232198b298d270a8a360747de0ca","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"uk","translated":"Підключення до сеансу…","updated_at":"2026-07-15T00:45:31.680Z"}
|
||||
{"cache_key":"94c154ef5fb3405c144a2a648e6c5cab9740fec55fe29ddfb21b192f48b8643c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillStatus.blockedAgentFilter","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"blocked by agent filter","text_hash":"b16ca6740fd805900151d4e254d16f5375bb1b52d75117f2cb663ff0f194874d","tgt_lang":"uk","translated":"заблоковано фільтром агента","updated_at":"2026-07-12T06:48:07.313Z"}
|
||||
{"cache_key":"94c63a8726854da419d46b8724eec008bb02c89d5ff53dce92db40c7c4097cbf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.sections.browser.description","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Browser automation settings","text_hash":"afa2191d7389067d0f6f97e9e4b518059338732b2b74b7457485f04d59abb432","tgt_lang":"uk","translated":"Налаштування автоматизації браузера","updated_at":"2026-07-12T06:45:55.278Z"}
|
||||
{"cache_key":"94c6dba19016d3ebe9f319c4b224c98b661f36f8fe5f5f7a4a87d42f18ef8779","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.secretCountPlural","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} secrets","text_hash":"c2415948dbe8d1915fd8ebf02fc8fd83144375381f5fe47ad832d16ef71f91d9","tgt_lang":"uk","translated":"{count} секретів","updated_at":"2026-07-12T06:47:22.144Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:51.846Z",
|
||||
"generatedAt": "2026-07-15T00:45:42.329Z",
|
||||
"locale": "vi",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -179,6 +179,7 @@
|
||||
{"cache_key":"19f5ca4070cd41065c8df2595365b90afb111f55f46147588f94c235e2d72fd3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agentTools.availableNowSubtitle","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"What this agent can use in the current chat session.","text_hash":"3972b644e6c3212107fc9c119c2897c2c786ca08ee620880f321f92496a5e7d2","tgt_lang":"vi","translated":"Những gì agent này có thể dùng trong phiên chat hiện tại.","updated_at":"2026-07-12T06:54:29.578Z"}
|
||||
{"cache_key":"1a41f539c53d96bd8cce5ba7ed80ba516be0ca849d4be03dc6aa20b55d2aef08","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.sideChat.close","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Close side chat","text_hash":"2a1484f044f8f6a1108e2eaaf52849239fa7317678c770fc392907a9a45e6570","tgt_lang":"vi","translated":"Đóng cuộc trò chuyện phụ","updated_at":"2026-07-12T06:56:47.205Z"}
|
||||
{"cache_key":"1a51d38dfdd4de7b887800b71759d7e1e36cd30aa5085fab63e7f5d19d0457be","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.contextUsageApprox","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"~{percent}% of context used ({used} / {context} tokens, approximate)","text_hash":"54007077673048ad26ff220971d2598fde53a34c33da9e1eaca0927ce80b2708","tgt_lang":"vi","translated":"~{percent}% ngữ cảnh đã sử dụng ({used} / {context} token, xấp xỉ)","updated_at":"2026-07-09T07:40:49.631Z"}
|
||||
{"cache_key":"1a854aa352934a5ad162a57740938be7ce3096ec636b749448fc7f926a33f58d","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"vi","translated":"Không thể kết nối với phiên trong vòng 30 giây.","updated_at":"2026-07-15T00:45:42.329Z"}
|
||||
{"cache_key":"1aa299910e591536f311340fbff62b6283139690d7ee4ab20b06ffbdf22f56ef","model":"claude-opus-4-6","provider":"anthropic","segment_id":"configView.autoSaveSaving","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Saving…","text_hash":"23e39291d6135814ed7c936e278974544b0df5fbf0eb0427b6700979b7472a93","tgt_lang":"vi","translated":"Đang lưu…","updated_at":"2026-07-14T12:53:44.445Z"}
|
||||
{"cache_key":"1ad063c1f6fe4c790c5abcdfaf4b7fd0cb9a05c302fd55a13c482c7c7e8bd99a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.execApprovals.addPattern","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Add pattern","text_hash":"d57e0aac9bfb822d6e9d05908d0f813fa353ba71e49f22d7d497f8f660679a6d","tgt_lang":"vi","translated":"Thêm mẫu","updated_at":"2026-07-12T06:52:08.251Z"}
|
||||
{"cache_key":"1b10d910bc88440f0ad20de866f07465ff2ece293d6bc09cfa80d6c6c342b5f0","model":"claude-sonnet-4-6","provider":"anthropic","segment_id":"cron.suggestions.ideas.watchdog.name","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Night watch","text_hash":"438d99f2923b4b1a22e4538646acb1dae4ca587870ca0ec3cdf19f132d9fb876","tgt_lang":"vi","translated":"Canh đêm","updated_at":"2026-07-11T22:48:46.070Z"}
|
||||
@@ -1684,6 +1685,7 @@
|
||||
{"cache_key":"edd05826519470eeb55b7450fe76833349a31c44fc1bb2a784868eef282b1021","model":"gpt-5","provider":"openai","segment_id":"usage.daily.compressedScaleHint","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Square-root scale keeps low-usage days visible.","text_hash":"9515e7c6db149c32b64dba95a43e31a61d53dce8f11fe98683b234fb1cfd1920","tgt_lang":"vi","translated":"Thang căn bậc hai giúp các ngày có mức sử dụng thấp vẫn hiển thị rõ.","updated_at":"2026-07-05T20:24:32.108Z"}
|
||||
{"cache_key":"eddbf928c3f0ab0153354d78a785d0f40411ccd1aff9c0169d2f98324cf838e6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.notifications.title","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Push notifications","text_hash":"a1fa4443fe4abe63d6a29c433e4e8f23604c6bda89d6cec04bc261b5021f74e4","tgt_lang":"vi","translated":"Thông báo đẩy","updated_at":"2026-07-12T06:53:48.470Z"}
|
||||
{"cache_key":"ee1d18cc9612222c980abbf669e41904092a1f0f90b59de0577fc53d4da2f346","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.appearance.importTheme","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Import theme","text_hash":"f9cc6392852003dbe15afb017131be1e2fd9aa71fe8823956c6dfd221cf077ff","tgt_lang":"vi","translated":"Nhập chủ đề","updated_at":"2026-07-12T06:54:04.680Z"}
|
||||
{"cache_key":"ee49eab704ecb4f5fe6be9eb499ed40ab9f98f5065c0ec7d65034a4bff2b1d78","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"vi","translated":"Đang kết nối với phiên…","updated_at":"2026-07-15T00:45:42.329Z"}
|
||||
{"cache_key":"ee7841c4730faccf0adba786191c372996ce6bc5e52570e598b39d51d5dda0b7","model":"gpt-5","provider":"openai","segment_id":"terminal.refreshSessions","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Refresh","text_hash":"0e91610117029a62a478b7fa7df0b8598bebe3ab1e192d4b1882e310719c9671","tgt_lang":"vi","translated":"Làm mới","updated_at":"2026-07-09T10:01:43.734Z"}
|
||||
{"cache_key":"ee91b4a15e6f450a5e52f89ec7485bca7b9b4df649e4504c41d94b2de0bd713d","model":"gpt-5.6-sol","provider":"openai","segment_id":"agentScope.label","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Agent scope","text_hash":"0833bc070d2c153692bc25fcc0b30f774cb415efdbf95fe8cd132feab8053698","tgt_lang":"vi","translated":"Phạm vi agent","updated_at":"2026-07-13T11:01:31.660Z"}
|
||||
{"cache_key":"eec2b89fc96c11aea0362c45a5981fab5d5b24a69ee996450047641dc58209bf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.search","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Search settings","text_hash":"255b076dd82855431a4ce9a722d77d41f47755ab11be14fc686fd71415cdeef0","tgt_lang":"vi","translated":"Tìm kiếm cài đặt","updated_at":"2026-07-12T06:54:12.096Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.151Z",
|
||||
"generatedAt": "2026-07-15T00:44:56.393Z",
|
||||
"locale": "zh-CN",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -539,6 +539,7 @@
|
||||
{"cache_key":"4d79c047e70ddfe14896c9fdc902a16a7ce97193f636a6df266b9b73d6f45ff7","model":"gpt-5.6-sol","provider":"openai","segment_id":"skillsPage.enabledNamed","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{name} enabled","text_hash":"52997ef00b23bebf057110d3caba48add4cb6b458b252474b02d22e27074e03b","tgt_lang":"zh-CN","translated":"{name} 已启用","updated_at":"2026-07-13T13:03:49.381Z"}
|
||||
{"cache_key":"4d7abbb2ed890a4fb51fc24f29385216258f198af90add19677a1984f9f8db98","model":"gpt-5.6-sol","provider":"openai","segment_id":"lazyView.staleSubtitle","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"OpenClaw was updated in the background. Reload to get the latest panel.","text_hash":"059f63e57629dbb4ea5aa42c5598d95f2889169d76efefa813d0bb148e56648e","tgt_lang":"zh-CN","translated":"OpenClaw 已在后台更新。请重新加载以获取最新面板。","updated_at":"2026-07-13T05:01:09.010Z"}
|
||||
{"cache_key":"4d9cf225c85a5b41525cd887a911893c8140b6f9e46d854c995fa5b7ce9458dc","model":"gpt-5.5","provider":"openai","segment_id":"tasksPage.recentSub","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Latest completed, failed, and cancelled tasks.","text_hash":"44280ebc1ef9ff6ae709f96c5d262b1818e8c580d877fb7f9885344e102eba59","tgt_lang":"zh-CN","translated":"最近已完成、失败和已取消的任务。","updated_at":"2026-07-09T21:53:01.582Z"}
|
||||
{"cache_key":"4db3df0f1aeb870e48c814d0af8a69b99211475789ac38178a538560fe3c6a5b","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"zh-CN","translated":"正在连接到会话…","updated_at":"2026-07-15T00:44:56.393Z"}
|
||||
{"cache_key":"4db81d22b1656ea356a6a78cbf47e987809783db1c8176c66bcfc49ef2d8b98c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"zh-CN","translated":"Gateway 任务","updated_at":"2026-06-16T14:12:56.348Z"}
|
||||
{"cache_key":"4dfc1918c5ffd5151839f83a5c20dfae03b360d92592d71feea4f8d8218524a5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"dreaming.wiki.id","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Id:","text_hash":"68d036759794566ce5d3e8d118a575d6b481ceeb79b324e938a3e06614164d32","tgt_lang":"zh-CN","translated":"Id:","updated_at":"2026-07-12T06:29:01.870Z"}
|
||||
{"cache_key":"4e119d261994fb901bc39e184d5995599e011a80b0daf21151309234acff1cd0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.messages.deleteMessage","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Delete message","text_hash":"87e7176a7ce700af0c4afe34a922d1401eec9611ffc5a99bdbf032e8654c2e9b","tgt_lang":"zh-CN","translated":"删除消息","updated_at":"2026-07-12T06:29:11.505Z"}
|
||||
@@ -1751,6 +1752,7 @@
|
||||
{"cache_key":"fe8e79f0b42cbf50f099424b829d10e99869c3f28205191e9c19f355ffbd3694","model":"claude-opus-4-8","provider":"anthropic","segment_id":"channels.telegram.title","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Telegram","text_hash":"acdd1e734125f341604c0efbabdcc4c4b0597e8f6235d66c2445edd1812838c1","tgt_lang":"zh-CN","translated":"Telegram","updated_at":"2026-07-12T06:25:05.607Z"}
|
||||
{"cache_key":"fe9033afedace899e183c9ba33ee53312da244aaab70ce62a5d929592512c68c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.revision.description","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Tell the agent what should change. The proposal stays pending and the workshop will create a revised version.","text_hash":"c9eb5236c5b73f0eec0f11927862295a2259dff5b052b04f0b01d846b0c97b22","tgt_lang":"zh-CN","translated":"告诉智能体需要更改什么。提案保持待处理状态,工作坊将创建一个修订版本。","updated_at":"2026-07-12T06:28:22.526Z"}
|
||||
{"cache_key":"fe94db7417cccf382133f998704fb44c831bd8eca497cc04ab4b033e7a95ec6f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.binding.useDefault","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Use default","text_hash":"a769cedc799260e81ce23b3fc1533ae48004f6ab25f0dffcc783d95fea378e19","tgt_lang":"zh-CN","translated":"使用默认值","updated_at":"2026-07-12T06:25:12.243Z"}
|
||||
{"cache_key":"febc50ec52f311860fa1de9531c4f3d9fa703ec4d7ff26d816bb9e7a1d836b2d","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"zh-CN","translated":"未能在 30 秒内连接到会话。","updated_at":"2026-07-15T00:44:56.393Z"}
|
||||
{"cache_key":"fec08a3e695a91329016c91ad131e996fe049d42823aca666ed87e1cee0dd616","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.modelControls.default","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Default","text_hash":"21b111cbfe6e8fca2d181c43f53ad548b22e38aca955b9824706a504b0a07a2d","tgt_lang":"zh-CN","translated":"默认","updated_at":"2026-07-06T20:20:02.809Z"}
|
||||
{"cache_key":"ff22c687393d76fce445196c6bcd52e2ee1017b41441d1aae57cadca995c9ed9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillStatus.blockedAllowlist","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"blocked by allowlist","text_hash":"5ef37759cb09792554c00666d262cfe812ae35a592a69c2c7b705be8687bca4b","tgt_lang":"zh-CN","translated":"被允许列表阻止","updated_at":"2026-07-12T06:27:47.508Z"}
|
||||
{"cache_key":"ff2abac63994ff6a2fd30e34bb286c86c2fef934c23c3a6d39c3e5ce8f3f02d9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.messages.activity","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Activity","text_hash":"38da1505ca8373288489495101b14f24ac95078f520ad64df18272aa6054f750","tgt_lang":"zh-CN","translated":"活动","updated_at":"2026-07-12T06:29:11.505Z"}
|
||||
|
||||
Generated
+6
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-07-14T23:35:50.272Z",
|
||||
"generatedAt": "2026-07-15T00:45:03.008Z",
|
||||
"locale": "zh-TW",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "85e004373478d946c5002519a35b2c50e1aa220fd2a81f7c1c80ded99bc7dc4e",
|
||||
"totalKeys": 3353,
|
||||
"translatedKeys": 3353,
|
||||
"model": "gpt-5.6-sol",
|
||||
"provider": "openai",
|
||||
"sourceHash": "dc6d0f7bdb69960a5b9d54c5743445b89b0689913b3f962829f8490dede56d2c",
|
||||
"totalKeys": 3355,
|
||||
"translatedKeys": 3355,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+2
@@ -1010,6 +1010,7 @@
|
||||
{"cache_key":"93635a60e63e0db12fee2ef6ec969903a6d72978e64ff50b5255464a257af7cd","model":"gpt-5.6-sol","provider":"openai","segment_id":"chat.progressLabels.molting","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Molting","text_hash":"fbd2ae2ba1642ca5ffd2a92167bfc7aca3669ae73d6a592cede7ddae67bb55c3","tgt_lang":"zh-TW","translated":"蛻殼中","updated_at":"2026-07-14T04:52:54.602Z"}
|
||||
{"cache_key":"93bd1734efd4f15a634a9c2c98bb1556a01f5ef5f3c63cb7e2d20efa3b5c665d","model":"claude-opus-4-6","provider":"anthropic","segment_id":"configView.applyBannerAction","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Restart & apply","text_hash":"31982092d02b6e51f60cd993f5377f10ce2f245aabe107c2380150342e0374fe","tgt_lang":"zh-TW","translated":"重新啟動並套用","updated_at":"2026-07-14T12:52:18.608Z"}
|
||||
{"cache_key":"93c80b85b65138c8af8ae94caf33c3ab88652cc6823dc0b2d2b66e368ad346f9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.detail.edited","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Edited {time}","text_hash":"51c9630775198996ab6bbb54de3215bb774c09198d49201b023b1119ddefa1ed","tgt_lang":"zh-TW","translated":"已編輯 {time}","updated_at":"2026-07-12T06:29:14.670Z"}
|
||||
{"cache_key":"93d6d39ad5b7cf6685f176435ba75590365873a97486576680dfe8cabd5fac7d","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connectionTimedOut","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Session did not connect within 30 seconds.","text_hash":"38c85973d7e3a6e0d95a423366c67d737a8857eeeb09954ed1e31bf47a0f4b9e","tgt_lang":"zh-TW","translated":"工作階段未能在 30 秒內連線。","updated_at":"2026-07-15T00:45:03.008Z"}
|
||||
{"cache_key":"94199e4653bb1f7a74390f940cb818546854b12fd00b1d56a63bd45a9ab6d2ec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.dismissWarning","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Don't remind again","text_hash":"f7f8a139c18f0c904c95a5aad9adc39b99d3e115b671dfc09c44f2d6970d05dc","tgt_lang":"zh-TW","translated":"不再提醒","updated_at":"2026-07-12T06:27:55.841Z"}
|
||||
{"cache_key":"9453b56e509d26313c25cf74cec0e9bcbbbc9c0163f9e933e9eb0d382d81530e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"zh-TW","translated":"相依項目","updated_at":"2026-06-16T14:13:10.175Z"}
|
||||
{"cache_key":"946f3c34437ac039723452352b26a4d2f001fbcd5e758c29275ea53c9dba74ef","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.form.failureAlertAfterHelp","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Consecutive errors before alerting.","text_hash":"cfab25f9233c6418534dc02f551be9afb62045e6c956ddb44340ce131ede1614","tgt_lang":"zh-TW","translated":"警示前的連續錯誤次數。","updated_at":"2026-07-12T06:30:38.985Z"}
|
||||
@@ -1637,6 +1638,7 @@
|
||||
{"cache_key":"e6fb37f69ea765d94a29762a32191317701dfbbe37f1967743c995c89a3d5f76","model":"gpt-5.6-sol","provider":"openai","segment_id":"configForm.sections.plugins.label","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Plugins","text_hash":"9514b7ff4860ead73491768e45cce0ce20e6e3473a7b272e496c43c875d80ac5","tgt_lang":"zh-TW","translated":"外掛程式","updated_at":"2026-07-12T00:07:58.605Z"}
|
||||
{"cache_key":"e6fb55b186b44041e533a3b7dcd2351664fbd7275e0181262e8f8fa7d439289d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"agents.toolCatalog.descriptions.browser","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Control web browser","text_hash":"4c99358b099daf6172352f111c30fc6e71b8ba519bbeab6d45107a902179512f","tgt_lang":"zh-TW","translated":"控制網頁瀏覽器","updated_at":"2026-07-12T06:26:21.254Z"}
|
||||
{"cache_key":"e7586e1763a46aa0c60aba4d2041d7085a725af507ebefd3bc4cbf507a669c24","model":"claude-opus-4-6","provider":"anthropic","segment_id":"chat.composer.realtimeTalkRequiresMicrophone","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Realtime voice input requires browser microphone access.","text_hash":"a70d86265802e30aac70647cde33c62c5386741941d2ea9f54636a0242109dcd","tgt_lang":"zh-TW","translated":"即時語音輸入需要瀏覽器麥克風存取權限。","updated_at":"2026-07-06T22:41:51.388Z"}
|
||||
{"cache_key":"e764e316ef136b4f93cfa22761ec379f44d716b9308edfd37a0391ccf17ed221","model":"gpt-5.6-sol","provider":"openai","segment_id":"terminal.connecting","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Connecting to session…","text_hash":"f1f49121a17a09b93f1601768c015f6fa9ef2056365c5a745837a2be98f71874","tgt_lang":"zh-TW","translated":"正在連線至工作階段…","updated_at":"2026-07-15T00:45:03.008Z"}
|
||||
{"cache_key":"e771be91505216368710f3f2007039e854a0c6f0d9d753cc4b2a8b8adbd78992","model":"claude-opus-4-8","provider":"anthropic","segment_id":"quickSettings.personal.clearAvatar","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Clear avatar","text_hash":"13bf7b9f89587bcbc16db609df7343b1fbb40330635edb276881c1ce235fbcb8","tgt_lang":"zh-TW","translated":"清除頭像","updated_at":"2026-07-12T06:27:14.052Z"}
|
||||
{"cache_key":"e7e2cebfcb556a07858fd9ff39d0c64b4ff0c3828f3a320ac488b0530c6c8ba2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configView.sections.canvasHost","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Canvas Host","text_hash":"53e6ca49326ab836d65a66fb09ce46caa85dfe68616be5776cb8d3096bda2e92","tgt_lang":"zh-TW","translated":"Canvas Host","updated_at":"2026-07-12T06:27:29.739Z"}
|
||||
{"cache_key":"e7eac3f0b14c54c128dc7e57f622844250263b814fb5eddcd7f030f34a3177d2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"configForm.schemaUnavailable","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Schema unavailable.","text_hash":"5ff36b82d87f7973ac44f95373bbcbf058d3d24977fbbe31b3c594ce8c231ea8","tgt_lang":"zh-TW","translated":"無法使用結構描述。","updated_at":"2026-07-12T06:26:35.708Z"}
|
||||
|
||||
Generated
+2
@@ -1602,6 +1602,8 @@ export const ar: TranslationMap = {
|
||||
attachSession: "الاتصال بالجلسة",
|
||||
attachFailed: "تعذّر الاتصال بجلسة الطرفية",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "جارٍ الاتصال بالجلسة…",
|
||||
connectionTimedOut: "تعذّر الاتصال بالجلسة خلال 30 ثانية.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+3
@@ -1624,6 +1624,9 @@ export const de: TranslationMap = {
|
||||
attachSession: "Sitzung verbinden",
|
||||
attachFailed: "Terminalsitzung konnte nicht verbunden werden",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Verbindung zur Sitzung wird hergestellt…",
|
||||
connectionTimedOut:
|
||||
"Die Verbindung zur Sitzung wurde nicht innerhalb von 30 Sekunden hergestellt.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
@@ -1575,6 +1575,8 @@ export const en: TranslationMap = {
|
||||
attachSession: "Attach session",
|
||||
attachFailed: "Could not attach terminal session",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Connecting to session…",
|
||||
connectionTimedOut: "Session did not connect within 30 seconds.",
|
||||
tabLabel: "shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1621,6 +1621,8 @@ export const es: TranslationMap = {
|
||||
attachSession: "Conectar sesión",
|
||||
attachFailed: "No se pudo conectar la sesión de terminal",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Conectando con la sesión…",
|
||||
connectionTimedOut: "La sesión no se conectó en un plazo de 30 segundos.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1611,6 +1611,8 @@ export const fa: TranslationMap = {
|
||||
attachSession: "اتصال به نشست",
|
||||
attachFailed: "اتصال به نشست ترمینال ممکن نشد",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "در حال اتصال به نشست…",
|
||||
connectionTimedOut: "اتصال به نشست ظرف ۳۰ ثانیه برقرار نشد.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1631,6 +1631,8 @@ export const fr: TranslationMap = {
|
||||
attachSession: "Se connecter à la session",
|
||||
attachFailed: "Impossible de se connecter à la session de terminal",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Connexion à la session…",
|
||||
connectionTimedOut: "La connexion à la session n’a pas été établie dans les 30 secondes.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1602,6 +1602,8 @@ export const hi: TranslationMap = {
|
||||
attachSession: "सत्र संलग्न करें",
|
||||
attachFailed: "टर्मिनल सत्र संलग्न नहीं किया जा सका",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "सत्र से कनेक्ट किया जा रहा है…",
|
||||
connectionTimedOut: "सत्र 30 सेकंड के भीतर कनेक्ट नहीं हुआ।",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1612,6 +1612,8 @@ export const id: TranslationMap = {
|
||||
attachSession: "Hubungkan sesi",
|
||||
attachFailed: "Tidak dapat menghubungkan sesi terminal",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Menghubungkan ke sesi…",
|
||||
connectionTimedOut: "Sesi tidak terhubung dalam waktu 30 detik.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1627,6 +1627,8 @@ export const it: TranslationMap = {
|
||||
attachSession: "Collega sessione",
|
||||
attachFailed: "Impossibile collegare la sessione del terminale",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Connessione alla sessione…",
|
||||
connectionTimedOut: "La sessione non si è connessa entro 30 secondi.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1620,6 +1620,8 @@ export const ja_JP: TranslationMap = {
|
||||
attachSession: "セッションに接続",
|
||||
attachFailed: "ターミナルセッションに接続できませんでした",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "セッションに接続しています…",
|
||||
connectionTimedOut: "30秒以内にセッションに接続できませんでした。",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1606,6 +1606,8 @@ export const ko: TranslationMap = {
|
||||
attachSession: "세션 연결",
|
||||
attachFailed: "터미널 세션을 연결할 수 없습니다",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "세션에 연결하는 중…",
|
||||
connectionTimedOut: "30초 이내에 세션에 연결하지 못했습니다.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1620,6 +1620,8 @@ export const nl: TranslationMap = {
|
||||
attachSession: "Sessie koppelen",
|
||||
attachFailed: "Kan terminalsessie niet koppelen",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Verbinding maken met sessie…",
|
||||
connectionTimedOut: "Er kon niet binnen 30 seconden verbinding worden gemaakt met de sessie.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1620,6 +1620,8 @@ export const pl: TranslationMap = {
|
||||
attachSession: "Podłącz sesję",
|
||||
attachFailed: "Nie udało się podłączyć sesji terminala",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Łączenie z sesją…",
|
||||
connectionTimedOut: "Nie udało się połączyć z sesją w ciągu 30 sekund.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1615,6 +1615,8 @@ export const pt_BR: TranslationMap = {
|
||||
attachSession: "Conectar à sessão",
|
||||
attachFailed: "Não foi possível conectar à sessão de terminal",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Conectando à sessão…",
|
||||
connectionTimedOut: "A sessão não foi conectada em até 30 segundos.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1623,6 +1623,8 @@ export const ru: TranslationMap = {
|
||||
attachSession: "Подключиться к сеансу",
|
||||
attachFailed: "Не удалось подключиться к сеансу терминала",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Подключение к сеансу…",
|
||||
connectionTimedOut: "Не удалось подключиться к сеансу в течение 30 секунд.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1593,6 +1593,8 @@ export const th: TranslationMap = {
|
||||
attachSession: "เชื่อมต่อเซสชัน",
|
||||
attachFailed: "ไม่สามารถเชื่อมต่อเซสชันเทอร์มินัลได้",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "กำลังเชื่อมต่อกับเซสชัน…",
|
||||
connectionTimedOut: "ไม่สามารถเชื่อมต่อเซสชันได้ภายใน 30 วินาที",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1619,6 +1619,8 @@ export const tr: TranslationMap = {
|
||||
attachSession: "Oturuma bağlan",
|
||||
attachFailed: "Terminal oturumuna bağlanılamadı",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Oturuma bağlanılıyor…",
|
||||
connectionTimedOut: "Oturum bağlantısı 30 saniye içinde kurulamadı.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1619,6 +1619,8 @@ export const uk: TranslationMap = {
|
||||
attachSession: "Підключитися до сеансу",
|
||||
attachFailed: "Не вдалося підключитися до сеансу термінала",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Підключення до сеансу…",
|
||||
connectionTimedOut: "Не вдалося підключитися до сеансу протягом 30 секунд.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1613,6 +1613,8 @@ export const vi: TranslationMap = {
|
||||
attachSession: "Kết nối phiên",
|
||||
attachFailed: "Không thể kết nối phiên terminal",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "Đang kết nối với phiên…",
|
||||
connectionTimedOut: "Không thể kết nối với phiên trong vòng 30 giây.",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1583,6 +1583,8 @@ export const zh_CN: TranslationMap = {
|
||||
attachSession: "连接会话",
|
||||
attachFailed: "无法连接终端会话",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "正在连接到会话…",
|
||||
connectionTimedOut: "未能在 30 秒内连接到会话。",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Generated
+2
@@ -1584,6 +1584,8 @@ export const zh_TW: TranslationMap = {
|
||||
attachSession: "連接工作階段",
|
||||
attachFailed: "無法連接終端機工作階段",
|
||||
starting: "Starting terminal…",
|
||||
connecting: "正在連線至工作階段…",
|
||||
connectionTimedOut: "工作階段未能在 30 秒內連線。",
|
||||
tabLabel: "Shell {n}",
|
||||
tabHint: "{agent} · {cwd}",
|
||||
exited: "exited",
|
||||
|
||||
Reference in New Issue
Block a user