diff --git a/CHANGELOG.md b/CHANGELOG.md index 6207a8ad629..6685dc1dda1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Changes - Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar. +- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev. - Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure. - Control UI/performance: record browser long animation frame or long task entries in the debug event log when supported, making slow dashboard renders easier to attribute from the UI. - Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams. @@ -58,10 +59,21 @@ Docs: https://docs.openclaw.ai - Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi. - Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90. - Plugins/ClawHub: annotate 429 errors from ClawHub with the reset window from `RateLimit-Reset`/`Retry-After` and append a `Sign in for higher rate limits.` hint when the request was unauthenticated, so users can see when downloads will recover and how to lift the cap. Thanks @romneyda. +- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight. ### Fixes +- Plugins/runtime state: keep the key being registered when namespace eviction runs in the same millisecond as existing entries, so `register` and `registerIfAbsent` do not report success while evicting their own fresh value. Thanks @vincentkoc. - Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis. +- Control UI/Talk: stop and clear failed realtime Talk sessions when dismissing runtime error banners, so the next Talk click starts a fresh session instead of only stopping the stale one. Thanks @vincentkoc. +- Control UI/Talk: retry from a failed realtime Talk session on the next Talk click instead of requiring a separate stale-session stop click first. Thanks @vincentkoc. +- Canvas host: preserve the Gateway TLS scheme in browser canvas host URLs and startup mount logs, so direct HTTPS gateways do not advertise insecure canvas links. Thanks @vincentkoc. +- WhatsApp/login: route login success and failure messages through the injected runtime, so setup/onboarding surfaces capture all login output instead of only the QR. Thanks @vincentkoc. +- Google Chat: create an isolated Google auth transport per auth client, so google-auth-library interceptor mutations do not accumulate across webhook verification and access-token clients. Thanks @vincentkoc. +- Doctor/plugins: remove orphaned managed npm copies of bundled `@openclaw/*` plugins during `doctor --fix`, so stale package manifests cannot shadow the current bundled plugin config schema. +- Control UI/performance: cap long-task and long-animation-frame diagnostics in the shared event log, so slow-render telemetry does not evict gateway/plugin events from the Debug and Overview views. Thanks @vincentkoc. +- Gateway/startup: log the canvas host mount only after the HTTP server has bound, so startup logs no longer report the canvas host as mounted before it can serve requests. +- Control UI/i18n: render the Sessions active filter tooltip with the configured minute count in every locale and make the i18n check reject placeholder drift. Thanks @BunsDev. - Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc. - Discord: clear stale startup probe bot/application status when the async bot probe throws, not just when it returns a degraded probe result. Thanks @vincentkoc. - Web search: scope explicit bundled `web_search` provider runtime loading through manifest ownership, so selecting DuckDuckGo/Gemini/etc. does not import unrelated bundled providers or log their optional dependency failures. Thanks @vincentkoc. @@ -72,6 +84,7 @@ Docs: https://docs.openclaw.ai - Media/images: keep HEIC/HEIF attachments fail-closed when optional Sharp conversion is unavailable instead of sending originals that still need conversion. Thanks @vincentkoc. - Google Meet: fork the caller's current agent transcript into agent-mode meeting consultant sessions, so Meet replies inherit the context from the tool call that joined the meeting. - iOS/mobile pairing: reject non-loopback `ws://` setup URLs before QR/setup-code issuance and let the iOS Gateway settings screen scan QR codes or paste full setup-code messages. Thanks @BunsDev. +- Control UI: keep Gateway Access inputs and locale picker contained inside the card at narrow and tablet widths. - Telegram/streaming: sanitize tool-progress draft preview backticks before shared compaction, so long backtick-heavy progress text still renders inside the safe code-formatted preview instead of collapsing to an ellipsis. - UI/chat: remove the unsupported `line-clamp` declaration from the chat queue text rule to eliminate Firefox console noise without changing visible truncation behavior. Thanks @ZanderH-code. - Agents/Pi: suppress persistence for synthetic mid-turn overflow continuation prompts, so transcript-retry recovery does not write the "continue from transcript" prompt as a new user turn. Thanks @vincentkoc. @@ -207,6 +220,8 @@ Docs: https://docs.openclaw.ai - Feishu: accept and honor `channels.feishu.blockStreaming` at the top level and per account, while keeping the legacy default off so Feishu cards no longer reject documented config or silently drop block replies. Fixes #75555. Thanks @vincentkoc. - Gateway/update: avoid `launchctl kickstart -k` immediately after fresh macOS update bootstraps, and unlink dangling global plugin-runtime symlinks during packaged postinstall and `doctor --fix` so upgrades no longer SIGTERM the newly booted Gateway or leave bundled plugin imports pointed at pruned `plugin-runtime-deps` trees. Completes #76261 and fixes #76466. (#76929) - Google Chat: normalize custom Google auth transport headers before google-auth/gaxios interceptors run, restoring webhook token verification when certificate retrieval expects Fetch `Headers`. Fixes #76742. Thanks @donbowman. +- Google Chat: normalize Google auth certificate response headers before google-auth-library reads cache-control, so inbound webhook auth no longer rejects with `res?.headers.get is not a function`. Fixes #76880. Thanks @donbowman. +- WhatsApp: route terminal login QR output through the active runtime for initial and restart sockets, so `openclaw channels login --channel whatsapp` does not lose the QR behind direct stdout writes. Fixes #76213. Thanks @dougvk. - Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc. - Docs/WhatsApp: merge the duplicate top-level `web` objects in the gateway channel config example so copy-pasted WhatsApp config keeps both `web.whatsapp` and reconnect settings. Fixes #76619. Thanks @WadydX. - Plugins/Anthropic: expose Claude thinking profiles from the bundled provider-policy artifact so non-runtime callers keep Opus 4.7 `adaptive`, `xhigh`, and `max` instead of downgrading to `high`. Fixes #76779. Thanks @tomascupr and @iAbhi001. diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index 878caf05584..6477a06a03b 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -387,6 +387,8 @@ The local plugin registry is OpenClaw's persisted cold read model for installed Use `plugins registry` to inspect whether the persisted registry is present, current, or stale. Use `--refresh` to rebuild it from the persisted plugin index, config policy, and manifest/package metadata. This is a repair path, not a runtime activation path. +`openclaw doctor --fix` also repairs registry-adjacent managed npm drift: if an orphaned `@openclaw/*` package under the managed plugin npm root shadows a bundled plugin, doctor removes that stale package and rebuilds the registry so startup validates against the bundled manifest. + `OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY=1` is a deprecated break-glass compatibility switch for registry read failures. Prefer `plugins registry --refresh` or `openclaw doctor --fix`; the env fallback is only for emergency startup recovery while the migration rolls out. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 761c54efd75..07c8d816c7e 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -344,7 +344,7 @@ That stages grounded durable candidates into the short-term dreaming store while When sandboxing is enabled, doctor checks Docker images and offers to build or switch to legacy names if the current image is missing. - Doctor removes legacy OpenClaw-generated plugin dependency staging state in `openclaw doctor --fix` / `openclaw doctor --repair` mode. This covers stale generated dependency roots, old install-stage directories, and package-local debris from earlier bundled-plugin dependency repair code. + Doctor removes legacy OpenClaw-generated plugin dependency staging state in `openclaw doctor --fix` / `openclaw doctor --repair` mode. This covers stale generated dependency roots, old install-stage directories, package-local debris from earlier bundled-plugin dependency repair code, and orphaned managed npm copies of bundled `@openclaw/*` plugins that can shadow the current bundled manifest. Doctor can also reinstall configured downloadable plugins when the config references them but the local plugin registry cannot find them. For the 2026.5.2 bundled-plugin externalization, doctor automatically installs downloadable plugins that the existing config already uses and then relies on `meta.lastTouchedVersion` to run that release pass only once. Gateway startup and config reload do not run package managers; plugin installs remain explicit doctor/install/update work. diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index e0dee0e8824..bd4835df097 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -417,12 +417,13 @@ Provider and channel execution paths must use the active runtime config snapshot }); await store.register("key-1", { value: "hello" }); + const claimed = await store.registerIfAbsent("dedupe-key", { value: "first" }); const value = await store.lookup("key-1"); await store.consume("key-1"); await store.clear(); ``` - Keyed stores survive restarts and are isolated by the runtime-bound plugin id. Limits: `maxEntries` per namespace, 1,000 live rows per plugin, JSON values under 64KB, and optional TTL expiry. + Keyed stores survive restarts and are isolated by the runtime-bound plugin id. Use `registerIfAbsent(...)` for atomic dedupe claims: it returns `true` when the key was missing or expired and registered, or `false` when a live value already exists without overwriting its value, creation time, or TTL. Limits: `maxEntries` per namespace, 1,000 live rows per plugin, JSON values under 64KB, and optional TTL expiry. Bundled plugins only in this release. diff --git a/extensions/googlechat/src/google-auth.runtime.test.ts b/extensions/googlechat/src/google-auth.runtime.test.ts index cb072ce96f7..f7ef267292b 100644 --- a/extensions/googlechat/src/google-auth.runtime.test.ts +++ b/extensions/googlechat/src/google-auth.runtime.test.ts @@ -348,6 +348,9 @@ describe("googlechat google auth runtime", () => { expect(transport.interceptors.request.add).toHaveBeenCalledWith({ resolved: expect.any(Function), }); + expect(transport.interceptors.response.add).toHaveBeenCalledWith({ + resolved: expect.any(Function), + }); expect("window" in globalThis).toBe(false); } finally { if (originalWindowDescriptor) { @@ -356,6 +359,18 @@ describe("googlechat google auth runtime", () => { } }); + it("keeps auth transports isolated from google-auth interceptor mutations", async () => { + const first = await getGoogleAuthTransport(); + const second = await getGoogleAuthTransport(); + + expect(first).not.toBe(second); + expect(mocks.gaxiosCtor).toHaveBeenCalledTimes(2); + expect(first.interceptors.request.add).toHaveBeenCalledOnce(); + expect(first.interceptors.response.add).toHaveBeenCalledOnce(); + expect(second.interceptors.request.add).toHaveBeenCalledOnce(); + expect(second.interceptors.response.add).toHaveBeenCalledOnce(); + }); + it("normalizes Google auth request headers before upstream interceptors run", async () => { const config = { headers: { "x-test": "1" }, @@ -369,6 +384,20 @@ describe("googlechat google auth runtime", () => { expect(normalized.headers.get("x-test")).toBe("1"); }); + it("normalizes Google auth response headers before upstream cache-control reads", () => { + const response = { + data: {}, + headers: { + "cache-control": "public, max-age=3600", + }, + }; + + const normalized = __testing.normalizeGoogleAuthResponseHeaders(response); + + expect(normalized.headers).toBeInstanceOf(Headers); + expect(normalized.headers.get("cache-control")).toBe("public, max-age=3600"); + }); + it("rejects service-account credentials that override Google auth endpoints", async () => { await expect( resolveValidatedGoogleChatCredentials({ diff --git a/extensions/googlechat/src/google-auth.runtime.ts b/extensions/googlechat/src/google-auth.runtime.ts index d7c621442c8..ca05810f762 100644 --- a/extensions/googlechat/src/google-auth.runtime.ts +++ b/extensions/googlechat/src/google-auth.runtime.ts @@ -23,6 +23,9 @@ type GoogleAuthTransport = InstanceType; type GoogleAuthRequestWithUnknownHeaders = RequestInit & { headers?: unknown; }; +type GoogleAuthResponseWithUnknownHeaders = { + headers?: unknown; +}; type GuardedGoogleAuthRequestInit = RequestInit & { agent?: unknown; cert?: unknown; @@ -68,7 +71,6 @@ const MAX_GOOGLE_AUTH_RESPONSE_BYTES = 1024 * 1024; const MAX_GOOGLE_CHAT_SERVICE_ACCOUNT_FILE_BYTES = 64 * 1024; let googleAuthRuntimePromise: Promise | null = null; -let googleAuthTransportPromise: Promise | null = null; function normalizeGoogleAuthPreparedRequestHeaders( config: T, @@ -79,12 +81,24 @@ function normalizeGoogleAuthPreparedRequestHeaders( + response: T, +): T & { headers: Headers } { + if (!(response.headers instanceof Headers)) { + response.headers = new Headers(response.headers as HeadersInit | undefined); + } + return response as T & { headers: Headers }; +} + function installGoogleAuthHeaderCompatibilityInterceptor( transport: GoogleAuthTransport, ): GoogleAuthTransport { transport.interceptors.request.add({ resolved: async (config) => normalizeGoogleAuthPreparedRequestHeaders(config), }); + transport.interceptors.response.add({ + resolved: async (response) => normalizeGoogleAuthResponseHeaders(response), + }); return transport; } @@ -521,22 +535,12 @@ export async function loadGoogleAuthRuntime(): Promise { } export async function getGoogleAuthTransport(): Promise { - if (!googleAuthTransportPromise) { - googleAuthTransportPromise = (async () => { - try { - const { Gaxios } = await loadGoogleAuthRuntime(); - return installGoogleAuthHeaderCompatibilityInterceptor( - new Gaxios({ - fetchImplementation: createGoogleAuthFetch(), - }), - ); - } catch (error) { - googleAuthTransportPromise = null; - throw error; - } - })(); - } - return await googleAuthTransportPromise; + const { Gaxios } = await loadGoogleAuthRuntime(); + return installGoogleAuthHeaderCompatibilityInterceptor( + new Gaxios({ + fetchImplementation: createGoogleAuthFetch(), + }), + ); } export async function resolveValidatedGoogleChatCredentials( @@ -555,9 +559,9 @@ export async function resolveValidatedGoogleChatCredentials( export const __testing = { resetGoogleAuthRuntimeForTests(): void { googleAuthRuntimePromise = null; - googleAuthTransportPromise = null; }, normalizeGoogleAuthPreparedRequestHeaders, + normalizeGoogleAuthResponseHeaders, resolveGoogleAuthEnvProxyUrl, validateGoogleChatServiceAccountCredentials, }; diff --git a/extensions/whatsapp/src/login.coverage.test.ts b/extensions/whatsapp/src/login.coverage.test.ts index 9c0e1b9b43b..f845da9b7df 100644 --- a/extensions/whatsapp/src/login.coverage.test.ts +++ b/extensions/whatsapp/src/login.coverage.test.ts @@ -1,7 +1,9 @@ import { rmSync } from "node:fs"; import fs from "node:fs/promises"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { loginWeb } from "./login.js"; +import { renderQrTerminal } from "./qr-terminal.js"; import { createWaSocket, formatError, waitForWaConnection } from "./session.js"; const rmMock = vi.spyOn(fs, "rm"); @@ -63,9 +65,14 @@ vi.mock("./session.js", async () => { }; }); +vi.mock("./qr-terminal.js", () => ({ + renderQrTerminal: vi.fn(async (qr: string) => `terminal:${qr}\n`), +})); + const createWaSocketMock = vi.mocked(createWaSocket); const waitForWaConnectionMock = vi.mocked(waitForWaConnection); const formatErrorMock = vi.mocked(formatError); +const renderQrTerminalMock = vi.mocked(renderQrTerminal); async function flushTasks() { await Promise.resolve(); @@ -94,7 +101,7 @@ describe("loginWeb coverage", () => { .mockRejectedValueOnce({ error: { output: { statusCode: 515 } } }) .mockResolvedValueOnce(undefined); - const runtime = { log: vi.fn(), error: vi.fn() } as never; + const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; const pendingLogin = loginWeb(false, waitForWaConnectionMock as never, runtime); await flushTasks(); @@ -104,19 +111,53 @@ describe("loginWeb coverage", () => { expect(createWaSocketMock).toHaveBeenCalledTimes(2); const firstSock = await createWaSocketMock.mock.results[0]?.value; expect(firstSock.ws.close).toHaveBeenCalled(); + expect(runtime.log).toHaveBeenCalledWith( + expect.stringContaining("Linked after restart; web session ready."), + ); vi.runAllTimers(); const secondSock = await createWaSocketMock.mock.results[1]?.value; expect(secondSock.ws.close).toHaveBeenCalled(); }); + it("routes QR output through runtime for initial and restart sockets", async () => { + waitForWaConnectionMock + .mockRejectedValueOnce({ error: { output: { statusCode: 515 } } }) + .mockResolvedValueOnce(undefined); + + const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; + await loginWeb(false, waitForWaConnectionMock as never, runtime); + + expect(createWaSocketMock).toHaveBeenCalledTimes(2); + expect(createWaSocketMock.mock.calls[0]?.[0]).toBe(false); + const initialOpts = createWaSocketMock.mock.calls[0]?.[2] as + | { onQr?: (qr: string) => void } + | undefined; + const restartOpts = createWaSocketMock.mock.calls[1]?.[2] as + | { onQr?: (qr: string) => void } + | undefined; + expect(initialOpts?.onQr).toBe(restartOpts?.onQr); + + initialOpts?.onQr?.("initial-qr"); + restartOpts?.onQr?.("restart-qr"); + await flushTasks(); + + expect(runtime.log).toHaveBeenCalledWith("Scan this QR in WhatsApp (Linked Devices):"); + expect(runtime.log).toHaveBeenCalledWith("terminal:initial-qr"); + expect(runtime.log).toHaveBeenCalledWith("terminal:restart-qr"); + expect(renderQrTerminalMock).toHaveBeenCalledWith("initial-qr", { small: true }); + expect(renderQrTerminalMock).toHaveBeenCalledWith("restart-qr", { small: true }); + }); + it("clears creds and throws when logged out", async () => { waitForWaConnectionMock.mockRejectedValueOnce({ output: { statusCode: 401 }, }); - await expect(loginWeb(false, waitForWaConnectionMock as never)).rejects.toThrow( + const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; + await expect(loginWeb(false, waitForWaConnectionMock as never, runtime)).rejects.toThrow( /cache cleared/i, ); + expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("session is logged out")); expect(rmMock).toHaveBeenCalledWith(testState.authDir, { recursive: true, force: true, @@ -125,9 +166,13 @@ describe("loginWeb coverage", () => { it("formats and rethrows generic errors", async () => { waitForWaConnectionMock.mockRejectedValueOnce(new Error("boom")); - await expect(loginWeb(false, waitForWaConnectionMock as never)).rejects.toThrow( + const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; + await expect(loginWeb(false, waitForWaConnectionMock as never, runtime)).rejects.toThrow( "formatted:Error: boom", ); + expect(runtime.error).toHaveBeenCalledWith( + expect.stringContaining("WhatsApp Web connection ended before fully opening."), + ); expect(formatErrorMock).toHaveBeenCalled(); }); }); diff --git a/extensions/whatsapp/src/login.ts b/extensions/whatsapp/src/login.ts index 5c7e8da61b3..014ec77d263 100644 --- a/extensions/whatsapp/src/login.ts +++ b/extensions/whatsapp/src/login.ts @@ -6,6 +6,7 @@ import { logInfo } from "openclaw/plugin-sdk/text-runtime"; import { resolveWhatsAppAccount } from "./accounts.js"; import { restoreCredsFromBackupIfNeeded } from "./auth-store.js"; import { closeWaSocketSoon, waitForWhatsAppLoginResult } from "./connection-controller.js"; +import { renderQrTerminal } from "./qr-terminal.js"; import { createWaSocket, waitForWaConnection } from "./session.js"; import { resolveWhatsAppSocketTiming } from "./socket-timing.js"; @@ -19,9 +20,20 @@ export async function loginWeb( const account = resolveWhatsAppAccount({ cfg, accountId }); const socketTiming = resolveWhatsAppSocketTiming(cfg); const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir); - let sock = await createWaSocket(true, verbose, { + const onQr = (qr: string) => { + runtime.log("Scan this QR in WhatsApp (Linked Devices):"); + void renderQrTerminal(qr, { small: true }) + .then((output) => { + runtime.log(output.endsWith("\n") ? output.slice(0, -1) : output); + }) + .catch((err) => { + runtime.error(`failed rendering WhatsApp QR: ${String(err)}`); + }); + }; + let sock = await createWaSocket(false, verbose, { authDir: account.authDir, ...socketTiming, + onQr, }); logInfo("Waiting for WhatsApp connection...", runtime); try { @@ -33,12 +45,13 @@ export async function loginWeb( runtime, waitForConnection, socketTiming, + onQr, onSocketReplaced: (replacementSock) => { sock = replacementSock; }, }); if (result.outcome === "connected") { - console.log( + runtime.log( success( result.restarted ? "✅ Linked after restart; web session ready." @@ -51,7 +64,7 @@ export async function loginWeb( } if (result.outcome === "logged-out") { - console.error( + runtime.error( danger( `WhatsApp reported the session is logged out. Cleared cached web session; please rerun ${formatCliCommand("openclaw channels login")} and scan the QR again.`, ), @@ -61,7 +74,7 @@ export async function loginWeb( }); } - console.error(danger(`WhatsApp Web connection ended before fully opening. ${result.message}`)); + runtime.error(danger(`WhatsApp Web connection ended before fully opening. ${result.message}`)); throw new Error(result.message, { cause: result.error }); } finally { // Let Baileys flush any final events before closing the socket. diff --git a/scripts/check-docker-e2e-boundaries.mjs b/scripts/check-docker-e2e-boundaries.mjs index 84a36640d1e..b20ffec0e22 100644 --- a/scripts/check-docker-e2e-boundaries.mjs +++ b/scripts/check-docker-e2e-boundaries.mjs @@ -12,9 +12,9 @@ const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".." const errors = []; const packageJson = JSON.parse(readText("package.json")); const packageScripts = new Set(Object.keys(packageJson.scripts ?? {})); -// This lane proves the published Codex npm plugin against live OpenAI auth, so -// it intentionally needs both live credentials and the package-backed image. -const livePackageBackedLanes = new Set(["live-codex-npm-plugin"]); +// These lanes prove package-installed surfaces against live auth, so they +// intentionally need both live credentials and a package-backed image. +const livePackageBackedLanes = new Set(["live-codex-npm-plugin", "openwebui"]); function readText(relativePath) { return fs.readFileSync(path.join(ROOT_DIR, relativePath), "utf8"); diff --git a/scripts/control-ui-i18n.ts b/scripts/control-ui-i18n.ts index 34c0474b0d2..e401251c3c4 100644 --- a/scripts/control-ui-i18n.ts +++ b/scripts/control-ui-i18n.ts @@ -1,4 +1,4 @@ -import { spawn } from "node:child_process"; +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; @@ -13,6 +13,8 @@ interface TranslationMap { [key: string]: string | TranslationMap; } +type TranslationValue = string | { [key: string]: TranslationValue }; + type LocaleEntry = { exportName: string; fileName: string; @@ -357,6 +359,68 @@ function compareStringArrays(left: string[], right: string[]) { return left.every((value, index) => value === right[index]); } +export type PlaceholderMismatch = { + key: string; + locale: string; + sourcePlaceholders: string[]; + translatedPlaceholders: string[]; +}; + +function extractTranslationPlaceholders(text: string): string[] { + return [...new Set([...text.matchAll(/\{(\w+)\}/g)].map((match) => match[1] ?? ""))] + .filter(Boolean) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function findPlaceholderMismatches( + sourceFlat: ReadonlyMap, + translatedFlat: ReadonlyMap, + locale: string, +): PlaceholderMismatch[] { + const mismatches: PlaceholderMismatch[] = []; + for (const [key, sourceText] of sourceFlat.entries()) { + const sourcePlaceholders = extractTranslationPlaceholders(sourceText); + const translatedPlaceholders = extractTranslationPlaceholders(translatedFlat.get(key) ?? ""); + if (!compareStringArrays(sourcePlaceholders, translatedPlaceholders)) { + mismatches.push({ + key, + locale, + sourcePlaceholders, + translatedPlaceholders, + }); + } + } + return mismatches; +} + +function assertPlaceholderParity( + sourceFlat: ReadonlyMap, + translatedFlat: ReadonlyMap, + locale: string, +) { + const mismatches = findPlaceholderMismatches(sourceFlat, translatedFlat, locale); + if (mismatches.length === 0) { + return; + } + + const details = mismatches + .slice(0, 20) + .map( + (mismatch) => + `${mismatch.locale}:${mismatch.key} expected {${mismatch.sourcePlaceholders.join("},{")}} got {${mismatch.translatedPlaceholders.join("},{")}}`, + ) + .join("\n"); + throw new Error( + [ + `control-ui-i18n placeholder mismatch detected for ${locale}.`, + details, + mismatches.length > 20 ? `...and ${mismatches.length - 20} more` : "", + ] + .filter(Boolean) + .join("\n"), + ); +} + function isIdentifier(value: string): boolean { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value); } @@ -1048,12 +1112,12 @@ class PiRpcClient { private readonly stderrChunks: string[] = []; private closed = false; private pending: PendingPrompt | null = null; - private readonly process; - private readonly stdin; + private readonly process: ChildProcessWithoutNullStreams; + private readonly stdin: ChildProcessWithoutNullStreams["stdin"]; private requestCount = 0; - private sequence = Promise.resolve(); + private sequence: Promise = Promise.resolve(); - private constructor(processHandle: ReturnType) { + private constructor(processHandle: ChildProcessWithoutNullStreams) { this.process = processHandle; this.stdin = processHandle.stdin; } @@ -1174,7 +1238,7 @@ class PiRpcClient { } async prompt(message: string, label: string): Promise { - this.sequence = this.sequence.then(async () => { + const result = this.sequence.then(async () => { if (this.closed) { throw new Error(`pi process unavailable${this.stderr() ? ` (${this.stderr()})` : ""}`); } @@ -1236,7 +1300,8 @@ class PiRpcClient { }); }); - return (await this.sequence) as string; + this.sequence = result.catch(() => undefined); + return await result; } async close() { @@ -1507,6 +1572,8 @@ async function syncLocale( // legitimately stay identical to English. Track fallback keys from actual // fallback decisions and previous fallback metadata instead. + assertPlaceholderParity(sourceFlat, nextFlat, entry.locale); + const nextMap: TranslationMap = {}; for (const [key, value] of sourceFlat.entries()) { setNestedValue(nextMap, key, nextFlat.get(key) ?? value); @@ -1698,7 +1765,14 @@ async function main() { } } -await main().catch((error) => { - console.error(formatErrorMessage(error)); - process.exit(1); -}); +function isCliEntrypoint() { + const entrypoint = process.argv[1]; + return Boolean(entrypoint && import.meta.url === pathToFileURL(path.resolve(entrypoint)).href); +} + +if (isCliEntrypoint()) { + await main().catch((error) => { + console.error(formatErrorMessage(error)); + process.exit(1); + }); +} diff --git a/scripts/lib/docker-e2e-plan.mjs b/scripts/lib/docker-e2e-plan.mjs index 421128108ab..b8de69eb664 100644 --- a/scripts/lib/docker-e2e-plan.mjs +++ b/scripts/lib/docker-e2e-plan.mjs @@ -369,7 +369,7 @@ function buildPlanJson(params) { bareImage: imageKinds.includes("bare"), e2eImage: imageKinds.length > 0, functionalImage: imageKinds.includes("functional"), - liveImage: scheduledLanes.some((poolLane) => poolLane.live), + liveImage: scheduledLanes.some((poolLane) => poolLane.needsLiveImage), package: lanesNeedOpenClawPackage(scheduledLanes), }, profile: params.profile, diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index e462220fe41..911e2d4922c 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -36,6 +36,7 @@ function lane(name, command, options = {}) { live: options.live === true, noOutputTimeoutMs: options.noOutputTimeoutMs, name, + needsLiveImage: options.needsLiveImage, retryPatterns: options.retryPatterns ?? [], retries: options.retries ?? 0, resources: options.resources ?? [], @@ -79,6 +80,7 @@ function liveLane(name, command, options = {}) { return lane(name, command, { ...options, live: true, + needsLiveImage: options.needsLiveImage ?? true, resources: ["live", ...liveProviderResources(options), ...(options.resources ?? [])], retryPatterns: options.retryPatterns ?? LIVE_RETRY_PATTERNS, retries: options.retries ?? DEFAULT_LIVE_RETRIES, @@ -158,6 +160,8 @@ export const mainLanes = [ }, ), liveLane("openwebui", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui", { + e2eImageKind: "functional", + needsLiveImage: false, provider: "openai", resources: ["service"], timeoutMs: OPENWEBUI_TIMEOUT_MS, @@ -583,6 +587,8 @@ const legacyReleasePathChunks = { function openWebUILane() { return liveLane("openwebui", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui", { + e2eImageKind: "functional", + needsLiveImage: false, provider: "openai", resources: ["service"], timeoutMs: OPENWEBUI_TIMEOUT_MS, diff --git a/scripts/run-node-watch-paths.mjs b/scripts/run-node-watch-paths.mjs new file mode 100644 index 00000000000..c04af8e25d9 --- /dev/null +++ b/scripts/run-node-watch-paths.mjs @@ -0,0 +1,63 @@ +import path from "node:path"; +import { + BUNDLED_PLUGIN_PATH_PREFIX, + BUNDLED_PLUGIN_ROOT_DIR, +} from "./lib/bundled-plugin-paths.mjs"; + +export const runNodeSourceRoots = ["src", BUNDLED_PLUGIN_ROOT_DIR]; +export const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"]; +export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles]; +export const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]); + +const ignoredRunNodeRepoPaths = new Set([ + "src/canvas-host/a2ui/.bundle.hash", + "src/canvas-host/a2ui/a2ui.bundle.js", +]); +const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/; + +export const normalizeRunNodePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/"); + +const isIgnoredSourcePath = (relativePath) => { + const normalizedPath = normalizeRunNodePath(relativePath); + return ( + normalizedPath.endsWith(".test.ts") || + normalizedPath.endsWith(".test.tsx") || + normalizedPath.endsWith("test-helpers.ts") + ); +}; + +const isBuildRelevantSourcePath = (relativePath) => { + const normalizedPath = normalizeRunNodePath(relativePath); + return extensionSourceFilePattern.test(normalizedPath) && !isIgnoredSourcePath(normalizedPath); +}; + +const isRestartRelevantExtensionPath = (relativePath) => { + const normalizedPath = normalizeRunNodePath(relativePath); + if (extensionRestartMetadataFiles.has(path.posix.basename(normalizedPath))) { + return true; + } + return isBuildRelevantSourcePath(normalizedPath); +}; + +const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => { + const normalizedPath = normalizeRunNodePath(repoPath).replace(/^\.\/+/, ""); + if (ignoredRunNodeRepoPaths.has(normalizedPath)) { + return false; + } + if (runNodeConfigFiles.includes(normalizedPath)) { + return true; + } + if (normalizedPath.startsWith("src/")) { + return !isIgnoredSourcePath(normalizedPath.slice("src/".length)); + } + if (normalizedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) { + return isRelevantBundledPluginPath(normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length)); + } + return false; +}; + +export const isBuildRelevantRunNodePath = (repoPath) => + isRelevantRunNodePath(repoPath, isBuildRelevantSourcePath); + +export const isRestartRelevantRunNodePath = (repoPath) => + isRelevantRunNodePath(repoPath, isRestartRelevantExtensionPath); diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index 0f536e3772a..7b485805527 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -16,14 +16,22 @@ import { writeRuntimePostBuildStamp as writeDistRuntimePostBuildStamp, } from "./lib/local-build-metadata.mjs"; import { listStaticExtensionAssetSources } from "./lib/static-extension-assets.mjs"; +import { + extensionRestartMetadataFiles, + isBuildRelevantRunNodePath, + isRestartRelevantRunNodePath, + normalizeRunNodePath as normalizePath, + runNodeConfigFiles, + runNodeSourceRoots, + runNodeWatchedPaths, +} from "./run-node-watch-paths.mjs"; import { runRuntimePostBuild } from "./runtime-postbuild.mjs"; +export { isBuildRelevantRunNodePath, isRestartRelevantRunNodePath, runNodeWatchedPaths }; + const buildScript = "scripts/tsdown-build.mjs"; const compilerArgs = [buildScript, "--no-clean"]; -const runNodeSourceRoots = ["src", BUNDLED_PLUGIN_ROOT_DIR]; -const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"]; -export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles]; const runtimePostBuildWatchedPaths = [ "scripts/copy-bundled-plugin-metadata.mjs", "scripts/copy-plugin-sdk-root-alias.mjs", @@ -40,63 +48,10 @@ const runtimePostBuildWatchedPaths = [ "src/plugin-sdk/root-alias.cjs", BUNDLED_PLUGIN_ROOT_DIR, ]; -const ignoredRunNodeRepoPaths = new Set([ - "src/canvas-host/a2ui/.bundle.hash", - "src/canvas-host/a2ui/a2ui.bundle.js", -]); const runtimePostBuildScriptPaths = new Set( runtimePostBuildWatchedPaths.filter((entry) => entry.startsWith("scripts/")), ); const runtimePostBuildStaticAssetPaths = new Set(listStaticExtensionAssetSources()); -const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/; -const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]); - -const normalizePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/"); - -const isIgnoredSourcePath = (relativePath) => { - const normalizedPath = normalizePath(relativePath); - return ( - normalizedPath.endsWith(".test.ts") || - normalizedPath.endsWith(".test.tsx") || - normalizedPath.endsWith("test-helpers.ts") - ); -}; - -const isBuildRelevantSourcePath = (relativePath) => { - const normalizedPath = normalizePath(relativePath); - return extensionSourceFilePattern.test(normalizedPath) && !isIgnoredSourcePath(normalizedPath); -}; - -const isRestartRelevantExtensionPath = (relativePath) => { - const normalizedPath = normalizePath(relativePath); - if (extensionRestartMetadataFiles.has(path.posix.basename(normalizedPath))) { - return true; - } - return isBuildRelevantSourcePath(normalizedPath); -}; - -const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => { - const normalizedPath = normalizePath(repoPath).replace(/^\.\/+/, ""); - if (ignoredRunNodeRepoPaths.has(normalizedPath)) { - return false; - } - if (runNodeConfigFiles.includes(normalizedPath)) { - return true; - } - if (normalizedPath.startsWith("src/")) { - return !isIgnoredSourcePath(normalizedPath.slice("src/".length)); - } - if (normalizedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) { - return isRelevantBundledPluginPath(normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length)); - } - return false; -}; - -export const isBuildRelevantRunNodePath = (repoPath) => - isRelevantRunNodePath(repoPath, isBuildRelevantSourcePath); - -export const isRestartRelevantRunNodePath = (repoPath) => - isRelevantRunNodePath(repoPath, isRestartRelevantExtensionPath); const statMtime = (filePath, fsImpl = fs) => { try { diff --git a/scripts/watch-node.mjs b/scripts/watch-node.mjs index 92131ff39de..6cb65d6dbc9 100644 --- a/scripts/watch-node.mjs +++ b/scripts/watch-node.mjs @@ -5,7 +5,7 @@ import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; -import { isRestartRelevantRunNodePath, runNodeWatchedPaths } from "./run-node.mjs"; +import { isRestartRelevantRunNodePath, runNodeWatchedPaths } from "./run-node-watch-paths.mjs"; const WATCH_NODE_RUNNER = "scripts/run-node.mjs"; const WATCH_RESTART_SIGNAL = "SIGTERM"; @@ -255,19 +255,6 @@ const releaseWatchLock = (lockHandle) => { * }} [params] */ export async function runWatchMain(params = {}) { - let createWatcher = params.createWatcher; - if (!createWatcher) { - try { - const chokidarModule = await (params.loadChokidar ?? loadChokidar)(); - createWatcher = (watchPaths, options) => chokidarModule.watch(watchPaths, options); - } catch (err) { - if (isInvalidPackageConfigError(err)) { - printFriendlyWatchStartupError(err); - } - throw err; - } - } - const deps = { spawn: params.spawn ?? spawn, process: params.process ?? process, @@ -278,7 +265,8 @@ export async function runWatchMain(params = {}) { sleep: params.sleep ?? sleep, signalProcess: params.signalProcess ?? ((pid, signal) => process.kill(pid, signal)), lockDisabled: params.lockDisabled === true, - createWatcher, + createWatcher: params.createWatcher, + loadChokidar: params.loadChokidar ?? loadChokidar, watchPaths: params.watchPaths ?? runNodeWatchedPaths, }; @@ -293,7 +281,7 @@ export async function runWatchMain(params = {}) { childEnv.OPENCLAW_WATCH_COMMAND = deps.args.join(" "); } - return await new Promise((resolve) => { + return await new Promise((resolve, reject) => { let settled = false; let shuttingDown = false; let restartRequested = false; @@ -357,6 +345,38 @@ export async function runWatchMain(params = {}) { settle(1); }; + const rejectWatcherStartupError = (err) => { + if (settled) { + return; + } + settled = true; + shuttingDown = true; + if (watchProcess && typeof watchProcess.kill === "function") { + watchProcess.kill(WATCH_RESTART_SIGNAL); + } + releaseWatchLock(lockHandle); + watcher?.close?.().catch?.(() => {}); + if (onSigInt) { + deps.process.off("SIGINT", onSigInt); + } + if (onSigTerm) { + deps.process.off("SIGTERM", onSigTerm); + } + reject(err); + }; + + const resolveCreateWatcher = async () => { + try { + const chokidarModule = await deps.loadChokidar(); + return (watchPaths, options) => chokidarModule.watch(watchPaths, options); + } catch (err) { + if (isInvalidPackageConfigError(err)) { + printFriendlyWatchStartupError(err); + } + throw err; + } + }; + const runAutoDoctorAndRestart = () => { autoDoctorAttempted = true; logWatcher( @@ -405,8 +425,11 @@ export async function runWatchMain(params = {}) { } }; - const startWatcher = () => { - watcher = deps.createWatcher(deps.watchPaths, { + const attachWatcher = (createWatcher) => { + if (settled) { + return; + } + watcher = createWatcher(deps.watchPaths, { ignoreInitial: true, ignored: (watchPath, stats) => isIgnoredWatchPath(watchPath, deps.cwd, deps.watchPaths, stats), @@ -417,6 +440,14 @@ export async function runWatchMain(params = {}) { watcher.on("error", handleWatcherError); }; + const startWatcher = () => { + if (deps.createWatcher) { + attachWatcher(deps.createWatcher); + return; + } + void resolveCreateWatcher().then(attachWatcher).catch(rejectWatcherStartupError); + }; + onSigInt = () => { shuttingDown = true; if (watchProcess && typeof watchProcess.kill === "function") { diff --git a/src/commands/doctor-plugin-registry.test.ts b/src/commands/doctor-plugin-registry.test.ts index d6ed337bc72..aa76bb870d3 100644 --- a/src/commands/doctor-plugin-registry.test.ts +++ b/src/commands/doctor-plugin-registry.test.ts @@ -60,6 +60,88 @@ function createCandidate(rootDir: string, id = "demo"): PluginCandidate { }; } +function createBundledCandidate(params: { + rootDir: string; + id: string; + packageName: string; + version: string; +}): PluginCandidate { + fs.writeFileSync( + path.join(params.rootDir, "index.ts"), + "throw new Error('runtime entry should not load during doctor registry repair');\n", + "utf8", + ); + fs.writeFileSync( + path.join(params.rootDir, "openclaw.plugin.json"), + JSON.stringify({ + id: params.id, + name: params.id, + configSchema: { type: "object" }, + providers: [params.id], + }), + "utf8", + ); + fs.writeFileSync( + path.join(params.rootDir, "package.json"), + JSON.stringify({ + name: params.packageName, + version: params.version, + }), + "utf8", + ); + return { + idHint: params.id, + source: path.join(params.rootDir, "index.ts"), + rootDir: params.rootDir, + origin: "bundled", + packageName: params.packageName, + packageVersion: params.version, + }; +} + +function createManagedNpmPlugin(params: { + stateDir: string; + id: string; + packageName: string; + version: string; +}) { + const npmRoot = path.join(params.stateDir, "npm"); + const packageDir = path.join(npmRoot, "node_modules", params.packageName); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync( + path.join(npmRoot, "package.json"), + JSON.stringify({ + dependencies: { + [params.packageName]: params.version, + }, + }), + "utf8", + ); + fs.writeFileSync( + path.join(packageDir, "package.json"), + JSON.stringify({ + name: params.packageName, + version: params.version, + openclaw: { + extensions: ["."], + }, + }), + "utf8", + ); + fs.writeFileSync( + path.join(packageDir, "openclaw.plugin.json"), + JSON.stringify({ + id: params.id, + name: params.id, + configSchema: { + type: "object", + }, + }), + "utf8", + ); + return { npmRoot, packageDir }; +} + function createCurrentIndex(): InstalledPluginIndex { return { version: 1, @@ -115,4 +197,108 @@ describe("maybeRepairPluginRegistryState", () => { expect(nextConfig).toEqual({}); expect(vi.mocked(note).mock.calls.join("\n")).toContain(DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV); }); + + it("warns about stale managed npm packages that shadow bundled plugins", async () => { + const stateDir = makeTempDir(); + const bundledDir = path.join(stateDir, "bundled", "google-meet"); + fs.mkdirSync(bundledDir, { recursive: true }); + createManagedNpmPlugin({ + stateDir, + id: "google-meet", + packageName: "@openclaw/google-meet", + version: "2026.5.2", + }); + await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir }); + + await maybeRepairPluginRegistryState({ + stateDir, + candidates: [ + createBundledCandidate({ + rootDir: bundledDir, + id: "google-meet", + packageName: "@openclaw/google-meet", + version: "2026.5.3", + }), + ], + env: hermeticEnv(), + config: { + plugins: { + allow: ["google-meet"], + entries: { + "google-meet": { + enabled: true, + config: {}, + }, + }, + }, + }, + prompter: { shouldRepair: false }, + }); + + expect(vi.mocked(note).mock.calls.join("\n")).toContain( + "Managed npm plugin packages shadow bundled plugins", + ); + expect(vi.mocked(note).mock.calls.join("\n")).toContain("@openclaw/google-meet@2026.5.2"); + expect( + fs.existsSync(path.join(stateDir, "npm", "node_modules", "@openclaw", "google-meet")), + ).toBe(true); + }); + + it("removes stale managed npm packages that shadow bundled plugins during repair", async () => { + const stateDir = makeTempDir(); + const bundledDir = path.join(stateDir, "bundled", "google-meet"); + fs.mkdirSync(bundledDir, { recursive: true }); + createManagedNpmPlugin({ + stateDir, + id: "google-meet", + packageName: "@openclaw/google-meet", + version: "2026.5.2", + }); + await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir }); + + await maybeRepairPluginRegistryState({ + stateDir, + candidates: [ + createBundledCandidate({ + rootDir: bundledDir, + id: "google-meet", + packageName: "@openclaw/google-meet", + version: "2026.5.3", + }), + ], + env: hermeticEnv(), + config: { + plugins: { + allow: ["google-meet"], + entries: { + "google-meet": { + enabled: true, + config: {}, + }, + }, + }, + }, + prompter: { shouldRepair: true }, + }); + + expect( + fs.existsSync(path.join(stateDir, "npm", "node_modules", "@openclaw", "google-meet")), + ).toBe(false); + expect( + JSON.parse(fs.readFileSync(path.join(stateDir, "npm", "package.json"), "utf8")), + ).not.toHaveProperty("dependencies"); + await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({ + refreshReason: "migration", + plugins: [ + expect.objectContaining({ + pluginId: "google-meet", + origin: "bundled", + rootDir: bundledDir, + }), + ], + }); + expect(vi.mocked(note).mock.calls.join("\n")).toContain( + "Removed stale managed npm plugin package", + ); + }); }); diff --git a/src/commands/doctor-plugin-registry.ts b/src/commands/doctor-plugin-registry.ts index f1d47c773f7..42e010407e4 100644 --- a/src/commands/doctor-plugin-registry.ts +++ b/src/commands/doctor-plugin-registry.ts @@ -1,6 +1,12 @@ +import fs from "node:fs"; +import path from "node:path"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { saveJsonFile } from "../infra/json-file.js"; +import { resolveDefaultPluginNpmDir } from "../plugins/install-paths.js"; import type { InstalledPluginIndexRecordStoreOptions } from "../plugins/installed-plugin-index-records.js"; +import { readPersistedInstalledPluginIndexInstallRecordsSync } from "../plugins/installed-plugin-index-records.js"; +import { loadInstalledPluginIndex } from "../plugins/installed-plugin-index.js"; import { refreshPluginRegistry } from "../plugins/plugin-registry.js"; import { note } from "../terminal/note.js"; import { shortenHomePath } from "../utils.js"; @@ -18,6 +24,169 @@ type PluginRegistryDoctorRepairParams = Omit; }; +type StaleManagedNpmBundledPlugin = { + pluginId: string; + packageName: string; + packageDir: string; + npmRoot: string; + version?: string; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readJsonObject(filePath: string): Record | null { + try { + const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown; + return isRecord(parsed) ? parsed : null; + } catch { + return null; + } +} + +function readStringMap(value: unknown): Record { + if (!isRecord(value)) { + return {}; + } + const result: Record = {}; + for (const [key, raw] of Object.entries(value)) { + if (typeof raw === "string" && raw.trim()) { + result[key] = raw.trim(); + } + } + return result; +} + +function readPackageVersion(packageDir: string): string | undefined { + const packageJson = readJsonObject(path.join(packageDir, "package.json")); + const version = packageJson?.version; + return typeof version === "string" && version.trim() ? version.trim() : undefined; +} + +function readPluginManifestId(packageDir: string): string | undefined { + const manifest = readJsonObject(path.join(packageDir, "openclaw.plugin.json")); + const id = manifest?.id; + return typeof id === "string" && id.trim() ? id.trim() : undefined; +} + +function listStaleManagedNpmBundledPlugins( + params: PluginRegistryDoctorRepairParams, +): StaleManagedNpmBundledPlugin[] { + const persistedInstallRecords = readPersistedInstalledPluginIndexInstallRecordsSync(params) ?? {}; + const currentBundled = loadInstalledPluginIndex({ + ...params, + installRecords: {}, + }).plugins.filter((plugin) => plugin.origin === "bundled" && plugin.packageName); + const bundledByPackage = new Map( + currentBundled.map((plugin) => [plugin.packageName, plugin] as const), + ); + const npmRoot = params.stateDir + ? path.join(params.stateDir, "npm") + : resolveDefaultPluginNpmDir(params.env); + const npmPackageJsonPath = path.join(npmRoot, "package.json"); + const dependencies = readStringMap(readJsonObject(npmPackageJsonPath)?.dependencies); + const stale: StaleManagedNpmBundledPlugin[] = []; + + for (const packageName of Object.keys(dependencies).toSorted()) { + if (!packageName.startsWith("@openclaw/")) { + continue; + } + const bundled = bundledByPackage.get(packageName); + if (!bundled) { + continue; + } + const packageDir = path.join(npmRoot, "node_modules", packageName); + const pluginId = readPluginManifestId(packageDir); + if (!pluginId || pluginId !== bundled.pluginId) { + continue; + } + const persistedRecord = persistedInstallRecords[pluginId]; + if (persistedRecord?.source === "npm") { + continue; + } + stale.push({ + pluginId, + packageName, + packageDir, + npmRoot, + ...(readPackageVersion(packageDir) ? { version: readPackageVersion(packageDir) } : {}), + }); + } + + return stale; +} + +function removeManagedNpmDependency(params: { + npmRoot: string; + packageName: string; + packageDir: string; +}): void { + const npmPackageJsonPath = path.join(params.npmRoot, "package.json"); + const packageJson = readJsonObject(npmPackageJsonPath) ?? {}; + const dependencies = readStringMap(packageJson.dependencies); + delete dependencies[params.packageName]; + const nextPackageJson = + Object.keys(dependencies).length === 0 + ? (() => { + const { dependencies: _dependencies, ...rest } = packageJson; + return rest; + })() + : { + ...packageJson, + dependencies, + }; + saveJsonFile(npmPackageJsonPath, nextPackageJson); + fs.rmSync(params.packageDir, { recursive: true, force: true }); + const scopeDir = path.dirname(params.packageDir); + if (path.basename(path.dirname(scopeDir)) === "node_modules") { + try { + fs.rmdirSync(scopeDir); + } catch { + // Other packages can still live under the scope directory. + } + } +} + +function maybeRepairStaleManagedNpmBundledPlugins( + params: PluginRegistryDoctorRepairParams, +): boolean { + const stale = listStaleManagedNpmBundledPlugins(params); + if (stale.length === 0) { + return false; + } + + if (!params.prompter.shouldRepair) { + note( + [ + "Managed npm plugin packages shadow bundled plugins:", + ...stale.map( + (plugin) => + `- ${plugin.pluginId}: ${plugin.packageName}${plugin.version ? `@${plugin.version}` : ""}`, + ), + `Repair with ${formatCliCommand("openclaw doctor --fix")} to remove stale managed npm packages and rebuild the plugin registry.`, + ].join("\n"), + "Plugin registry", + ); + return false; + } + + for (const plugin of stale) { + removeManagedNpmDependency(plugin); + } + note( + [ + "Removed stale managed npm plugin package(s) shadowing bundled plugins:", + ...stale.map( + (plugin) => + `- ${plugin.pluginId}: ${plugin.packageName}${plugin.version ? `@${plugin.version}` : ""}`, + ), + ].join("\n"), + "Plugin registry", + ); + return true; +} + export async function maybeRepairPluginRegistryState( params: PluginRegistryDoctorRepairParams, ): Promise { @@ -37,6 +206,7 @@ export async function maybeRepairPluginRegistryState( ...params, config: params.config, }; + const removedStaleManagedNpmBundledPlugins = maybeRepairStaleManagedNpmBundledPlugins(params); if (!params.prompter.shouldRepair) { if (preflight.action === "migrate") { note( @@ -63,7 +233,7 @@ export async function maybeRepairPluginRegistryState( return params.config; } - if (preflight.action === "skip-existing") { + if (preflight.action === "skip-existing" || removedStaleManagedNpmBundledPlugins) { const index = await refreshPluginRegistry({ ...migrationParams, reason: "migration", diff --git a/src/gateway/server-runtime-state.test.ts b/src/gateway/server-runtime-state.test.ts index 3fb42b87b93..fe0f294d632 100644 --- a/src/gateway/server-runtime-state.test.ts +++ b/src/gateway/server-runtime-state.test.ts @@ -1,4 +1,7 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { getActivePluginChannelRegistry, @@ -26,10 +29,13 @@ function createRegistryWithRoute(path: string) { } describe("createGatewayRuntimeState", () => { + const tempDirs: string[] = []; + afterEach(() => { releasePinnedPluginHttpRouteRegistry(); releasePinnedPluginChannelRegistry(); resetPluginRuntimeStateForTest(); + return Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); }); it("releases post-bootstrap repinned plugin registries on cleanup", async () => { @@ -70,4 +76,38 @@ describe("createGatewayRuntimeState", () => { expect(resolveActivePluginHttpRouteRegistry(fallbackRegistry)).toBe(startupRegistry); expect(getActivePluginChannelRegistry()).toBe(startupRegistry); }); + + it("creates the canvas host without logging it before HTTP bind", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-runtime-")); + tempDirs.push(root); + const registry = createEmptyPluginRegistry(); + const logCanvas = { info: vi.fn(), warn: vi.fn() }; + + const runtimeState = await createGatewayRuntimeState({ + cfg: { canvasHost: { root, liveReload: false } }, + bindHost: "127.0.0.1", + port: 18789, + controlUiEnabled: false, + controlUiBasePath: "/", + openAiChatCompletionsEnabled: false, + openResponsesEnabled: false, + resolvedAuth: {} as never, + getResolvedAuth: () => ({}) as never, + hooksConfig: () => null, + getHookClientIpConfig: () => ({}) as never, + pluginRegistry: registry, + deps: {} as never, + canvasRuntime: { log: () => {} } as never, + canvasHostEnabled: true, + allowCanvasHostInTests: true, + logCanvas, + log: { info: () => {}, warn: () => {} }, + logHooks: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as never, + logPlugins: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as never, + }); + + expect(runtimeState.canvasHost?.rootDir).toBe(root); + expect(logCanvas.info).not.toHaveBeenCalled(); + await runtimeState.canvasHost?.close(); + }); }); diff --git a/src/gateway/server-runtime-state.ts b/src/gateway/server-runtime-state.ts index d1d9b05bf61..0b1b624c137 100644 --- a/src/gateway/server-runtime-state.ts +++ b/src/gateway/server-runtime-state.ts @@ -130,9 +130,6 @@ export async function createGatewayRuntimeState(params: { }); if (handler.rootDir) { canvasHost = handler; - params.logCanvas.info( - `canvas host mounted at http://${params.bindHost}:${params.port}${CANVAS_HOST_PATH}/ (root ${handler.rootDir})`, - ); } } catch (err) { params.logCanvas.warn(`canvas host failed to start: ${String(err)}`); diff --git a/src/gateway/server-ws-runtime.ts b/src/gateway/server-ws-runtime.ts index 340ccf3add0..e020d11491c 100644 --- a/src/gateway/server-ws-runtime.ts +++ b/src/gateway/server-ws-runtime.ts @@ -29,6 +29,7 @@ export function attachGatewayWsHandlers(params: GatewayWsRuntimeParams) { port: params.port, gatewayHost: params.gatewayHost, canvasHostEnabled: params.canvasHostEnabled, + canvasHostScheme: params.canvasHostScheme, canvasHostServerPort: params.canvasHostServerPort, resolvedAuth: params.resolvedAuth, getResolvedAuth: params.getResolvedAuth, diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index d4ecdad7128..8c92fa07008 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1,6 +1,7 @@ import { monitorEventLoopDelay, performance } from "node:perf_hooks"; import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/run-state.js"; import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js"; +import { CANVAS_HOST_PATH } from "../canvas-host/a2ui-shared.js"; import type { CanvasHostServer } from "../canvas-host/server.js"; import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js"; import { @@ -1328,6 +1329,7 @@ export async function startGatewayServer( } const { attachGatewayWsHandlers } = await import("./server-ws-runtime.js"); + const canvasHostScheme = gatewayTls.enabled ? "https" : "http"; attachGatewayWsHandlers({ wss, clients, @@ -1335,6 +1337,7 @@ export async function startGatewayServer( port, gatewayHost: bindHost ?? undefined, canvasHostEnabled: Boolean(canvasHost), + canvasHostScheme, canvasHostServerPort, resolvedAuth, getResolvedAuth, @@ -1354,6 +1357,11 @@ export async function startGatewayServer( context: gatewayRequestContext, }); await startListening(); + if (canvasHost?.rootDir) { + logCanvas.info( + `canvas host mounted at ${canvasHostScheme}://${bindHost}:${port}${CANVAS_HOST_PATH}/ (root ${canvasHost.rootDir})`, + ); + } startupTrace.mark("http.bound"); const sessionDeliveryRecoveryMaxEnqueuedAt = Date.now(); let postAttachRuntimeReturned = false; diff --git a/src/gateway/server/ws-connection.test.ts b/src/gateway/server/ws-connection.test.ts index 3e15cd44e3d..8e61172b8a6 100644 --- a/src/gateway/server/ws-connection.test.ts +++ b/src/gateway/server/ws-connection.test.ts @@ -111,6 +111,63 @@ describe("attachGatewayWsConnectionHandler", () => { ); }); + it("uses the gateway TLS scheme for canvas host URLs", async () => { + const listeners = new Map void>(); + const wss = { + on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { + listeners.set(event, handler); + }), + } as unknown as WebSocketServer; + const socket = Object.assign(new EventEmitter(), { + _socket: { + remoteAddress: "127.0.0.1", + remotePort: 1234, + localAddress: "127.0.0.1", + localPort: 5678, + }, + send: vi.fn(), + close: vi.fn(), + }); + const upgradeReq = { + headers: { host: "gateway.example.com" }, + socket: { localAddress: "127.0.0.1" }, + }; + + attachGatewayWsConnectionHandler({ + wss, + clients: new Set(), + preauthConnectionBudget: { release: vi.fn() } as never, + port: 18789, + canvasHostEnabled: true, + canvasHostScheme: "https", + resolvedAuth: createResolvedAuth("token"), + gatewayMethods: [], + events: [], + refreshHealthSnapshot: vi.fn(async () => ({}) as never), + logGateway: createLogger() as never, + logHealth: createLogger() as never, + logWsControl: createLogger() as never, + extraHandlers: {}, + broadcast: vi.fn(), + buildRequestContext: () => + ({ + unsubscribeAllSessionEvents: vi.fn(), + nodeRegistry: { unregister: vi.fn() }, + nodeUnsubscribeAll: vi.fn(), + }) as never, + }); + + const onConnection = listeners.get("connection"); + expect(onConnection).toBeTypeOf("function"); + onConnection?.(socket, upgradeReq); + await waitForLazyMessageHandler(); + + const passed = attachGatewayWsMessageHandlerMock.mock.calls[0]?.[0] as { + canvasHostUrl?: string; + }; + expect(passed.canvasHostUrl).toBe("https://gateway.example.com:443"); + }); + it("rejects late client registration after a pre-connect socket close", async () => { const listeners = new Map void>(); const wss = { diff --git a/src/gateway/server/ws-connection.ts b/src/gateway/server/ws-connection.ts index 22d9ebae3be..c36aa488906 100644 --- a/src/gateway/server/ws-connection.ts +++ b/src/gateway/server/ws-connection.ts @@ -124,6 +124,7 @@ export type GatewayWsSharedHandlerParams = { port: number; gatewayHost?: string; canvasHostEnabled: boolean; + canvasHostScheme?: "http" | "https"; canvasHostServerPort?: number; resolvedAuth: ResolvedGatewayAuth; getResolvedAuth?: () => ResolvedGatewayAuth; @@ -199,6 +200,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti port, gatewayHost, canvasHostEnabled, + canvasHostScheme, canvasHostServerPort, resolvedAuth, getResolvedAuth = () => resolvedAuth, @@ -253,6 +255,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti requestHost: upgradeReq.headers.host, forwardedProto: upgradeReq.headers["x-forwarded-proto"], localAddress: upgradeReq.socket?.localAddress, + scheme: canvasHostScheme, }); logWs("in", "open", { connId, remoteAddr, remotePort, localAddr, localPort, endpoint }); diff --git a/src/infra/watch-node.test.ts b/src/infra/watch-node.test.ts index 3c573da5e43..3e6f787e3c9 100644 --- a/src/infra/watch-node.test.ts +++ b/src/infra/watch-node.test.ts @@ -155,6 +155,49 @@ describe("watch-node script", () => { }); }); + it("starts the runner before loading chokidar", async () => { + const child = Object.assign(new EventEmitter(), { + kill: vi.fn(() => {}), + }); + const spawn = vi.fn(() => child); + const watcher = Object.assign(new EventEmitter(), { + close: vi.fn(async () => {}), + }); + const watch = vi.fn(() => watcher); + let resolveLoadChokidar: (value: { watch: typeof watch }) => void = () => {}; + const loadChokidar = vi.fn( + () => + new Promise<{ watch: typeof watch }>((resolve) => { + resolveLoadChokidar = resolve; + }), + ); + const fakeProcess = createFakeProcess(); + + const runPromise = runWatch({ + args: ["gateway", "--force"], + loadChokidar, + lockDisabled: true, + process: fakeProcess, + spawn, + }); + + expect(spawn).toHaveBeenCalledTimes(1); + expect(loadChokidar).toHaveBeenCalledTimes(1); + expect(spawn.mock.invocationCallOrder[0]).toBeLessThan( + loadChokidar.mock.invocationCallOrder[0], + ); + + resolveLoadChokidar({ watch }); + await new Promise((resolve) => setImmediate(resolve)); + expect(watch).toHaveBeenCalledTimes(1); + + fakeProcess.emit("SIGINT"); + const exitCode = await runPromise; + expect(exitCode).toBe(130); + expect(child.kill).toHaveBeenCalledWith("SIGTERM"); + expect(watcher.close).toHaveBeenCalledTimes(1); + }); + it("terminates child on SIGINT and returns shell interrupt code", async () => { const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness(); @@ -412,6 +455,10 @@ describe("watch-node script", () => { ), { code: "ERR_INVALID_PACKAGE_CONFIG" }, ); + const child = Object.assign(new EventEmitter(), { + kill: vi.fn(() => {}), + }); + const spawn = vi.fn(() => child); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { @@ -423,9 +470,12 @@ describe("watch-node script", () => { throw error; }), process: createFakeProcess(), + spawn, }), ).rejects.toBe(error); + expect(spawn).toHaveBeenCalledTimes(1); + expect(child.kill).toHaveBeenCalledWith("SIGTERM"); expect(errorSpy.mock.calls).toEqual([ [""], [ @@ -450,6 +500,10 @@ describe("watch-node script", () => { const error = Object.assign(new Error("Cannot find package 'chokidar'"), { code: "ERR_MODULE_NOT_FOUND", }); + const child = Object.assign(new EventEmitter(), { + kill: vi.fn(() => {}), + }); + const spawn = vi.fn(() => child); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { @@ -459,9 +513,12 @@ describe("watch-node script", () => { throw error; }), process: createFakeProcess(), + spawn, }), ).rejects.toBe(error); + expect(spawn).toHaveBeenCalledTimes(1); + expect(child.kill).toHaveBeenCalledWith("SIGTERM"); expect(errorSpy).not.toHaveBeenCalled(); } finally { errorSpy.mockRestore(); diff --git a/src/plugin-state/plugin-state-store.e2e.test.ts b/src/plugin-state/plugin-state-store.e2e.test.ts index 95bc5ffa593..d6ab66a72de 100644 --- a/src/plugin-state/plugin-state-store.e2e.test.ts +++ b/src/plugin-state/plugin-state-store.e2e.test.ts @@ -31,6 +31,7 @@ describe("runtime smoke", () => { }); expect(store).toBeDefined(); expect(typeof store.register).toBe("function"); + expect(typeof store.registerIfAbsent).toBe("function"); expect(typeof store.lookup).toBe("function"); expect(typeof store.consume).toBe("function"); }); diff --git a/src/plugin-state/plugin-state-store.runtime.test.ts b/src/plugin-state/plugin-state-store.runtime.test.ts index 851cf6bf0ac..b44e19e36c2 100644 --- a/src/plugin-state/plugin-state-store.runtime.test.ts +++ b/src/plugin-state/plugin-state-store.runtime.test.ts @@ -65,7 +65,8 @@ describe("plugin runtime state proxy", () => { namespace: "runtime", maxEntries: 10, }); - await store.register("k", { plugin: "discord" }); + await expect(store.registerIfAbsent("k", { plugin: "discord" })).resolves.toBe(true); + await expect(store.registerIfAbsent("k", { plugin: "duplicate" })).resolves.toBe(false); const telegram = createPluginRecord("telegram", "bundled"); registry.registry.plugins.push(telegram); diff --git a/src/plugin-state/plugin-state-store.sqlite.ts b/src/plugin-state/plugin-state-store.sqlite.ts index ab5e98b77a9..d3e97c7c19b 100644 --- a/src/plugin-state/plugin-state-store.sqlite.ts +++ b/src/plugin-state/plugin-state-store.sqlite.ts @@ -40,6 +40,7 @@ type UserVersionRow = { type PluginStateStatements = { upsertEntry: StatementSync; + insertEntryIfAbsent: StatementSync; selectEntry: StatementSync; selectEntries: StatementSync; deleteEntry: StatementSync; @@ -190,6 +191,23 @@ function createStatements(db: DatabaseSync): PluginStateStatements { created_at = excluded.created_at, expires_at = excluded.expires_at `), + insertEntryIfAbsent: db.prepare(` + INSERT OR IGNORE INTO plugin_state_entries ( + plugin_id, + namespace, + entry_key, + value_json, + created_at, + expires_at + ) VALUES ( + @plugin_id, + @namespace, + @entry_key, + @value_json, + @created_at, + @expires_at + ) + `), selectEntry: db.prepare(` SELECT plugin_id, namespace, entry_key, value_json, created_at, expires_at FROM plugin_state_entries @@ -241,6 +259,7 @@ function createStatements(db: DatabaseSync): PluginStateStatements { FROM plugin_state_entries WHERE plugin_id = ? AND namespace = ? + AND entry_key <> ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at ASC, entry_key ASC LIMIT ? @@ -363,6 +382,46 @@ function runWriteTransaction( } } +function enforcePostRegisterLimits(params: { + store: PluginStateDatabase; + pluginId: string; + namespace: string; + maxEntries: number; + now: number; + protectedKey: string; +}): void { + const namespaceCount = countRow( + params.store.statements.countLiveNamespace.get( + params.pluginId, + params.namespace, + params.now, + ) as CountRow | undefined, + ); + if (namespaceCount > params.maxEntries) { + params.store.statements.deleteOldestNamespace.run( + params.pluginId, + params.namespace, + params.protectedKey, + params.now, + namespaceCount - params.maxEntries, + ); + } + + const pluginCount = countRow( + params.store.statements.countLivePlugin.get(params.pluginId, params.now) as + | CountRow + | undefined, + ); + if (pluginCount > MAX_ENTRIES_PER_PLUGIN) { + throw createPluginStateError({ + code: "PLUGIN_STATE_LIMIT_EXCEEDED", + operation: "register", + message: `Plugin state for ${params.pluginId} exceeds the ${MAX_ENTRIES_PER_PLUGIN} live row limit.`, + path: params.store.path, + }); + } +} + export function pluginStateRegister(params: { pluginId: string; namespace: string; @@ -384,32 +443,58 @@ export function pluginStateRegister(params: { created_at: now, expires_at: expiresAt, }); + enforcePostRegisterLimits({ + store, + pluginId: params.pluginId, + namespace: params.namespace, + maxEntries: params.maxEntries, + now, + protectedKey: params.key, + }); + }); + } catch (error) { + throw wrapPluginStateError( + error, + "register", + "PLUGIN_STATE_WRITE_FAILED", + "Failed to register plugin state entry.", + ); + } +} - const namespaceCount = countRow( - store.statements.countLiveNamespace.get(params.pluginId, params.namespace, now) as - | CountRow - | undefined, - ); - if (namespaceCount > params.maxEntries) { - store.statements.deleteOldestNamespace.run( - params.pluginId, - params.namespace, - now, - namespaceCount - params.maxEntries, - ); - } - - const pluginCount = countRow( - store.statements.countLivePlugin.get(params.pluginId, now) as CountRow | undefined, - ); - if (pluginCount > MAX_ENTRIES_PER_PLUGIN) { - throw createPluginStateError({ - code: "PLUGIN_STATE_LIMIT_EXCEEDED", - operation: "register", - message: `Plugin state for ${params.pluginId} exceeds the ${MAX_ENTRIES_PER_PLUGIN} live row limit.`, - path: store.path, - }); +export function pluginStateRegisterIfAbsent(params: { + pluginId: string; + namespace: string; + key: string; + valueJson: string; + maxEntries: number; + ttlMs?: number; +}): boolean { + try { + return runWriteTransaction("register", (store) => { + const now = Date.now(); + const expiresAt = params.ttlMs == null ? null : now + params.ttlMs; + store.statements.pruneExpiredNamespace.run(params.pluginId, params.namespace, now); + const result = store.statements.insertEntryIfAbsent.run({ + plugin_id: params.pluginId, + namespace: params.namespace, + entry_key: params.key, + value_json: params.valueJson, + created_at: now, + expires_at: expiresAt, + }); + if (result.changes === 0) { + return false; } + enforcePostRegisterLimits({ + store, + pluginId: params.pluginId, + namespace: params.namespace, + maxEntries: params.maxEntries, + now, + protectedKey: params.key, + }); + return true; }); } catch (error) { throw wrapPluginStateError( diff --git a/src/plugin-state/plugin-state-store.test.ts b/src/plugin-state/plugin-state-store.test.ts index 3babcb9626d..e2f08e85d71 100644 --- a/src/plugin-state/plugin-state-store.test.ts +++ b/src/plugin-state/plugin-state-store.test.ts @@ -58,6 +58,145 @@ describe("plugin state keyed store", () => { }); }); + it("registerIfAbsent inserts the first value and preserves live duplicates", async () => { + await withOpenClawTestState({ label: "plugin-state-register-if-absent-live" }, async () => { + vi.useFakeTimers(); + const store = createPluginStateKeyedStore<{ version: number }>("discord", { + namespace: "claims", + maxEntries: 10, + }); + + vi.setSystemTime(1000); + await expect(store.registerIfAbsent("claim", { version: 1 }, { ttlMs: 1000 })).resolves.toBe( + true, + ); + vi.setSystemTime(1200); + await expect(store.registerIfAbsent("claim", { version: 2 }, { ttlMs: 5000 })).resolves.toBe( + false, + ); + + await expect(store.lookup("claim")).resolves.toEqual({ version: 1 }); + await expect(store.entries()).resolves.toMatchObject([ + { key: "claim", value: { version: 1 }, createdAt: 1000, expiresAt: 2000 }, + ]); + }); + }); + + it("registerIfAbsent replaces expired keys", async () => { + await withOpenClawTestState({ label: "plugin-state-register-if-absent-expired" }, async () => { + vi.useFakeTimers(); + const store = createPluginStateKeyedStore<{ version: number }>("discord", { + namespace: "claims-expired", + maxEntries: 10, + }); + + vi.setSystemTime(1000); + await expect(store.registerIfAbsent("claim", { version: 1 }, { ttlMs: 100 })).resolves.toBe( + true, + ); + vi.setSystemTime(1200); + await expect(store.registerIfAbsent("claim", { version: 2 })).resolves.toBe(true); + + await expect(store.lookup("claim")).resolves.toEqual({ version: 2 }); + await expect(store.entries()).resolves.toMatchObject([ + { key: "claim", value: { version: 2 }, createdAt: 1200 }, + ]); + }); + }); + + it("registerIfAbsent keeps plugin and namespace claims isolated", async () => { + await withOpenClawTestState( + { label: "plugin-state-register-if-absent-isolation" }, + async () => { + const discordA = createPluginStateKeyedStore<{ owner: string }>("discord", { + namespace: "claims-a", + maxEntries: 10, + }); + const discordB = createPluginStateKeyedStore<{ owner: string }>("discord", { + namespace: "claims-b", + maxEntries: 10, + }); + const telegramA = createPluginStateKeyedStore<{ owner: string }>("telegram", { + namespace: "claims-a", + maxEntries: 10, + }); + + await expect(discordA.registerIfAbsent("same", { owner: "discord-a" })).resolves.toBe(true); + await expect(discordB.registerIfAbsent("same", { owner: "discord-b" })).resolves.toBe(true); + await expect(telegramA.registerIfAbsent("same", { owner: "telegram-a" })).resolves.toBe( + true, + ); + await expect(discordA.registerIfAbsent("same", { owner: "overwrite" })).resolves.toBe( + false, + ); + + await expect(discordA.lookup("same")).resolves.toEqual({ owner: "discord-a" }); + await expect(discordB.lookup("same")).resolves.toEqual({ owner: "discord-b" }); + await expect(telegramA.lookup("same")).resolves.toEqual({ owner: "telegram-a" }); + }, + ); + }); + + it("registerIfAbsent only lets one parallel claimant win", async () => { + await withOpenClawTestState({ label: "plugin-state-register-if-absent-race" }, async () => { + const store = createPluginStateKeyedStore<{ claimant: number }>("discord", { + namespace: "claims-race", + maxEntries: 10, + }); + + const attempts = await Promise.all( + Array.from({ length: 25 }, async (_, claimant) => + store.registerIfAbsent("claim", { claimant }), + ), + ); + + expect(attempts.filter(Boolean)).toHaveLength(1); + const stored = await store.lookup("claim"); + expect(stored).toBeDefined(); + expect(attempts[stored?.claimant ?? -1]).toBe(true); + }); + }); + + it("registerIfAbsent preserves eviction and plugin row cap behavior", async () => { + await withOpenClawTestState({ label: "plugin-state-register-if-absent-limits" }, async () => { + vi.useFakeTimers(); + const evicting = createPluginStateKeyedStore("discord", { + namespace: "claims-evict", + maxEntries: 2, + }); + vi.setSystemTime(1000); + await evicting.registerIfAbsent("a", 1); + vi.setSystemTime(2000); + await evicting.registerIfAbsent("b", 2); + vi.setSystemTime(3000); + await evicting.registerIfAbsent("c", 3); + await expect(evicting.entries()).resolves.toMatchObject([{ key: "b" }, { key: "c" }]); + + seedPluginStateEntriesForTests([ + ...Array.from({ length: 999 }, (_, entryIndex) => ({ + pluginId: "limited-plugin", + namespace: "limit", + key: `k-${entryIndex}`, + value: { entryIndex }, + })), + { + pluginId: "limited-plugin", + namespace: "sibling", + key: "k-0", + value: { sibling: true }, + }, + ]); + const limited = createPluginStateKeyedStore("limited-plugin", { + namespace: "limit", + maxEntries: 1_001, + }); + await expect(limited.registerIfAbsent("overflow", { overflow: true })).rejects.toMatchObject({ + code: "PLUGIN_STATE_LIMIT_EXCEEDED", + }); + await expect(limited.lookup("overflow")).resolves.toBeUndefined(); + }); + }); + it("returns undefined for missing lookups and consumes by deleting atomically", async () => { await withOpenClawTestState({ label: "plugin-state-consume" }, async () => { const store = createPluginStateKeyedStore<{ ok: boolean }>("discord", { @@ -125,6 +264,40 @@ describe("plugin state keyed store", () => { }); }); + it("keeps the just-registered key when namespace eviction timestamps tie", async () => { + await withOpenClawTestState({ label: "plugin-state-eviction-tie-register" }, async () => { + vi.useFakeTimers(); + vi.setSystemTime(1000); + const store = createPluginStateKeyedStore("discord", { + namespace: "evict-tie-register", + maxEntries: 1, + }); + + await store.register("z", 1); + await store.register("a", 2); + + await expect(store.entries()).resolves.toMatchObject([{ key: "a", value: 2 }]); + await expect(store.lookup("z")).resolves.toBeUndefined(); + }); + }); + + it("keeps a same-millisecond registerIfAbsent claim during namespace eviction", async () => { + await withOpenClawTestState({ label: "plugin-state-eviction-tie-claim" }, async () => { + vi.useFakeTimers(); + vi.setSystemTime(1000); + const store = createPluginStateKeyedStore("discord", { + namespace: "evict-tie-claim", + maxEntries: 1, + }); + + await expect(store.registerIfAbsent("z", 1)).resolves.toBe(true); + await expect(store.registerIfAbsent("a", 2)).resolves.toBe(true); + + await expect(store.entries()).resolves.toMatchObject([{ key: "a", value: 2 }]); + await expect(store.lookup("z")).resolves.toBeUndefined(); + }); + }); + it("rejects when the per-plugin live row ceiling would be exceeded without evicting siblings", async () => { await withOpenClawTestState({ label: "plugin-state-plugin-limit" }, async () => { seedPluginStateEntriesForTests([ diff --git a/src/plugin-state/plugin-state-store.ts b/src/plugin-state/plugin-state-store.ts index b8002581f06..5ae358cedb9 100644 --- a/src/plugin-state/plugin-state-store.ts +++ b/src/plugin-state/plugin-state-store.ts @@ -7,6 +7,7 @@ import { pluginStateEntries, pluginStateLookup, pluginStateRegister, + pluginStateRegisterIfAbsent, } from "./plugin-state-store.sqlite.js"; import type { OpenKeyedStoreOptions, @@ -217,20 +218,44 @@ function createKeyedStoreForPluginId( const defaultTtlMs = validateOptionalTtlMs(options.defaultTtlMs); assertConsistentOptions(pluginId, namespace, { maxEntries, defaultTtlMs }); + const prepareRegisterParams = ( + key: string, + value: T, + opts?: { ttlMs?: number }, + ): { key: string; valueJson: string; ttlMs?: number } => { + const normalizedKey = validateKey(key, "register"); + assertJsonSerializable(value); + const json = JSON.stringify(value); + assertValueSize(json); + const ttlMs = validateOptionalTtlMs(opts?.ttlMs, "register") ?? defaultTtlMs; + return { + key: normalizedKey, + valueJson: json, + ...(ttlMs != null ? { ttlMs } : {}), + }; + }; + return { async register(key, value, opts) { - const normalizedKey = validateKey(key, "register"); - assertJsonSerializable(value); - const json = JSON.stringify(value); - assertValueSize(json); - const ttlMs = validateOptionalTtlMs(opts?.ttlMs, "register") ?? defaultTtlMs; + const params = prepareRegisterParams(key, value, opts); pluginStateRegister({ pluginId, namespace, - key: normalizedKey, - valueJson: json, + key: params.key, + valueJson: params.valueJson, maxEntries, - ...(ttlMs != null ? { ttlMs } : {}), + ...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}), + }); + }, + async registerIfAbsent(key, value, opts) { + const params = prepareRegisterParams(key, value, opts); + return pluginStateRegisterIfAbsent({ + pluginId, + namespace, + key: params.key, + valueJson: params.valueJson, + maxEntries, + ...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}), }); }, async lookup(key) { diff --git a/src/plugin-state/plugin-state-store.types.ts b/src/plugin-state/plugin-state-store.types.ts index 8cb17bffe62..9c07e95a7b3 100644 --- a/src/plugin-state/plugin-state-store.types.ts +++ b/src/plugin-state/plugin-state-store.types.ts @@ -7,6 +7,7 @@ export type PluginStateEntry = { export type PluginStateKeyedStore = { register(key: string, value: T, opts?: { ttlMs?: number }): Promise; + registerIfAbsent(key: string, value: T, opts?: { ttlMs?: number }): Promise; lookup(key: string): Promise; consume(key: string): Promise; delete(key: string): Promise; diff --git a/src/scripts/control-ui-i18n.test.ts b/src/scripts/control-ui-i18n.test.ts new file mode 100644 index 00000000000..56bab9b31ca --- /dev/null +++ b/src/scripts/control-ui-i18n.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { findPlaceholderMismatches } from "../../scripts/control-ui-i18n.ts"; + +describe("control-ui-i18n placeholder validation", () => { + it("reports missing and extra placeholders by key", () => { + const mismatches = findPlaceholderMismatches( + new Map([ + ["sessionsView.activeTooltip", "Updated in the last {count} minutes."], + ["sessionsView.store", "Store: {path}"], + ["sessionsView.limitTooltip", "Max sessions to load."], + ]), + new Map([ + ["sessionsView.activeTooltip", "Actualizadas en los últimos N minutos."], + ["sessionsView.store", "Almacén: {path}"], + ["sessionsView.limitTooltip", "Máximo {extra} de sesiones."], + ]), + "es", + ); + + expect(mismatches).toEqual([ + { + key: "sessionsView.activeTooltip", + locale: "es", + sourcePlaceholders: ["count"], + translatedPlaceholders: [], + }, + { + key: "sessionsView.limitTooltip", + locale: "es", + sourcePlaceholders: [], + translatedPlaceholders: ["extra"], + }, + ]); + }); +}); diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index c94a2a19978..d4160b16557 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -472,7 +472,7 @@ describe("scripts/lib/docker-e2e-plan", () => { }); }); - it("plans Open WebUI as a live-only lane with OpenAI credentials", () => { + it("plans Open WebUI as a live-auth functional image lane", () => { const plan = planFor({ includeOpenWebUI: true, selectedLaneNames: ["openwebui"], @@ -481,17 +481,17 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(plan.credentials).toEqual(["openai"]); expect(plan.lanes).toEqual([ expect.objectContaining({ - imageKind: undefined, + imageKind: "functional", live: true, name: "openwebui", resources: expect.arrayContaining(["docker", "live", "live:openai", "service"]), }), ]); expect(plan.needs).toMatchObject({ - e2eImage: false, - functionalImage: false, - liveImage: true, - package: false, + e2eImage: true, + functionalImage: true, + liveImage: false, + package: true, }); }); diff --git a/ui/src/i18n/.i18n/ar.meta.json b/ui/src/i18n/.i18n/ar.meta.json index 22350cf6f61..6696bba25f2 100644 --- a/ui/src/i18n/.i18n/ar.meta.json +++ b/ui/src/i18n/.i18n/ar.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:16:37.931Z", + "generatedAt": "2026-05-04T07:26:59.541Z", "locale": "ar", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/ar.tm.jsonl b/ui/src/i18n/.i18n/ar.tm.jsonl index 1b4fb112829..b7eacf33109 100644 --- a/ui/src/i18n/.i18n/ar.tm.jsonl +++ b/ui/src/i18n/.i18n/ar.tm.jsonl @@ -516,6 +516,7 @@ {"cache_key":"86ff2ddd9482449b2b14084d2aebe364253836d183d518b5c91f91592da1736b","model":"gpt-5.5","provider":"openai","segment_id":"overview.notes.cronTitle","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Cron reminders","text_hash":"b691bf454c30632ee7c03f2d9f3693ab0d165beffa1629a7db30cc09bcfe8591","tgt_lang":"ar","translated":"تذكيرات Cron","updated_at":"2026-04-29T17:37:46.915Z"} {"cache_key":"8748fd8ba00e0e460371d88843a3a77d3c33c447b54451e82cafb023004fb724","model":"gpt-5.5","provider":"openai","segment_id":"nodes.binding.execNodeBinding","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Exec node binding","text_hash":"4f421128b0cba9533df139c20d023669afc1a78e06544578fa84c32681a863bc","tgt_lang":"ar","translated":"ارتباط عقدة Exec","updated_at":"2026-04-29T17:37:24.688Z"} {"cache_key":"874d6b8fef7c97961b90d1979cc890ef7e602783a65e09ac69ee2484b35593f6","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.errorHint","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Error rate = errors / total messages. Lower is better.","text_hash":"4626170f699e5b41fb2a4044fc94204ca8b706a9878382c9d57d97fbb7f8b1f9","tgt_lang":"ar","translated":"معدل الأخطاء = الأخطاء / إجمالي الرسائل. الأقل أفضل.","updated_at":"2026-04-29T17:39:11.159Z"} +{"cache_key":"87b12e832562d2c22c098cc5a101fc6ddc88acfb7a1a91cd3ca6582855c22160","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"ar","translated":"تم التحديث خلال آخر {count} دقيقة.","updated_at":"2026-05-04T07:16:37.779Z"} {"cache_key":"87c906d74357fde0ea44bb95406acb07ada29430720094905be5039c5ebc23c8","model":"gpt-5.5","provider":"openai","segment_id":"languages.jaJP","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"日本語 (Japanese)","text_hash":"6da707c478f800a1b4c4fb6eac67f61d1046ecf2f3f297b1785ceb926e69c559","tgt_lang":"ar","translated":"日本語 (اليابانية)","updated_at":"2026-04-29T17:39:41.587Z"} {"cache_key":"8814497f7d48fbcabd66e63cc96524d9edfdd1e17fb24d7a3c557a9fc1bd48ee","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.content","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Content","text_hash":"47bd29075f8b8019f0beec6d86beda7c9bf67aaf05053dcbe0b3bcb63968517f","tgt_lang":"ar","translated":"المحتوى","updated_at":"2026-04-29T19:26:31.017Z"} {"cache_key":"8826f0c4d5ecef2794ce4277c9fd264366d1c98d5dbf1ab37c6ac6413ac76a69","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.searchJobs","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Search jobs","text_hash":"989ecb5d07fd4c769ec4212085c63eab4b2bbede961979f8903fd98ed5c874d9","tgt_lang":"ar","translated":"البحث في المهام","updated_at":"2026-04-29T17:39:51.444Z"} @@ -857,7 +858,6 @@ {"cache_key":"db2e1aada64090da62bbd47ed321af80af1b079496895e5caba16b6b16b4a264","model":"gpt-5.5","provider":"openai","segment_id":"agents.tabs.files","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Files","text_hash":"abc7e9892806b047b4d4786b3685285543f76ca314c4c76246d5f6544c7856c9","tgt_lang":"ar","translated":"الملفات","updated_at":"2026-04-29T19:26:14.154Z"} {"cache_key":"db45515210ec5f3d28b21bd5316130d482ef0b37519e1bf21eae93db0b56cf2f","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.advanced.summaryFromDailyLog","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"from daily log","text_hash":"59fca1391a37fc29f10922b2793abf2505ab02e7667d0d5afccb99475662f0aa","tgt_lang":"ar","translated":"من السجل اليومي","updated_at":"2026-04-29T17:38:16.973Z"} {"cache_key":"db5e1c913e7e339221cc6a3e00b009424d48fbce228df4a362b247cd6a8f466f","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.topAgents","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Top Agents","text_hash":"078a5214ffb35216e4af2b069b54f9525725f6f35c16a1ab1a9f7445f1f4e6ea","tgt_lang":"ar","translated":"أهم الوكلاء","updated_at":"2026-04-29T17:39:11.160Z"} -{"cache_key":"db94b2ba3e4c63f631d66ae9723139577007d6fb349c9d5db12e6b9f6ce5992b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"ar","translated":"تم التحديث خلال آخر N دقيقة.","updated_at":"2026-05-04T07:16:37.779Z"} {"cache_key":"dbb3dea3961dbcad4a322d3678671eba936f49610bea50333830deec2b2a95a7","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.toolResult","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Tool result","text_hash":"9bb620efa692f707a302a5f42464015a54c20843e2f76f18a1542626b886bb91","tgt_lang":"ar","translated":"نتيجة الأداة","updated_at":"2026-04-29T17:39:29.944Z"} {"cache_key":"dbb7dcb9a46334b5d97c772e3d3ece94fb7bb81418e88e47f0cb30d2baff9371","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.prompt","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"prompt","text_hash":"cf07194ee232eb531e15f690000d19846dea69cf05504782658afcfacb9228a2","tgt_lang":"ar","translated":"المطالبة","updated_at":"2026-04-29T17:39:11.159Z"} {"cache_key":"dc88ac1cd0ff6ab1cbba348c7ebc889d0b7e4356921e320e4c8a1c22199e12da","model":"gpt-5.5","provider":"openai","segment_id":"execApproval.expired","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"expired","text_hash":"fa64ea1e82e1206f828ab2a02917c7e92accb98e3b95881a1b4ad52b914b66e3","tgt_lang":"ar","translated":"منتهية الصلاحية","updated_at":"2026-04-29T19:26:37.887Z"} diff --git a/ui/src/i18n/.i18n/de.meta.json b/ui/src/i18n/.i18n/de.meta.json index ebbf8e50548..99f2ae72a18 100644 --- a/ui/src/i18n/.i18n/de.meta.json +++ b/ui/src/i18n/.i18n/de.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:14:17.720Z", + "generatedAt": "2026-05-04T07:26:57.765Z", "locale": "de", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/de.tm.jsonl b/ui/src/i18n/.i18n/de.tm.jsonl index 3c516ba2419..08386f0a483 100644 --- a/ui/src/i18n/.i18n/de.tm.jsonl +++ b/ui/src/i18n/.i18n/de.tm.jsonl @@ -76,7 +76,6 @@ {"cache_key":"186e64f5c7705d37d6753c6235c2509a4e0f67fea0c9c04b9aaec80237f0f057","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.phrases.weavingShortTerm","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"weaving short-term into long-term…","text_hash":"1d64d672d34876489dc3885e05677abcae21d06bfa1d25ed87001721e441bd12","tgt_lang":"de","translated":"Kurzfristiges wird ins Langfristige eingewebt…","updated_at":"2026-04-06T02:48:28.029Z"} {"cache_key":"1967ab05b305f0272c9bdf6795a64b4edb4e63a3a6b9c9f08d96e8f58ce51376","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.modelHelp","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Start typing to pick a known model, or enter a custom one.","text_hash":"6ebac6c51e0da79d2ad76fe3d1395dff0c7a51ec7aa0d6b39ac38b0ba9fd8724","tgt_lang":"de","translated":"Beginne zu tippen, um ein bekanntes Modell auszuwählen, oder gib ein benutzerdefiniertes ein.","updated_at":"2026-04-05T17:12:53.251Z"} {"cache_key":"19c10ff4e2d5e344c5bfa137df8625ca428e891207ec4dcb8726cd04a9543f41","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.expression","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Expression","text_hash":"c67415bcff328a59fd399e2a7ca9691e0044192fb7480ae501644339965d046d","tgt_lang":"de","translated":"Ausdruck","updated_at":"2026-04-05T17:12:43.392Z"} -{"cache_key":"19d919555152a508770de8c998a86cf8d871e5b97bc3a2753c99838eb2868127","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"de","translated":"In den letzten N Minuten aktualisiert.","updated_at":"2026-05-04T07:14:17.568Z"} {"cache_key":"19f83759cc39880e5e66e82c01e8fabe0e0c169938b8a5d382b83036f892d0b8","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.staggerPlaceholder","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"30","text_hash":"624b60c58c9d8bfb6ff1886c2fd605d2adeb6ea4da576068201b6c6958ce93f4","tgt_lang":"de","translated":"30","updated_at":"2026-04-06T02:59:31.642Z"} {"cache_key":"1a48a1b97bec57ed23694dee36adeb0f42ad5c0c3b76c32e54a10e54b60d0053","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.phrases.defragmentingMindPalace","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"defragmenting the mind palace…","text_hash":"72b86d992fabe3f675a0ec75cf83dc5f7db1f0abc80faff08117748445f70ed2","tgt_lang":"de","translated":"der Gedächtnispalast wird defragmentiert…","updated_at":"2026-04-06T02:48:28.029Z"} {"cache_key":"1a4f4e937fd87325160cb8d340e93cfbbccf5b861e9dd1e46153f1df0cd270e7","model":"gpt-5.4","provider":"openai","segment_id":"agentTools.connected","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Connected","text_hash":"22965568d22a14ee17af055d2870b50afcfe9fd94a83eec3196e266932297bb2","tgt_lang":"de","translated":"Verbunden","updated_at":"2026-04-06T02:48:23.494Z"} @@ -522,6 +521,7 @@ {"cache_key":"a547d4ebf6532336ed9e5b42a337b1ff2b268d14af76e5b6dd5a92a39fce6f7b","model":"gpt-5.4","provider":"openai","segment_id":"common.call","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Call","text_hash":"d6e645b7d2b2da646d44130464143171935ffa47558b4e36c05df175de7197ba","tgt_lang":"de","translated":"Anrufen","updated_at":"2026-04-06T02:47:24.182Z"} {"cache_key":"a6523675639f0de5a25efc80fb22d4e29911e0046cb2d30c4d23959847affbfc","model":"gpt-5.4","provider":"openai","segment_id":"usage.query.apply","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Filter (client-side)","text_hash":"77e09b6867cffeb5bdf24c22b34dfe5eca471bf52337bfc8c372e3cead606eae","tgt_lang":"de","translated":"Filtern (clientseitig)","updated_at":"2026-04-05T17:11:35.181Z"} {"cache_key":"a6ac91d786ae4faacfc644a980e1cf0ef6f84305b1aaf9e75a2fb2aca1db0da4","model":"gpt-5.4","provider":"openai","segment_id":"overview.palette.placeholder","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Type a command…","text_hash":"96489e83623d94011df336e2a4d1a62eaf2b14913aecb4845bb11e13d88733e7","tgt_lang":"de","translated":"Befehl eingeben…","updated_at":"2026-04-05T17:10:45.990Z"} +{"cache_key":"a6d620e25df46cf5a4499d793c32673db33182e434ff18aa09d3705fff8148c7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"de","translated":"In den letzten {count} Minuten aktualisiert.","updated_at":"2026-05-04T07:14:17.568Z"} {"cache_key":"a6dffa0416f73c436619e8ab235d30b09139fcb5f57730d08cbe90aa091a2cd7","model":"gpt-5.4","provider":"openai","segment_id":"cron.summary.yes","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Yes","text_hash":"85a39ab345d672ff8ca9b9c6876f3adcacf45ee7c1e2dbd2408fd338bd55e07e","tgt_lang":"de","translated":"Ja","updated_at":"2026-04-05T17:12:05.893Z"} {"cache_key":"a7520d22d43e0b7bf4a170ca2bbeb741899a775e397c574393ddf01782b9ca0d","model":"gpt-5.4","provider":"openai","segment_id":"cron.jobState.last","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Last","text_hash":"eb970eb0951c6cdeac1ec0cc723fc91e30b0c26ee6f3b5ee0e574db7f487dc55","tgt_lang":"de","translated":"Letzte","updated_at":"2026-04-05T17:13:00.438Z"} {"cache_key":"a771fbe351fd3c5f0bd23aeab8ccbad8a61171b2e7eaf9909626928c75a5301c","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.exactTimingHelp","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Run on exact cron boundaries with no spread.","text_hash":"9703f65e118e6804dabd58b8a31e34c994208f511a16eb699173991d6a041b57","tgt_lang":"de","translated":"Wird exakt zu den Cron-Grenzen ohne Streuung ausgeführt.","updated_at":"2026-04-05T17:12:53.251Z"} diff --git a/ui/src/i18n/.i18n/es.meta.json b/ui/src/i18n/.i18n/es.meta.json index c892761c5a6..8f0fbd7be8a 100644 --- a/ui/src/i18n/.i18n/es.meta.json +++ b/ui/src/i18n/.i18n/es.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:15:25.305Z", + "generatedAt": "2026-05-04T07:26:58.121Z", "locale": "es", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/es.tm.jsonl b/ui/src/i18n/.i18n/es.tm.jsonl index b7a23bd41fa..2c9cbfa7412 100644 --- a/ui/src/i18n/.i18n/es.tm.jsonl +++ b/ui/src/i18n/.i18n/es.tm.jsonl @@ -434,6 +434,7 @@ {"cache_key":"bd2dd0f20661923f2087f0311481aecc629886efbad43ef1360f932bc9de05dd","model":"gpt-5.4","provider":"openai","segment_id":"tabs.automation","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"es","translated":"Automatización","updated_at":"2026-04-05T17:12:01.459Z"} {"cache_key":"be282f3ee76fa824b4b3208dabb506f937e75da205340d8f8f809448bda51b9d","model":"gpt-5.4","provider":"openai","segment_id":"usage.filters.timeZone","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Time zone","text_hash":"b9fe1464783e1c0d3a12dbde2686e883482a4fa03f33351af3e576d7a9d32fe0","tgt_lang":"es","translated":"Zona horaria","updated_at":"2026-04-05T17:12:08.307Z"} {"cache_key":"be56542829c14b1befee5ba1e0dd0f56be3dec53fd23731997b5c2199baed6c7","model":"gpt-5.4","provider":"openai","segment_id":"usage.details.noMessages","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"No messages","text_hash":"a06faf2668c28d0b26a3d89a7cb8751f4d952bc6f38ba9e0c202218269bdc659","tgt_lang":"es","translated":"No hay mensajes","updated_at":"2026-04-05T17:12:54.430Z"} +{"cache_key":"be7adfd7b2c68e262375e296ce7e3c231aba919c80aad180646b23d8ef1be014","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"es","translated":"Actualizadas en los últimos {count} minutos.","updated_at":"2026-05-04T07:15:25.153Z"} {"cache_key":"bebb1fc5049f715a5781d00ce46635c968dafc9791d127a68230d7784e15a29a","model":"gpt-5.4","provider":"openai","segment_id":"languages.id","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Bahasa Indonesia (Indonesian)","text_hash":"5c9f82fd90a4d39be1781670006d9cb199f5f2be0abd06d73d536dbc65f2b9d4","tgt_lang":"es","translated":"Bahasa Indonesia (indonesio)","updated_at":"2026-04-05T17:12:58.558Z"} {"cache_key":"bfa1cc1654bac15b79c4f992f83006f4830e883970e590de99db175c785bafa3","model":"gpt-5.4","provider":"openai","segment_id":"usage.filters.timeZoneLocal","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Local","text_hash":"8c31e6e7223097e2e4847773c47a4efab6aaf79deeecc92a7759891c74976dde","tgt_lang":"es","translated":"Local","updated_at":"2026-04-06T02:59:34.859Z"} {"cache_key":"bfba64750c2aa019859c2dd3a266236d1ae1c8954e8a9832971e12d0d3edd423","model":"gpt-5.4","provider":"openai","segment_id":"overview.quickActions.newSession","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"New Session","text_hash":"fc0bb85f3867f1df067d69d6446c6df5b8bdd4caf25718a67bdc68c9e079bd5f","tgt_lang":"es","translated":"Nueva sesión","updated_at":"2026-04-05T17:12:04.947Z"} @@ -493,7 +494,6 @@ {"cache_key":"d20e6177c43d74e40b73afe3cb75cb1d44150cf2badbaf1d684aa6884408ca32","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.customOption","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{value} (custom)","text_hash":"3c72a6f7c232c01c3d59e562bc0423a5fe43ef909dbd539a3779d2c0961cebfd","tgt_lang":"es","translated":"{value} (personalizado)","updated_at":"2026-04-29T20:13:26.402Z"} {"cache_key":"d275cca20a795265f4ac5c850a774804ab1d756c280b99b1dba255e253a8cdc2","model":"gpt-5.4","provider":"openai","segment_id":"overview.access.passwordPlaceholder","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"system or shared password","text_hash":"34a9738798b1867d236d9f47ade0fb12cb06f64709c78661289f169c94336e36","tgt_lang":"es","translated":"contraseña del sistema o compartida","updated_at":"2026-04-20T06:26:23.812Z"} {"cache_key":"d27fc7dbdab50c8a679d4ad4d4449c7b151ce0cadea109481e2e2cdbe5dece0c","model":"gpt-5.5","provider":"openai","segment_id":"chat.commandPaletteTitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Search or jump to… (⌘K)","text_hash":"3116c088ff7d8d4e10c5a0e27fd960bc1cb60a21ac94153f7290e4e0ab9ac22c","tgt_lang":"es","translated":"Buscar o ir a… (⌘K)","updated_at":"2026-04-29T20:13:29.857Z"} -{"cache_key":"d2f19138951a0b6eabf7f5495d216b2b50cbf1f955ccdd788a891db4cb1baad3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"es","translated":"Actualizadas en los últimos N minutos.","updated_at":"2026-05-04T07:15:25.153Z"} {"cache_key":"d3474f125cd0ba017ae1cb444f77db33759d3996dfaef5f993cc922e3b1662a1","model":"gpt-5.4","provider":"openai","segment_id":"usage.mosaic.eightPm","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"8pm","text_hash":"232df857db5e72521b783719e674c41bce48738283c637b44ed2a80fa81ec56c","tgt_lang":"es","translated":"8 p. m.","updated_at":"2026-04-05T17:12:54.430Z"} {"cache_key":"d568b08679f46bb0237e2d845ad7c7cb6f19a3436a7077542764c580e2667f00","model":"gpt-5.4","provider":"openai","segment_id":"usage.mosaic.fourAm","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"4am","text_hash":"c2a15a1684ec7e544681bcb5cc60f3c192fa87ed733d0a4b6b975db88724a9fb","tgt_lang":"es","translated":"4 a. m.","updated_at":"2026-04-05T17:12:54.430Z"} {"cache_key":"d56cecd4b0584d94fa42e9250403d2ae8c1d3285c65f7d50ed723dbfe2decead","model":"gpt-5.4","provider":"openai","segment_id":"common.na","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"n/a","text_hash":"a683c5c5349f6f7fb903ba8a9e7e55d0ba1b8f03579f95be83f4954c33e81098","tgt_lang":"es","translated":"n/d","updated_at":"2026-04-05T17:12:01.459Z"} diff --git a/ui/src/i18n/.i18n/fa.meta.json b/ui/src/i18n/.i18n/fa.meta.json index af9d4b229f2..b7ecc387157 100644 --- a/ui/src/i18n/.i18n/fa.meta.json +++ b/ui/src/i18n/.i18n/fa.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:19:05.383Z", + "generatedAt": "2026-05-04T07:44:21.069Z", "locale": "fa", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/fa.tm.jsonl b/ui/src/i18n/.i18n/fa.tm.jsonl index 8e3d346bcca..33166e1bc5d 100644 --- a/ui/src/i18n/.i18n/fa.tm.jsonl +++ b/ui/src/i18n/.i18n/fa.tm.jsonl @@ -114,7 +114,6 @@ {"cache_key":"1c8e141e908c337f80e4669dd3e7d063a591b2d4c74e0b25ed4ce666ef63e569","model":"gpt-5.5","provider":"openai","segment_id":"common.relink","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Relink","text_hash":"6c2050caec79d2e5993192ad10a22ec6347ab647a1a7dfd9e797e64737f3f295","tgt_lang":"fa","translated":"پیوند مجدد","updated_at":"2026-04-29T17:41:14.004Z"} {"cache_key":"1d06bb3a56ac24d0472090086ebbf2c4fe425c93bc4867af9d1f889acee5873a","model":"gpt-5.5","provider":"openai","segment_id":"overview.cards.modelAuthOk","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} ok","text_hash":"40dbcccbec9af7050dad66aa98388376a2bd3c6a2ebaf8eb53d72510d6104801","tgt_lang":"fa","translated":"{count} سالم","updated_at":"2026-04-29T17:42:27.724Z"} {"cache_key":"1d1ade18b6c14f8c47e9f8bafaab740a964fcf10e05dc3048edbeef7cff81007","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.errorsHint","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Total message and tool errors in range.","text_hash":"d99a4b10fb87bda650577c36cec57f531433cbee6046ebb8e614af9e2fffce28","tgt_lang":"fa","translated":"مجموع خطاهای پیام و ابزار در بازه.","updated_at":"2026-04-29T17:43:47.268Z"} -{"cache_key":"1d2b5123f2ee644728076d7390ea4d8465b95d7822566f6c54d743bb0cb812db","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"fa","translated":"در N دقیقهٔ گذشته بهروزرسانی شده است.","updated_at":"2026-05-04T07:19:05.230Z"} {"cache_key":"1d545d9b0715e3dce98c229dc6896ff25931beaf5364549d57827784cf2981ac","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.coreFilesSubtitle","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Bootstrap persona, identity, and tool guidance.","text_hash":"d75ad947c2751bddf5612450c6bf1d53c8ae3d8fe51dc9479032eb677d081662","tgt_lang":"fa","translated":"شخصیت اولیه، هویت و راهنمای ابزارها.","updated_at":"2026-04-29T19:28:51.004Z"} {"cache_key":"1d7fb910b957873b4980233feb70de844da8c907fc4356059b51ad241725be9e","model":"gpt-5.5","provider":"openai","segment_id":"tabs.cron","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Cron Jobs","text_hash":"043d5c96a8cd2805d6743faef29eaa7deb83ff3ed45f8cd42df1b75c257f8d65","tgt_lang":"fa","translated":"کارهای Cron","updated_at":"2026-04-29T17:41:42.715Z"} {"cache_key":"1d7ff1b96387d6c1afd7e4bff9de2b9dfeb4a9ad4148d63c15728256711e802d","model":"gpt-5.5","provider":"openai","segment_id":"languages.ko","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"한국어 (Korean)","text_hash":"30f959f34501d524b06cf98b3711cdffea10a6479a316cf2c030362e8d274740","tgt_lang":"fa","translated":"한국어 (کرهای)","updated_at":"2026-04-29T17:44:38.057Z"} @@ -777,6 +776,7 @@ {"cache_key":"c8b48b22dec0ec0eef79960aa831e3481950669b70d89047e64f2df998a2d017","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.user","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"user","text_hash":"04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb","tgt_lang":"fa","translated":"کاربر","updated_at":"2026-04-29T17:43:47.268Z"} {"cache_key":"c8ef818b3fdf3ce18ca4d75e4b306485e69f982caa368b576210101cb72c6c89","model":"gpt-5.5","provider":"openai","segment_id":"debug.health","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Health","text_hash":"55898449eb74fb2e348d13e5a5d84ab019bb87ea92687b50b3d3302eb409b784","tgt_lang":"fa","translated":"سلامت","updated_at":"2026-04-29T19:28:57.124Z"} {"cache_key":"c963c7d4d5849bf37e0abff5817f630e6a5e6bb97c5b11ffb0967f5062149925","model":"gpt-5.5","provider":"openai","segment_id":"cron.runs.selectedJob","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Selected job","text_hash":"e8262f191cf46042f768de21dc32acfec69dea069022bb4a6ad55f62752556f8","tgt_lang":"fa","translated":"کار انتخابشده","updated_at":"2026-04-29T17:44:48.591Z"} +{"cache_key":"c9a15ff8699d6a81dd8f5b8c43807a42a0fd07d01b23d5e6e097ebfed5585fc4","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"fa","translated":"در {count} دقیقهٔ گذشته بهروزرسانی شده است.","updated_at":"2026-05-04T07:19:05.230Z"} {"cache_key":"ca101c355edf68f84ec555d7a74c017f1e011a930a754cdf4640b1b05020d608","model":"gpt-5.5","provider":"openai","segment_id":"common.none","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"none","text_hash":"140bedbf9c3f6d56a9846d2ba7088798683f4da0c248231336e6a05679e4fdfe","tgt_lang":"fa","translated":"هیچکدام","updated_at":"2026-04-29T20:17:19.091Z"} {"cache_key":"ca81392f3d094bb70b7d98cbe70e5b7a70dab2727c6c44495b9fb2234e92adfd","model":"gpt-5.5","provider":"openai","segment_id":"overview.access.togglePasswordVisibility","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Toggle password visibility","text_hash":"1016c07b0f58d365790cc799fb215afd92fde1aeb5ac47cd17260e327465b2d6","tgt_lang":"fa","translated":"تغییر نمایش گذرواژه","updated_at":"2026-04-29T17:41:58.639Z"} {"cache_key":"ca913c0daa0dd1fdb1b014a8ae04cbc9b10790e1a34d979d51672c5b4ea75542","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.collapsePreview","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Collapse preview","text_hash":"90e8d06c0309d797a91911f446a0d6218d659c7c8769e2ab4034bc6e0c4c008d","tgt_lang":"fa","translated":"جمع کردن پیشنمایش","updated_at":"2026-04-29T19:28:51.004Z"} diff --git a/ui/src/i18n/.i18n/fr.meta.json b/ui/src/i18n/.i18n/fr.meta.json index 7770729d271..0138b86f9df 100644 --- a/ui/src/i18n/.i18n/fr.meta.json +++ b/ui/src/i18n/.i18n/fr.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:15:37.965Z", + "generatedAt": "2026-05-04T07:26:59.152Z", "locale": "fr", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/fr.tm.jsonl b/ui/src/i18n/.i18n/fr.tm.jsonl index d8cb9df8d07..eccb2d1ac35 100644 --- a/ui/src/i18n/.i18n/fr.tm.jsonl +++ b/ui/src/i18n/.i18n/fr.tm.jsonl @@ -269,7 +269,6 @@ {"cache_key":"4c5c87f5e6ee9cfbc6eecf43892784caae091eca5403113b6e3197c2028f1380","model":"gpt-5.4","provider":"openai","segment_id":"usage.mosaic.fourAm","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"4am","text_hash":"c2a15a1684ec7e544681bcb5cc60f3c192fa87ed733d0a4b6b975db88724a9fb","tgt_lang":"fr","translated":"4 h","updated_at":"2026-04-05T17:14:34.186Z"} {"cache_key":"4d0b5f009b88933d1cb5af49225b88ae7d209f41995886dd671025e9da67eb8a","model":"gpt-5.4","provider":"openai","segment_id":"common.reloadConfig","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Reload Config","text_hash":"48e6315352561c36be84097326fbb3558b4c2fa3fc4f833402d32040ccb640f7","tgt_lang":"fr","translated":"Recharger la config","updated_at":"2026-04-06T02:49:37.962Z"} {"cache_key":"4d26ed4f667406ac8ab8dd3c29ea6ceb2813e40095f4e56fa5b5cfad6a32de3f","model":"gpt-5.4","provider":"openai","segment_id":"cron.runs.status","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Status","text_hash":"920e413c7d411b61ef3e8c63b1cb6ad058d5f95f8b481dbafe60248387d8c355","tgt_lang":"fr","translated":"Statut","updated_at":"2026-04-05T17:15:40.832Z"} -{"cache_key":"4d36de99d636d0c44cba83c0bf21ddf721d8b69e1d1df2b70bbbd4d26e1140c6","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"fr","translated":"Mis à jour au cours des N dernières minutes.","updated_at":"2026-05-04T07:15:37.813Z"} {"cache_key":"4d5d6aab0201e7d94f684cb480cd7102fdbe60a40fef9ea9013a32931b351628","model":"gpt-5.4","provider":"openai","segment_id":"cron.runs.loadMore","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Load more runs","text_hash":"627fcc156ad8a34716755bb53feca47c761b91b0edf23b93571d935cb3f2d02b","tgt_lang":"fr","translated":"Charger plus d’exécutions","updated_at":"2026-04-05T17:15:40.832Z"} {"cache_key":"4d6481d8606fb94bff64f7ab604b4f80a2a4a5210ed9841e572f782bb3d1beb6","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.hours","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Hours","text_hash":"21e8492938abc179410c21f3598f141c4c59a8bf2d3b4e475b7d83e10adfc00f","tgt_lang":"fr","translated":"Heures","updated_at":"2026-04-05T17:15:46.853Z"} {"cache_key":"4d6ee9a10c33ac5ecc2fede40be878f0836722afbf2e2408c7ceb71227e75697","model":"gpt-5.4","provider":"openai","segment_id":"channels.nostr.editProfile","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Edit Profile","text_hash":"fec2ac0f4cf167e35facd4d2038d15e8d60cbd604d7769635012a48a87363f44","tgt_lang":"fr","translated":"Modifier le profil","updated_at":"2026-04-06T02:49:41.314Z"} @@ -830,6 +829,7 @@ {"cache_key":"f4a6416b416deae8e77be595aab5c1d09aa7c908607ca552a3d2240daf65881a","model":"gpt-5.4","provider":"openai","segment_id":"cron.runs.scope","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Scope","text_hash":"b073f6c68ef8721107fd9815b19b2c35ec111d526b75c2123d1111ba64424000","tgt_lang":"fr","translated":"Portée","updated_at":"2026-04-05T17:15:36.630Z"} {"cache_key":"f53123f8620ff14b55a189012693155688bc80201a7f8dbc542586471c333cd3","model":"gpt-5.4","provider":"openai","segment_id":"common.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-04-06T02:49:41.314Z"} {"cache_key":"f587fdb319aa4b0ff115902d4c4207d5ac1e6f7d4c72d1ec005bc230e379899b","model":"gpt-5.4","provider":"openai","segment_id":"common.unsavedChanges","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"You have unsaved changes","text_hash":"a4b17bc7db59e76b073a344d84ce06457042dde8c293cf91b4a994db2de58da7","tgt_lang":"fr","translated":"Vous avez des modifications non enregistrées","updated_at":"2026-04-06T02:49:41.314Z"} +{"cache_key":"f61ac080ffca2183d666cf12c0f022a358eec60e49f49739301b76c9a214ee36","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"fr","translated":"Mis à jour au cours des {count} dernières minutes.","updated_at":"2026-05-04T07:15:37.813Z"} {"cache_key":"f6330b87a3bca7e526882ca439f6d458fc4ff8223ea9fd5d03c5768466444a67","model":"gpt-5.4","provider":"openai","segment_id":"languages.fr","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Français (French)","text_hash":"51d624360ae74f9507dda57a5b639a12ee70571f23dd7d954e7c53bdd85372c8","tgt_lang":"fr","translated":"Français (français)","updated_at":"2026-04-05T17:15:33.911Z"} {"cache_key":"f6587a1259a7af867ee3007e1aada3d4c02d27758e7870bbb091630bc8933f2f","model":"gpt-5.4","provider":"openai","segment_id":"nav.collapse","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Collapse sidebar","text_hash":"aab31cde23ba9783050a754575b80c05e0e799b1542990b24b4b4bde2327e37e","tgt_lang":"fr","translated":"Réduire la barre latérale","updated_at":"2026-04-05T17:13:51.251Z"} {"cache_key":"f6ff21e009ea9f0f04340c9797fde1acd8d2235286ba2df529dca0ed21674157","model":"gpt-5.4","provider":"openai","segment_id":"common.showAdvanced","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Show Advanced","text_hash":"365075d1bf3ed18878ba0bb50360278b7eaa5973d32ed92fa1544238c09254cb","tgt_lang":"fr","translated":"Afficher les options avancées","updated_at":"2026-04-06T02:49:41.314Z"} diff --git a/ui/src/i18n/.i18n/id.meta.json b/ui/src/i18n/.i18n/id.meta.json index 1fbb4dcab94..973db59571c 100644 --- a/ui/src/i18n/.i18n/id.meta.json +++ b/ui/src/i18n/.i18n/id.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:17:48.480Z", + "generatedAt": "2026-05-04T07:44:19.311Z", "locale": "id", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/id.tm.jsonl b/ui/src/i18n/.i18n/id.tm.jsonl index 983bf38fb03..8a6477735b3 100644 --- a/ui/src/i18n/.i18n/id.tm.jsonl +++ b/ui/src/i18n/.i18n/id.tm.jsonl @@ -258,6 +258,7 @@ {"cache_key":"4a3f0e7e125c2ad6b510a4628f1418a7e3f86e29083a578e92baa73d918e8802","model":"gpt-5.4","provider":"openai","segment_id":"instances.toggleHostVisibility","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Toggle host visibility","text_hash":"dd0188424f6a0434d4af848b7462f4d12da05800bfc24d82cb2c0d7e443b657b","tgt_lang":"id","translated":"Alihkan visibilitas host","updated_at":"2026-04-06T02:50:52.064Z"} {"cache_key":"4a7addb31bd281b24d228e559e98b8a20e2bb2d00f8b9a1109744524ec6cc3e5","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.required","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Required","text_hash":"4850b174b713d88cfc63de107830d5388929020e78abc91fc19bba7a6821625f","tgt_lang":"id","translated":"Wajib","updated_at":"2026-04-05T17:16:04.514Z"} {"cache_key":"4aaec8cd3b497c1bcc8cb5cf49b82ef249405afef3dcdace4df9723192cdcc19","model":"gpt-5.4","provider":"openai","segment_id":"cron.jobState.last","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Last","text_hash":"eb970eb0951c6cdeac1ec0cc723fc91e30b0c26ee6f3b5ee0e574db7f487dc55","tgt_lang":"id","translated":"Terakhir","updated_at":"2026-04-05T17:16:23.016Z"} +{"cache_key":"4b334e4c9316c61e22d31ef3c5186792a5e9d6a63e40ff67c64ef6d47b45a95e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"id","translated":"Diperbarui dalam {count} menit terakhir.","updated_at":"2026-05-04T07:17:48.328Z"} {"cache_key":"4b54aea412e02ff625edbe431c15ffececae28982a1c4a2dd62a6c527e78749d","model":"gpt-5.5","provider":"openai","segment_id":"common.none","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"none","text_hash":"140bedbf9c3f6d56a9846d2ba7088798683f4da0c248231336e6a05679e4fdfe","tgt_lang":"id","translated":"tidak ada","updated_at":"2026-04-29T20:16:00.609Z"} {"cache_key":"4b8c0080994e02fe518c61f2046ea4f0713c630bd6a048d5d62ef2d059e73072","model":"gpt-5.4","provider":"openai","segment_id":"cron.runs.noMatching","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"No matching runs.","text_hash":"567dd6add9cc8e3c398162d00493ca9f17fcd61ca079c5d8650f02d3f8ee0410","tgt_lang":"id","translated":"Tidak ada proses yang cocok.","updated_at":"2026-04-05T17:16:01.471Z"} {"cache_key":"4ba08285fc8f46d34932c7a7872a2eefd77ea11b52be39210547f07d99aef4c0","model":"gpt-5.5","provider":"openai","segment_id":"chat.commandPaletteTitle","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Search or jump to… (⌘K)","text_hash":"3116c088ff7d8d4e10c5a0e27fd960bc1cb60a21ac94153f7290e4e0ab9ac22c","tgt_lang":"id","translated":"Cari atau lompat ke… (⌘K)","updated_at":"2026-04-29T20:16:07.309Z"} @@ -603,7 +604,6 @@ {"cache_key":"b587cd454d4acd129f0789c813b0530dc78b954f626a652b29062ae6ddcdbe54","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.namePlaceholder","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"e.g., Morning inbox check","text_hash":"149ef2da53b9dbcd4cb688e9d86fdb3780f50a88886ae841004fa3993ccd4e9f","tgt_lang":"id","translated":"mis., Pemeriksaan kotak masuk pagi","updated_at":"2026-04-29T20:16:15.616Z"} {"cache_key":"b627e8d97b8ea2edcb42e0fc65c665bc5649ee51870b00b92cf64ce8e2b87ae8","model":"gpt-5.4","provider":"openai","segment_id":"instances.subtitle","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Presence beacons from the gateway and clients.","text_hash":"5349f6c160fabe02b9b0d3065e8cd995704de9fcb2894945af4660d9cb35f666","tgt_lang":"id","translated":"Beacon kehadiran dari gateway dan klien.","updated_at":"2026-04-06T02:50:52.064Z"} {"cache_key":"b671d266afdd9e6af65d745951be63e33512e15cf673fd9d90918112143dce0a","model":"gpt-5.4","provider":"openai","segment_id":"usage.sessions.noRecent","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"No recent sessions","text_hash":"100ac08064a6d5867a400a56b2949f9de3f6da4602a99461ee3a300c20273c1b","tgt_lang":"id","translated":"Tidak ada sesi terbaru","updated_at":"2026-04-05T17:15:40.941Z"} -{"cache_key":"b6793cfa809550ae22f1ec1fe2c04887a042eacbf35937dc6f2ce4e4ecbc5061","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"id","translated":"Diperbarui dalam N menit terakhir.","updated_at":"2026-05-04T07:17:48.328Z"} {"cache_key":"b689b1f92f734091adc37fb5d7b133bb62a7c3152c1fd9f5e823bf930c354ebb","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.phrases.indexingDay","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"softly indexing the day…","text_hash":"ff48bcdd6ad07670194006da8e1f7c90138be97b7e6f46fb37119baadb7a2455","tgt_lang":"id","translated":"mengindeks hari ini dengan lembut…","updated_at":"2026-04-06T02:51:04.169Z"} {"cache_key":"b6b30b878e20251b64bba26e360563cfbbf87d8314e424ef3d5969ffbabcda04","model":"gpt-5.4","provider":"openai","segment_id":"usage.details.noContextData","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"No context data","text_hash":"b47c4d5f0e9832bb8f16a4025296a6c41d7aaa7200a07746b6e35359dc464f28","tgt_lang":"id","translated":"Tidak ada data konteks","updated_at":"2026-04-05T17:15:46.359Z"} {"cache_key":"b6d03776279fbea4ee13e0238e948af474e923930b0f54ccaf604e255a91c100","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.stream","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"stream","text_hash":"dca83e717b1f64eb141057a7415a330ad1361f51703efa2e4776f40047898a04","tgt_lang":"id","translated":"stream","updated_at":"2026-04-29T20:16:03.576Z"} diff --git a/ui/src/i18n/.i18n/it.meta.json b/ui/src/i18n/.i18n/it.meta.json index 03d2d5f4d08..cdc905e18d8 100644 --- a/ui/src/i18n/.i18n/it.meta.json +++ b/ui/src/i18n/.i18n/it.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:16:44.691Z", + "generatedAt": "2026-05-04T07:26:59.895Z", "locale": "it", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/it.tm.jsonl b/ui/src/i18n/.i18n/it.tm.jsonl index adfa53316b6..a7c754cb0d4 100644 --- a/ui/src/i18n/.i18n/it.tm.jsonl +++ b/ui/src/i18n/.i18n/it.tm.jsonl @@ -289,6 +289,7 @@ {"cache_key":"4cff963879b797b912a60f4a21c8a5b159e5daff580b82a0546ef110b9452bdd","model":"gpt-5.5","provider":"openai","segment_id":"usage.filters.agent","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Agent","text_hash":"11b39c93777e8f1f3983bdba7c72b22fe68cfea20c677e9de53e17cb7dbfb19f","tgt_lang":"it","translated":"Agente","updated_at":"2026-04-29T17:38:30.670Z"} {"cache_key":"4da4d7fb6631e95224a59e89e0624bf8b5de1b51cdf809156cd9c1dccbeee74a","model":"gpt-5.5","provider":"openai","segment_id":"agents.channels.connectedCount","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{connected}/{total} connected","text_hash":"6729df072a594588965877e7cd93c8bc996861680ea407de026e042f432778ce","tgt_lang":"it","translated":"{connected}/{total} connessi","updated_at":"2026-04-29T19:26:37.953Z"} {"cache_key":"4db16fc0bdc3b2d548469add03ee1ab895f6fbdc7c5b5fd0415b8c1a60046be4","model":"gpt-5.5","provider":"openai","segment_id":"usage.empty.featureTimeline","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Timeline drilldown","text_hash":"f02787b793baa84fe08d54066fbe5cf694a7bfd5c3d5fbe4216e50f14d771db4","tgt_lang":"it","translated":"Approfondimento timeline","updated_at":"2026-04-29T17:38:36.275Z"} +{"cache_key":"4dd37930ac7d5c6b562611b90c5ce1e9a7bc43188a22796a5a97334a3e36b0c7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"it","translated":"Aggiornate negli ultimi {count} minuti.","updated_at":"2026-05-04T07:16:44.539Z"} {"cache_key":"4e6326fde8f67d35daadf85f2192d9738cab3db5c5bb10f50044aa05391f668d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.minutesPlaceholder","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"min","text_hash":"1f6fa6f69d185e6086d04e7330361bf9001a3b8d0ce511171055dc34eb90c1c5","tgt_lang":"it","translated":"min","updated_at":"2026-04-29T20:14:41.034Z"} {"cache_key":"4e758f4e992f8e0ece366869d9ce53dc93281ccd152f441e3bc5b74fec5d376e","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.toolResult","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Tool result","text_hash":"9bb620efa692f707a302a5f42464015a54c20843e2f76f18a1542626b886bb91","tgt_lang":"it","translated":"Risultato strumento","updated_at":"2026-04-29T17:39:02.591Z"} {"cache_key":"4e7b84d4bf18fdeeb854ddc17c47dc52d7fba773b4d7a7977bb2c0d9f6823ecf","model":"gpt-5.5","provider":"openai","segment_id":"overview.snapshot.lastChannelsRefresh","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Last Channels Refresh","text_hash":"97a20d4f5b29914b8a08748cfc55d704a4d52ed948180cc90b7c1e06267c692f","tgt_lang":"it","translated":"Ultimo aggiornamento canali","updated_at":"2026-04-29T17:37:44.955Z"} @@ -857,7 +858,6 @@ {"cache_key":"e128507a65967458a35f53eb8332bc1dd489282bf0bffec1d3226dc5ca78c8c8","model":"gpt-5.5","provider":"openai","segment_id":"usage.breakdown.total","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Total","text_hash":"c9b3c38247f744e17dd26fda097d6a9ba9332586b6bdaa038bf8f313a863f2b8","tgt_lang":"it","translated":"Totale","updated_at":"2026-04-29T17:38:40.537Z"} {"cache_key":"e17c69778c3dc22c201cad77c741b0b8f62bf010ec932cee7cc8339adf7f1520","model":"gpt-5.5","provider":"openai","segment_id":"overview.access.passwordPlaceholder","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"system or shared password","text_hash":"34a9738798b1867d236d9f47ade0fb12cb06f64709c78661289f169c94336e36","tgt_lang":"it","translated":"password di sistema o condivisa","updated_at":"2026-04-29T17:37:41.072Z"} {"cache_key":"e18cf6741e577b0700d4008ba9f82af49c0cb8728f2856aec00bb4bbe530b4fe","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.channels","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Channels and settings.","text_hash":"c638a7924fc0fc1cf02059111dd7d81a01173c0b223b2b43526dbb37a9f5604e","tgt_lang":"it","translated":"Canali e impostazioni.","updated_at":"2026-04-29T17:37:35.636Z"} -{"cache_key":"e1c62b85487cc1f7c6d622b3b33e7cddbb964092a5517513822c34185d981555","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"it","translated":"Aggiornate negli ultimi N minuti.","updated_at":"2026-05-04T07:16:44.539Z"} {"cache_key":"e1d065a24e8a4c9912992731b7762ab630b3237bd2f74069161e9a1fd746794f","model":"gpt-5.5","provider":"openai","segment_id":"common.relink","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Relink","text_hash":"6c2050caec79d2e5993192ad10a22ec6347ab647a1a7dfd9e797e64737f3f295","tgt_lang":"it","translated":"Ricollega","updated_at":"2026-04-29T17:37:15.862Z"} {"cache_key":"e1f6c4bf2b4823d4bd4127e7f46de588442c5c8391e1f26451b5aa08f32720cc","model":"gpt-5.5","provider":"openai","segment_id":"common.yes","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Yes","text_hash":"85a39ab345d672ff8ca9b9c6876f3adcacf45ee7c1e2dbd2408fd338bd55e07e","tgt_lang":"it","translated":"Sì","updated_at":"2026-04-29T17:37:06.761Z"} {"cache_key":"e25684dd156b9928499d7fe393cbb7f7708de28a00db8ef36895989802294f54","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.scene.working","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Working…","text_hash":"5474eef8d0f179c707cf418e2bbb468c77cc24edc5e9f5f4e137e85e06a8eea0","tgt_lang":"it","translated":"Elaborazione…","updated_at":"2026-04-29T17:38:08.298Z"} diff --git a/ui/src/i18n/.i18n/ja-JP.meta.json b/ui/src/i18n/.i18n/ja-JP.meta.json index a3ed1bdcf74..e4a34da1341 100644 --- a/ui/src/i18n/.i18n/ja-JP.meta.json +++ b/ui/src/i18n/.i18n/ja-JP.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:15:33.763Z", + "generatedAt": "2026-05-04T07:26:58.455Z", "locale": "ja-JP", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/ja-JP.tm.jsonl b/ui/src/i18n/.i18n/ja-JP.tm.jsonl index 6bf160ea841..f38b6b1caf9 100644 --- a/ui/src/i18n/.i18n/ja-JP.tm.jsonl +++ b/ui/src/i18n/.i18n/ja-JP.tm.jsonl @@ -156,6 +156,7 @@ {"cache_key":"292e4c5c114f4afe567d2cddc91646812b09628773d8ede1431232ce14a94e28","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.advanced.shortTermDescription","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Current short-term candidates waiting to graduate into real memory.","text_hash":"0895c842efb140d4ebcd01bd1e976ecfa7e8d7318bd70d4ff1874976ba4729b8","tgt_lang":"ja-JP","translated":"実際の記憶に昇格するのを待っている現在の短期候補です。","updated_at":"2026-04-10T07:59:01.981Z"} {"cache_key":"294a5b573d0b89565b3b7145becda08a8d48d49eeabff278fc22e3913e3e24e5","model":"gpt-5.4","provider":"openai","segment_id":"languages.th","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"ไทย (Thai)","text_hash":"0339954ca7e472c2f007782682a76629a864d63d3e419430bb5f6c72c4c1c88d","tgt_lang":"ja-JP","translated":"ไทย (Thai)","updated_at":"2026-04-23T06:30:12.886Z"} {"cache_key":"29a1dccf67d6cedd5985cfdbba0f63dff7d4e0c52a44b25be99218eacf0e9557","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.advanced.emptyGrounded","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"No staged grounded replay entries right now.","text_hash":"3c85fa80872b7e5f27da121c22707aecb7dc74f627b2bcecff0373916fbf7270","tgt_lang":"ja-JP","translated":"現在、段階的な grounded replay エントリはありません。","updated_at":"2026-04-10T07:59:04.061Z"} +{"cache_key":"29acbf496f58e63239331cd56ef22ff01e3f109bef2139396a8b4b8a400dc83b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"ja-JP","translated":"過去 {count} 分以内に更新されました。","updated_at":"2026-05-04T07:15:33.611Z"} {"cache_key":"29b50ad1833ee9f6f266836be9e6cbc8efc7352da4c7b3efc616f5bd342a83e3","model":"gpt-5.4","provider":"openai","segment_id":"instances.showHosts","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Show hosts and IPs","text_hash":"fdc74f36ced00b110a24962032b06ee3f88f264688dab2b5dbdf4ccbccbcfa5b","tgt_lang":"ja-JP","translated":"ホストと IP を表示","updated_at":"2026-04-06T02:49:09.318Z"} {"cache_key":"29dc7e8b4c1e90617cf563ddc949363e8525e404403e1885c4ecabd0ee5db5c9","model":"gpt-5.4","provider":"openai","segment_id":"usage.sessions.descending","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Descending","text_hash":"79479a6c76d8416ab7839952a2f8222e350862464f4d02db13d8d8f9551dbf8e","tgt_lang":"ja-JP","translated":"降順","updated_at":"2026-04-05T17:13:23.087Z"} {"cache_key":"2a4e83ecffc2e41ad10cc0f30402f60ad52b67af4f3b8dd9582d3af787d21d10","model":"gpt-5.4","provider":"openai","segment_id":"usage.filters.remove","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Remove filter","text_hash":"23c5cdc6269ef451d3b3aed87b2cf78c0153cc9097143b6140f23d2331f5947f","tgt_lang":"ja-JP","translated":"フィルターを削除","updated_at":"2026-04-05T17:13:06.557Z"} @@ -577,7 +578,6 @@ {"cache_key":"ab257e73fd4aa8519857697549bc1c2385fa1f03f91529d3c61f95d27cf9c177","model":"gpt-5.4","provider":"openai","segment_id":"login.hideToken","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Hide token","text_hash":"ae132305cb4bfbfe5508d7a36a29a914ce321156b8b2e26d5cbddd29d033c713","tgt_lang":"ja-JP","translated":"トークンを非表示","updated_at":"2026-04-20T06:29:59.089Z"} {"cache_key":"aba5dc2f76ea3b71eca9533bb8359a2d66b1014ddeb85cff710a18c6faf5f375","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.schedules.everyEvening.description","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Daily at 6:00 PM","text_hash":"260b9ed9fe7fac30932e89488f121facfb1cd04b8ecda5ca2ece9a207ad6662a","tgt_lang":"ja-JP","translated":"毎日午後6:00","updated_at":"2026-04-29T20:13:41.861Z"} {"cache_key":"abd23e20fa8144e3ef1246403762747ecd08307ca03a1876f9a3b7312509ef0a","model":"gpt-5.4","provider":"openai","segment_id":"overview.access.togglePasswordVisibility","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Toggle password visibility","text_hash":"1016c07b0f58d365790cc799fb215afd92fde1aeb5ac47cd17260e327465b2d6","tgt_lang":"ja-JP","translated":"パスワードの表示/非表示を切り替え","updated_at":"2026-04-20T06:26:30.900Z"} -{"cache_key":"ac51635aaca50bc1d644461d9dbb1fd9775da881fc54cd60cef0be58f94a1c89","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"ja-JP","translated":"過去 N 分以内に更新されました。","updated_at":"2026-05-04T07:15:33.611Z"} {"cache_key":"ac90723f29ea65a52c674ab3f325e5dd760501c3d88e718ef4abe548b9bc1210","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.scene.clearGrounded","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Clear Grounded","text_hash":"9d643608d2334885c6dfee865cacda8bc0d01f1a099b4ec8d710f3896f3e5091","tgt_lang":"ja-JP","translated":"グラウンデッドをクリア","updated_at":"2026-04-08T22:27:51.616Z"} {"cache_key":"acd682d1d19dba60bb45e192f82ac86cfb6dd3dc6da61d22e67fa1dbf82f4096","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.messages","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Messages","text_hash":"04d7b48339271ea67d3c8493e07e90bc68dc565485eebe5e0b67c21c1586e3c0","tgt_lang":"ja-JP","translated":"メッセージ","updated_at":"2026-04-05T17:13:16.725Z"} {"cache_key":"ace89a8465089f61314961df347bb4f18997b8890a277d6242bac38081c455a4","model":"gpt-5.5","provider":"openai","segment_id":"usage.cacheStatus.status.refreshing","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"refreshing","text_hash":"0b61ac5d9426518ad7908a62037255c6881f9a5fa404ef3b99c24baa2111a174","tgt_lang":"ja-JP","translated":"更新中","updated_at":"2026-05-03T18:28:28.429Z"} diff --git a/ui/src/i18n/.i18n/ko.meta.json b/ui/src/i18n/.i18n/ko.meta.json index 371156d01c6..223d6284e0b 100644 --- a/ui/src/i18n/.i18n/ko.meta.json +++ b/ui/src/i18n/.i18n/ko.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:15:43.403Z", + "generatedAt": "2026-05-04T07:26:58.813Z", "locale": "ko", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/ko.tm.jsonl b/ui/src/i18n/.i18n/ko.tm.jsonl index e43ae342c22..8bb3c2238fe 100644 --- a/ui/src/i18n/.i18n/ko.tm.jsonl +++ b/ui/src/i18n/.i18n/ko.tm.jsonl @@ -585,6 +585,7 @@ {"cache_key":"ad2bf1924fdaf7c930a5f536f13f354123b251d43006eda550ed8ca346f6f318","model":"gpt-5.5","provider":"openai","segment_id":"chat.gatewayStatus","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Gateway status: {status}","text_hash":"5778a6ee172589bbd9027790e112d2f90264f86b112308924bf1acabc6b31935","tgt_lang":"ko","translated":"Gateway 상태: {status}","updated_at":"2026-04-29T20:13:56.264Z"} {"cache_key":"ada8768d373bb7398977df352967c77136e57f3760b759cbac9ee75a7c104208","model":"gpt-5.4","provider":"openai","segment_id":"usage.sessions.copyName","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Copy session name","text_hash":"30a6a5c11915b5b6a99698ebe1cee13b7b84adcc45ccd0a827decce17ce45a2d","tgt_lang":"ko","translated":"세션 이름 복사","updated_at":"2026-04-05T17:14:28.018Z"} {"cache_key":"adbd0d490fe023fd077df4b8da7b90b7b2e7391591bd19c7fb4ca83beb077c11","model":"gpt-5.4","provider":"openai","segment_id":"cron.runEntry.next","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Next {rel}","text_hash":"5103a64770ff39be372a8004ce2b7dfc3cb3a84d79bf86a9e3ecee19b01a9e97","tgt_lang":"ko","translated":"다음 {rel}","updated_at":"2026-04-05T17:15:12.312Z"} +{"cache_key":"add76d950b1c6512f35113bf14ebee6884f3237bd3d84e7f2f4560431c1eb794","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"ko","translated":"최근 {count}분 이내에 업데이트됨.","updated_at":"2026-05-04T07:15:43.251Z"} {"cache_key":"ae36821694d0b8f05e0a1d8c3d397e58f94c53e0906c27e504d0fa4e5e29bf50","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.phrases.whisperingVectorStore","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"whispering to the vector store…","text_hash":"44f8f2666f20599ad12e2e33ea95c6f37c8a2b422bf438d4bdb59e778ae6a527","tgt_lang":"ko","translated":"벡터 저장소에 속삭이는 중…","updated_at":"2026-04-06T02:49:37.847Z"} {"cache_key":"ae4351968b781330e07f39c794762afe0b50e3a81817478e5c3059f6a454aacc","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.schedules.once.label","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Run once","text_hash":"5f041f4be2d3becdcb1363508b415794005ddbcfae08c4d6d5a29d615ea73922","tgt_lang":"ko","translated":"한 번 실행","updated_at":"2026-04-29T20:13:59.874Z"} {"cache_key":"ae9c8c49c556d9a403531442e85e072d2afcf5d86593a7cc0b4763db8d497d73","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.addJob","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Add job","text_hash":"30984d76f83a02109b01e7d7b2fabb4695ddadf3cdfc5c5b79a3d596b8fbb2ba","tgt_lang":"ko","translated":"작업 추가","updated_at":"2026-04-05T17:15:08.972Z"} @@ -675,7 +676,6 @@ {"cache_key":"c9c5f7fa93a6817671b59583b660a79f77827ff5d2e531d47464daccada8a630","model":"gpt-5.4","provider":"openai","segment_id":"cron.jobState.last","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Last","text_hash":"eb970eb0951c6cdeac1ec0cc723fc91e30b0c26ee6f3b5ee0e574db7f487dc55","tgt_lang":"ko","translated":"마지막","updated_at":"2026-04-05T17:15:12.312Z"} {"cache_key":"c9f56c04be9fd0a319588f6d09ba5341840af7abd30e24df22696af61fb0d5af","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.topTools","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Top Tools","text_hash":"ff908e711c3c21e0074b29e1f2953688ab11a463b463af18005e8900d92f1ee5","tgt_lang":"ko","translated":"상위 도구","updated_at":"2026-04-05T17:14:21.763Z"} {"cache_key":"ca18cf1bb219a55fb6715e83b956f0dda581f93b4a757a81d8003dbf46606b2b","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.promptPlaceholder","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"e.g., Check my inbox for urgent emails and summarize them...","text_hash":"f4675787351dcf3b421c7f187fc9e501f37cb0a5ca79ddcde938cf99efe2dac1","tgt_lang":"ko","translated":"예: 받은편지함에서 긴급한 이메일을 확인하고 요약하기...","updated_at":"2026-04-29T20:14:03.346Z"} -{"cache_key":"caa8a224e7d11b564f50a2464abeb134e25a678647b097a407cc3dab0ebc30a9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"ko","translated":"최근 N분 이내에 업데이트됨.","updated_at":"2026-05-04T07:15:43.251Z"} {"cache_key":"caaf2012eab30de293e1365ae17728634f80d0dcc10eb260b1c3938e8ad43b90","model":"gpt-5.4","provider":"openai","segment_id":"usage.filters.clear","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Clear","text_hash":"83b12c2216efb4fdc924e1deb5182e905e4926ed0c1c324d467107f46d5a26a9","tgt_lang":"ko","translated":"지우기","updated_at":"2026-04-05T17:14:06.820Z"} {"cache_key":"cab211c93327185e96161e6434a659d416aaf099346331d5f8db367a2e2866bc","model":"gpt-5.4","provider":"openai","segment_id":"cron.jobs.title","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Jobs","text_hash":"2f17a0f8d518e491c5a0c490b2c1991828dd87d173994ba40996e1da59d4e368","tgt_lang":"ko","translated":"작업","updated_at":"2026-04-05T17:14:40.640Z"} {"cache_key":"cab533aee3881fcc62ae5b14cbd2d6fa5fd7c30b24003e06d6a272de2ce14752","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.trace.shortTerm","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Short-term","text_hash":"5bb852d4225d676aa64e8933284475ce54fd35d9535b4f5b4b37c42245112df0","tgt_lang":"ko","translated":"단기","updated_at":"2026-04-08T18:37:46.634Z"} diff --git a/ui/src/i18n/.i18n/nl.meta.json b/ui/src/i18n/.i18n/nl.meta.json index 8b6aa63140b..24c9a2f4bb5 100644 --- a/ui/src/i18n/.i18n/nl.meta.json +++ b/ui/src/i18n/.i18n/nl.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:18:58.818Z", + "generatedAt": "2026-05-04T07:44:20.725Z", "locale": "nl", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/nl.tm.jsonl b/ui/src/i18n/.i18n/nl.tm.jsonl index 6fe407b6db4..25f7ddf2bf0 100644 --- a/ui/src/i18n/.i18n/nl.tm.jsonl +++ b/ui/src/i18n/.i18n/nl.tm.jsonl @@ -282,6 +282,7 @@ {"cache_key":"4a49c5383f69c2ccf868bcc472904926855d1ea19c5b7f6e39201d40f090fa31","model":"gpt-5.5","provider":"openai","segment_id":"cron.errors.webhookUrlInvalid","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Webhook URL must start with http:// or https://.","text_hash":"08a52ce0d5afdaa43d74ecefd749f61e6ecc3368a92a459f07bf85e612ac7dc1","tgt_lang":"nl","translated":"Webhook-URL moet beginnen met http:// of https://.","updated_at":"2026-04-29T17:42:07.746Z"} {"cache_key":"4a52373c509a16c05b7a8291b2806f29cf4b9ebfa69817d0fec89fdf69594950","model":"gpt-5.5","provider":"openai","segment_id":"agents.cronPanel.schedulerSubtitle","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Gateway cron status.","text_hash":"f56600509094c3eb8014ac7811bdebc67fd9c8332603859f4718ac6f6ea2f378","tgt_lang":"nl","translated":"Gateway-cronstatus.","updated_at":"2026-04-29T19:28:46.675Z"} {"cache_key":"4a7a503097a7582a7e593d8aacd811f98c2860c84ef9c9442b8818971b230dfb","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.createSubtitle","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Create a scheduled wakeup or agent run.","text_hash":"63ed10abfd41f9a26d9630dfb564122e33a033a0abcee985c0c935076fa0e269","tgt_lang":"nl","translated":"Maak een geplande wakeup of agent-run.","updated_at":"2026-04-29T17:41:38.443Z"} +{"cache_key":"4af6785cd94ed43e19b8e4565a1074f38b515047ad8729de6572842c65923cfc","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"nl","translated":"Bijgewerkt in de afgelopen {count} minuten.","updated_at":"2026-05-04T07:18:58.666Z"} {"cache_key":"4c49d1ce80fe78d71e573b486691939d2622da86413fbdc5da3eddee62d77f9a","model":"gpt-5.5","provider":"openai","segment_id":"common.mode","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Mode","text_hash":"5e23ec6a300dc60a79641769017e16e9bf042cbd8fd0a54586a048ab9da972ff","tgt_lang":"nl","translated":"Modus","updated_at":"2026-04-29T17:39:48.839Z"} {"cache_key":"4c665dc12881845b071791a01ff83f18a629614dcf9d8dc85b57f607eda91bcb","model":"gpt-5.5","provider":"openai","segment_id":"agentTools.connected","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Connected","text_hash":"22965568d22a14ee17af055d2870b50afcfe9fd94a83eec3196e266932297bb2","tgt_lang":"nl","translated":"Verbonden","updated_at":"2026-04-29T17:40:04.533Z"} {"cache_key":"4c957227ff64902cd114ebcd8aefe4560f968789d1cf5269337a57467178d0d0","model":"gpt-5.5","provider":"openai","segment_id":"common.na","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"n/a","text_hash":"a683c5c5349f6f7fb903ba8a9e7e55d0ba1b8f03579f95be83f4954c33e81098","tgt_lang":"nl","translated":"n.v.t.","updated_at":"2026-04-29T17:39:48.839Z"} @@ -635,7 +636,6 @@ {"cache_key":"a25ebb20dc026f66913226b7a28dcdb648c095313708e14771580ff24b96e7a8","model":"gpt-5.5","provider":"openai","segment_id":"tabs.automation","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"nl","translated":"Automatisering","updated_at":"2026-04-29T17:40:06.931Z"} {"cache_key":"a2638d00525c1326b354211faf0097398241f297814fc258a64b8a796b542000","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.savedPreview","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Saved Preview","text_hash":"114b12b88b6da1bb0386785ef5f86fc52d93d7ba6d803497d47e1e2648cfc2b6","tgt_lang":"nl","translated":"Opgeslagen preview","updated_at":"2026-04-29T19:28:50.601Z"} {"cache_key":"a27d4f4024692d56020c96e386b5d7ba4166137ea8045b50de4e3eec3164a10a","model":"gpt-5.5","provider":"openai","segment_id":"agents.cronPanel.schedulerTitle","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Scheduler","text_hash":"d3a27d96cd0791a2b2161ed5cf5e3b5c0d360d05070e7bf6bf0e45d4e5a8f264","tgt_lang":"nl","translated":"Planner","updated_at":"2026-04-29T19:28:40.901Z"} -{"cache_key":"a2dceb2b580f19401f9f8708bd5af2af47fd425ef1ed7f8d6ec618e84817d5df","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"nl","translated":"Bijgewerkt in de afgelopen N minuten.","updated_at":"2026-05-04T07:18:58.666Z"} {"cache_key":"a3188f26446d6d1605a8611c5d4c0892d77f675e9d02b445bd3191abee03ab5f","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.unit","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Unit","text_hash":"4e545960f1bffc134026127ef92963e136ec84b24bb2a6103c0731a64843a40b","tgt_lang":"nl","translated":"Eenheid","updated_at":"2026-04-29T17:41:42.374Z"} {"cache_key":"a37f612d80b07efff9d49ec038b5470568ee394b423891c6248324c2940052f9","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.baseContextPerMessage","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Base context per message","text_hash":"f97ff4c2483a2174935304524775bc8191237e0bd314d05470c8b1f30ce435b6","tgt_lang":"nl","translated":"Basiscontext per bericht","updated_at":"2026-04-29T17:41:14.532Z"} {"cache_key":"a38673d69c79c12bd380e8753769e7a7b679fcb7f61af4991a1013b1a30d2212","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.limitTooltip","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Max sessions to load.","text_hash":"c641a9d09477295f5478e1d3837b0fcc0e0969859f4dba407079b0825b9cd076","tgt_lang":"nl","translated":"Maximaal aantal te laden sessies.","updated_at":"2026-05-04T07:18:58.666Z"} diff --git a/ui/src/i18n/.i18n/pl.meta.json b/ui/src/i18n/.i18n/pl.meta.json index c0b4e72a89e..6690c7f9fbd 100644 --- a/ui/src/i18n/.i18n/pl.meta.json +++ b/ui/src/i18n/.i18n/pl.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:17:54.025Z", + "generatedAt": "2026-05-04T07:44:19.638Z", "locale": "pl", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/pl.tm.jsonl b/ui/src/i18n/.i18n/pl.tm.jsonl index d746b152491..e334b63bf58 100644 --- a/ui/src/i18n/.i18n/pl.tm.jsonl +++ b/ui/src/i18n/.i18n/pl.tm.jsonl @@ -92,7 +92,6 @@ {"cache_key":"1e782a344f3566e4b7839d75a9942f2e3dcb7d427f6da99cd505296fb774ce51","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.topChannels","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Top Channels","text_hash":"92e23b093bbed13d780e3254f68e4b497623baebf74b36b59cdd2116c8de9e58","tgt_lang":"pl","translated":"Najpopularniejsze kanały","updated_at":"2026-04-05T17:16:52.148Z"} {"cache_key":"1e9c38dfe98ab46b5db2f210704698fb34a7e5d6303c4a58f67b3c76f5e166f3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.reasoning","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Reasoning","text_hash":"d8211e24e83d1600a1b0cfe2f7baa68e4d4eb71131a0b2b1b2050cba111ea481","tgt_lang":"pl","translated":"Rozumowanie","updated_at":"2026-04-29T20:16:02.824Z"} {"cache_key":"1ef3ff481ea100a778bd4fc749088a1d6dba226618edc33af8973f7315f72aad","model":"gpt-5.4","provider":"openai","segment_id":"overview.snapshot.uptime","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Uptime","text_hash":"d63ab4711473b0398feb4b56622605d5d2ec7ecd3b1bb5070a7dd56de96aaf88","tgt_lang":"pl","translated":"Czas działania","updated_at":"2026-04-05T17:16:24.832Z"} -{"cache_key":"1fb3e21c0d1a983de6881085cf77bfc4b27f2f7057c08a8859a86b3df32bc31d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"pl","translated":"Zaktualizowano w ciągu ostatnich N minut.","updated_at":"2026-05-04T07:17:53.873Z"} {"cache_key":"1fc239dcbddab0cb3ce1f5a99f640aafa5b0db561853069da162b432331795b5","model":"gpt-5.4","provider":"openai","segment_id":"usage.empty.title","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Start with a date range","text_hash":"b7c62643985a46857b304fcad4565f828cba8925e4f5de2a078f647414b6279c","tgt_lang":"pl","translated":"Zacznij od zakresu dat","updated_at":"2026-04-05T17:16:43.737Z"} {"cache_key":"1fc57e826bc02ee366a64e7e341272eafa26aa927a41e379e0b5e3875ad3446a","model":"gpt-5.4","provider":"openai","segment_id":"overview.access.password","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Password (not stored)","text_hash":"a693085108fe8ddea3acb78ba8ac0c275e593fc85db1c526006247ceb1372dda","tgt_lang":"pl","translated":"Hasło (nie jest przechowywane)","updated_at":"2026-04-05T17:16:24.832Z"} {"cache_key":"1feb8393b4693aaf0c9d975ff472f43cd9420b34c9f7c1a472bd33228779a14f","model":"gpt-5.4","provider":"openai","segment_id":"overview.snapshot.title","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Snapshot","text_hash":"6ad27bd4ec33b079208334dfea86ff96900f95ca640dda1d2638d694d077668b","tgt_lang":"pl","translated":"Migawka","updated_at":"2026-04-05T17:16:24.832Z"} @@ -183,6 +182,7 @@ {"cache_key":"3a7fa6979487597d2ee3056ef8a0a2e2e35ca37daf3c71c4e5604af359aa1758","model":"gpt-5.4","provider":"openai","segment_id":"channels.generic.subtitle","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Channel status and configuration.","text_hash":"af598d2e3f8e7a9dcacdc23e2865c738ceced7ac9c98bb19ff0fde64e76d5be0","tgt_lang":"pl","translated":"Stan kanału i konfiguracja.","updated_at":"2026-04-06T02:51:07.591Z"} {"cache_key":"3adf66835f27121500f18f5eb1cc5d04abd5d089f35dd87726698aff00b7842b","model":"gpt-5.4","provider":"openai","segment_id":"login.subtitle","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Gateway Dashboard","text_hash":"a8a4f466acb4542337608029c6f0769f3daa5fed65128f73ab99f00eddfa6ccb","tgt_lang":"pl","translated":"Panel Gateway","updated_at":"2026-04-05T17:17:09.206Z"} {"cache_key":"3af21ed1de8098be64a1c2b2b8086792d3651f973d652acea275856b5fbfc80e","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.deleteAfterRun","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Delete after run","text_hash":"ed7fcb6a70cb79c43343fd72da48695bc36b8863afba224ed8f7fc3d797e20d3","tgt_lang":"pl","translated":"Usuń po uruchomieniu","updated_at":"2026-04-05T17:17:34.464Z"} +{"cache_key":"3b1749a46c1d0b0e79233db4f0fc9c3ee819491626591655a009d402981f6208","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"pl","translated":"Zaktualizowano w ciągu ostatnich {count} minut.","updated_at":"2026-05-04T07:17:53.873Z"} {"cache_key":"3b849ef69896c7a24f74423ae79346a647b9202a5a978037d8f88b1cd9a2cd2c","model":"gpt-5.4","provider":"openai","segment_id":"channels.health.title","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Channel health","text_hash":"b3575639c4703c004745caf32e50f3458615d3a75b993ef9e7cf58ec1436eadb","tgt_lang":"pl","translated":"Stan kanału","updated_at":"2026-04-06T02:51:07.591Z"} {"cache_key":"3c07cce7e0467a8f425f318e202b5085b72fcd2261ad006f390a3b9f5ea879ca","model":"gpt-5.4","provider":"openai","segment_id":"usage.details.baseContextPerMessage","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Base context per message","text_hash":"f97ff4c2483a2174935304524775bc8191237e0bd314d05470c8b1f30ce435b6","tgt_lang":"pl","translated":"Bazowy kontekst na wiadomość","updated_at":"2026-04-05T17:17:02.403Z"} {"cache_key":"3c2694f87ae73fd1044a13dc4ebd527576dbc9c92426128928557b39a20b9591","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.avgTokensHint","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Average tokens per message in this range.","text_hash":"bbd6264e7d1f78cedb1fa94a36a3cc55900f5f9c4c63171482b3c3ceb6898bdf","tgt_lang":"pl","translated":"Średnia liczba tokenów na wiadomość w tym zakresie.","updated_at":"2026-04-05T17:16:47.830Z"} diff --git a/ui/src/i18n/.i18n/pt-BR.meta.json b/ui/src/i18n/.i18n/pt-BR.meta.json index 19bac4ee50f..635eaf2f1e6 100644 --- a/ui/src/i18n/.i18n/pt-BR.meta.json +++ b/ui/src/i18n/.i18n/pt-BR.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:14:22.617Z", + "generatedAt": "2026-05-04T07:26:57.413Z", "locale": "pt-BR", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/pt-BR.tm.jsonl b/ui/src/i18n/.i18n/pt-BR.tm.jsonl index 3a64173c5d0..5099acab91c 100644 --- a/ui/src/i18n/.i18n/pt-BR.tm.jsonl +++ b/ui/src/i18n/.i18n/pt-BR.tm.jsonl @@ -324,6 +324,7 @@ {"cache_key":"78f778cfd1c2a5441db496d63ce1f21a8b892bed5b5a9b159107000ed2fc7a96","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.stats.grounded","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Grounded","text_hash":"5b6f73f04fe1a6af2dc43bebb45478862b0bd1fe079eed12f8bc2000a59bf68c","tgt_lang":"pt-BR","translated":"Grounded","updated_at":"2026-04-08T22:26:37.444Z"} {"cache_key":"7931c1bd20cca11ed4aab09fec6f5b6017ae658564f5e4a508db60449a386351","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.customOption","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{value} (custom)","text_hash":"3c72a6f7c232c01c3d59e562bc0423a5fe43ef909dbd539a3779d2c0961cebfd","tgt_lang":"pt-BR","translated":"{value} (personalizado)","updated_at":"2026-04-29T20:12:14.854Z"} {"cache_key":"79358b55eccd969214a10df5e35817efc7291f7553b63040aed8347ed01a1163","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.phase.light","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Light","text_hash":"dbcd5e7bb7a0f538810de44c3efbd813037ee3fa358747bb71fa58e157af45f7","tgt_lang":"pt-BR","translated":"Leve","updated_at":"2026-04-10T07:58:35.935Z"} +{"cache_key":"79425970e2dd06f22115786c1a9b4bf1df9acafd7e60d578a7b1e7716f3cf95f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"pt-BR","translated":"Atualizadas nos últimos {count} minutos.","updated_at":"2026-05-04T07:14:22.465Z"} {"cache_key":"797d0a82ce905eac2416961630698410948ff758ead01c889b2df8db6f1912d5","model":"gpt-5.4","provider":"openai","segment_id":"instances.noInstances","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"No instances reported yet.","text_hash":"b59d2b2a9c8f6feb0c3981115571dbde79e50246927749b595ccaf0d0266f9c0","tgt_lang":"pt-BR","translated":"Nenhuma instância reportada ainda.","updated_at":"2026-04-06T02:47:48.413Z"} {"cache_key":"79c412da89feae31d8c71f3c434d7b8685f927294f6800111c6b189f72df840b","model":"gpt-5.4","provider":"openai","segment_id":"instances.lastInput","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Last input {time}","text_hash":"04c40c4d7fa4438b7d6afe2f3997bc427522d67e80f8adc42ee0269eed294760","tgt_lang":"pt-BR","translated":"Última entrada {time}","updated_at":"2026-04-06T02:47:48.413Z"} {"cache_key":"79cc524cfe4bcb5d807cca66eda620f5d2aa31f527ecaa80fe7918b024ec50c0","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.nameOptional","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Name (optional)","text_hash":"e09b37bcc4f301f48329f9e0c5ee747acf821d692e9aac10abea5970ba70b1b4","tgt_lang":"pt-BR","translated":"Nome (opcional)","updated_at":"2026-04-29T20:12:27.045Z"} @@ -436,7 +437,6 @@ {"cache_key":"9ee24af6b8aed172f51ab83ecca70d6a4f50092450f597b18d44c1b661ba95b8","model":"gpt-5.4","provider":"openai","segment_id":"usage.export.dailyCsv","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Daily CSV","text_hash":"84cace61dc7bdfca594e2a15b42e4325fb280c3dc02c4059b824fa01f485721d","tgt_lang":"pt-BR","translated":"CSV diário","updated_at":"2026-04-05T17:10:49.727Z"} {"cache_key":"9eff6fafd82819fd3d54682acd66291af243e1284de5f49241c685de54ebf5aa","model":"gpt-5.4","provider":"openai","segment_id":"overview.cards.modelAuthExpiresIn","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"expires {when}","text_hash":"a70e9d68758ae6b1c6bbe34b53bfe111aaad9a6ef498e98f3b8210d43be64196","tgt_lang":"pt-BR","translated":"expira {when}","updated_at":"2026-04-15T05:42:32.223Z"} {"cache_key":"9f0dec450146fa5b8d3214c8d662d50c5cb06f875bd6d5acce73bafbf57578ee","model":"gpt-5.4","provider":"openai","segment_id":"usage.daily.costTitle","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Daily Cost","text_hash":"7de5f8facf96834a19c79853ff2f0a5a4d0c2bc73a4059893f3a5c8c7f207627","tgt_lang":"pt-BR","translated":"Custo diário","updated_at":"2026-04-05T17:10:49.727Z"} -{"cache_key":"9f59a4c4da7cbb84167b8d88a6504c87375e67b5528a72962c31790bf5d6b680","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"pt-BR","translated":"Atualizadas nos últimos N minutos.","updated_at":"2026-05-04T07:14:22.465Z"} {"cache_key":"9f5aaa40b47cb645d172cf973d2b77bc292551dc86e069a1abb7baaf41caf3b1","model":"gpt-5.4","provider":"openai","segment_id":"usage.mosaic.sun","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Sun","text_hash":"db18f17fe532007616d0d0fcc303281c35aafc940b13e6af55e63f8fed304718","tgt_lang":"pt-BR","translated":"Dom","updated_at":"2026-04-05T17:11:24.071Z"} {"cache_key":"9f5fcd137b60539a2671fb2d7e3cdb93e695220571112c0cce9f0c87337f95a4","model":"gpt-5.5","provider":"openai","segment_id":"lazyView.unknownError","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Unknown module load error.","text_hash":"aac7340f2785adb609e98dd446aa05dff6d6a839e94c666cf3663aaab9ec75da","tgt_lang":"pt-BR","translated":"Erro desconhecido ao carregar módulo.","updated_at":"2026-04-27T12:10:49.819Z"} {"cache_key":"9f6fea71d465a68fed43595a09ad3924c742ff13a92d91aaaf683ff8987d1630","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.status.active","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Dreaming Active","text_hash":"fd7a73177f09d63e4afe11f3ac6e028368eb1c3163b80022a9bf46b94e1b658a","tgt_lang":"pt-BR","translated":"Dreaming ativo","updated_at":"2026-04-06T02:47:51.702Z"} diff --git a/ui/src/i18n/.i18n/raw-copy-baseline.json b/ui/src/i18n/.i18n/raw-copy-baseline.json index e819b8a71ca..7bca1626d8f 100644 --- a/ui/src/i18n/.i18n/raw-copy-baseline.json +++ b/ui/src/i18n/.i18n/raw-copy-baseline.json @@ -1457,6 +1457,13 @@ "path": "ui/src/ui/views/chat.ts", "text": "Command arguments" }, + { + "count": 1, + "kind": "html-attribute", + "name": "aria-label", + "path": "ui/src/ui/views/chat.ts", + "text": "Dismiss error" + }, { "count": 1, "kind": "html-attribute", @@ -1506,6 +1513,13 @@ "path": "ui/src/ui/views/chat.ts", "text": "Attach file" }, + { + "count": 1, + "kind": "html-attribute", + "name": "title", + "path": "ui/src/ui/views/chat.ts", + "text": "Dismiss error" + }, { "count": 1, "kind": "html-attribute", diff --git a/ui/src/i18n/.i18n/th.meta.json b/ui/src/i18n/.i18n/th.meta.json index 61bc1946ecc..ea652e26374 100644 --- a/ui/src/i18n/.i18n/th.meta.json +++ b/ui/src/i18n/.i18n/th.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:18:01.683Z", + "generatedAt": "2026-05-04T07:44:19.987Z", "locale": "th", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/th.tm.jsonl b/ui/src/i18n/.i18n/th.tm.jsonl index 4736630d3cd..2cea2b254fa 100644 --- a/ui/src/i18n/.i18n/th.tm.jsonl +++ b/ui/src/i18n/.i18n/th.tm.jsonl @@ -106,6 +106,7 @@ {"cache_key":"239c7a26f12fb2f770c395e37daafc6f65086a58ccccb97dafdecd65afcdb408","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.perMinute","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"/ min","text_hash":"ede1804d815f1fc5f7a6975db537261fea2fe5e95e58eb82e088af45aa525acc","tgt_lang":"th","translated":"/ นาที","updated_at":"2026-04-23T06:27:44.114Z"} {"cache_key":"23aa942c05cf807e1359020fe39bd22109661f864a13e131805174a64a881d14","model":"gpt-5.4","provider":"openai","segment_id":"usage.common.unknown","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"unknown","text_hash":"b23a6a8439c0dde5515893e7c90c1e3233b8616e634470f20dc4928bcf3609bc","tgt_lang":"th","translated":"ไม่ทราบ","updated_at":"2026-04-23T06:27:22.345Z"} {"cache_key":"23dcea3fbe99bfd34f62de0e40e55473f3005b6124ae32d0d9d0f25073f52134","model":"gpt-5.4","provider":"openai","segment_id":"usage.details.reset","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Reset","text_hash":"daee7606b339f3c339076fe2c9f372a3ff40c8ee896005d829c7481b64ca5303","tgt_lang":"th","translated":"รีเซ็ต","updated_at":"2026-04-23T06:27:59.553Z"} +{"cache_key":"244ce07f279cd89f031eb359157811de1177ee5c1d8b2b5bd38f8236d4dce76e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"th","translated":"อัปเดตในช่วง {count} นาทีที่ผ่านมา","updated_at":"2026-05-04T07:18:01.529Z"} {"cache_key":"24a12991ce198e5b58e521d1bee4e4cd9eb570a826cc614807dda65f498b7e09","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.status.promotedSuffix","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"promoted","text_hash":"348f71b67f2d742317773fc33fa48fa65f4a016adc8ce1a5afdbc50ce33b2c34","tgt_lang":"th","translated":"เลื่อนระดับแล้ว","updated_at":"2026-04-23T06:26:47.687Z"} {"cache_key":"24b1ca89c2fa9b55f96ce6f619822bf22875bef31bb9228d280ccbedf1b61029","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.advanced.summaryWaiting","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"waiting","text_hash":"80cfa3e7f28dde4df64436b652230aff28d7779116d1369c21ef2bbf37261d71","tgt_lang":"th","translated":"กำลังรอ","updated_at":"2026-04-23T06:26:53.343Z"} {"cache_key":"24f2236a499d7dd44b0aadb4ace5e7283f0069b9528cb44cf6d2d99dc9e92779","model":"gpt-5.4","provider":"openai","segment_id":"overview.cards.skills","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Skills","text_hash":"66d0f523a379b2de6f8d5fba3a817ebc395f7bcaa54cc132ca9dfa665d1e9378","tgt_lang":"th","translated":"ทักษะ","updated_at":"2026-04-23T06:26:43.104Z"} @@ -507,7 +508,6 @@ {"cache_key":"96fcffff67a8509e15f8d1b00dc1edc56d94a3296162e50f9a756a9b958bbdac","model":"gpt-5.4","provider":"openai","segment_id":"instances.title","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Connected Instances","text_hash":"2530c88aeba856f87750a97e01ee81c93f02da297a96acd456d3ff0adbb60a3d","tgt_lang":"th","translated":"อินสแตนซ์ที่เชื่อมต่อ","updated_at":"2026-04-23T06:26:03.424Z"} {"cache_key":"96fe46fa322193544728feedcee8716704a17f6895e2efdad9c3e4b09d84b49a","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.messagesAbbrev","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"msgs","text_hash":"8dc321b9135ee4fbee83a304b911e871f83e7ae84d344bae6f464804f77b2f86","tgt_lang":"th","translated":"ข้อความ","updated_at":"2026-04-23T06:27:38.605Z"} {"cache_key":"97595ea7e1301a8c7560e6fb8b311a7c33de120f147f27bb9e152cde75eb7c02","model":"gpt-5.4","provider":"openai","segment_id":"overview.pairing.scopeUpgradeTitle","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Scope upgrade pending approval.","text_hash":"01f51310417022d876b39bac2b047896b7a52e4be59e9ea7ce5416ae0c9010b3","tgt_lang":"th","translated":"การอัปเกรดขอบเขตรอการอนุมัติ","updated_at":"2026-04-23T06:26:29.742Z"} -{"cache_key":"976fd74b58efbeeaddf142bed0c9021592cb31eebc5f3466370b4fb7050380b8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"th","translated":"อัปเดตในช่วง N นาทีที่ผ่านมา","updated_at":"2026-05-04T07:18:01.529Z"} {"cache_key":"97cdccaabec5aea52074355a07806f5fc6252fcd00a1c360de4f70d03c5bd121","model":"gpt-5.4","provider":"openai","segment_id":"subtitles.communications","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Channels, messages, and audio settings.","text_hash":"def8e69dd8fc17bc8fa0c1beabe41f35979a41f9e91b3c5a0eec162c58ac3a1b","tgt_lang":"th","translated":"ช่องทาง ข้อความ และการตั้งค่าเสียง","updated_at":"2026-04-23T06:26:16.011Z"} {"cache_key":"97ea9c4e6cda9bc30f558361959c3a87b4b9ccd885b262ba28d75d2a5b8976ef","model":"gpt-5.4","provider":"openai","segment_id":"usage.mosaic.noon","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Noon","text_hash":"e227fdfa5daf8a279db1e378933f2c784c8ddd21993dd5220c0106a0247a5f09","tgt_lang":"th","translated":"เที่ยงวัน","updated_at":"2026-04-23T06:28:07.264Z"} {"cache_key":"98236c0e9455b936b23afa2fa3af3a227c446c52e8662e6181d4db5e1395ba7e","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.trace.emptyPromoted","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Nothing promoted yet today.","text_hash":"4da842404d1c9c9bd3d2a7bd71fe3b16fb6af8db427d1fb00111f56c4a6f15b2","tgt_lang":"th","translated":"ยังไม่มีสิ่งใดได้รับการเลื่อนระดับในวันนี้","updated_at":"2026-04-23T06:27:13.277Z"} diff --git a/ui/src/i18n/.i18n/tr.meta.json b/ui/src/i18n/.i18n/tr.meta.json index 1edfcb4c13a..7dc1f2e8089 100644 --- a/ui/src/i18n/.i18n/tr.meta.json +++ b/ui/src/i18n/.i18n/tr.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:16:49.946Z", + "generatedAt": "2026-05-04T07:27:00.334Z", "locale": "tr", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/tr.tm.jsonl b/ui/src/i18n/.i18n/tr.tm.jsonl index 422c07547c9..62aed9cd819 100644 --- a/ui/src/i18n/.i18n/tr.tm.jsonl +++ b/ui/src/i18n/.i18n/tr.tm.jsonl @@ -66,6 +66,7 @@ {"cache_key":"0f4a104b8a050dcbaeb1e7709b7d2676b74ec5ac671caa64cd8b07e1a7942fca","model":"gpt-5.5","provider":"openai","segment_id":"chat.commandPaletteTitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Search or jump to… (⌘K)","text_hash":"3116c088ff7d8d4e10c5a0e27fd960bc1cb60a21ac94153f7290e4e0ab9ac22c","tgt_lang":"tr","translated":"Ara veya şuraya git… (⌘K)","updated_at":"2026-04-29T20:15:07.759Z"} {"cache_key":"0f6405edc0e1329f5f0c151401ddc027f5ed0ba51d8dab9af24de060a8b73f8b","model":"gpt-5.4","provider":"openai","segment_id":"common.linked","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Linked","text_hash":"bfda026e6c598dde4d1b23c6a1789ba5a900b2e6d2e6b493469417c81dd16947","tgt_lang":"tr","translated":"Bağlandı","updated_at":"2026-04-06T02:50:00.165Z"} {"cache_key":"106e4be4ece5064e8c9f6d2832b75babe3b91315b94002b9513ef183919d9df8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.subtitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Active session keys and per-session overrides.","text_hash":"7d09f6d3eea2e0d13f41feea1e22b5432d2a2ba0721af5fc87faff98fe04e8e5","tgt_lang":"tr","translated":"Etkin oturum anahtarları ve oturum bazlı geçersiz kılmalar.","updated_at":"2026-04-29T20:15:00.381Z"} +{"cache_key":"10ca1a672bdf5bde524ca4aa0a028476b68079d5c83830a0f293bba66a06e123","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"tr","translated":"Son {count} dakika içinde güncellendi.","updated_at":"2026-05-04T07:16:49.792Z"} {"cache_key":"115b43798f1c8d424b9e3decc40ea58697cba2d8fec11b92e649b2700cbc335f","model":"gpt-5.4","provider":"openai","segment_id":"cron.errors.invalidIntervalAmount","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Invalid interval amount.","text_hash":"00547e12dda54278adb10d27e4d77113926832b609b0d0220c4614a4a223d636","tgt_lang":"tr","translated":"Geçersiz aralık miktarı.","updated_at":"2026-04-05T17:16:38.206Z"} {"cache_key":"11f72a0287e3201cb03dbc4a136ac3164e294b57857efde42428d9bca89340a7","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.updateSubtitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Update the selected scheduled job.","text_hash":"ed99ca1a9cd6abc6cef3c8ab9022ec162d7b7080c2fb4c5c9d3b58be2229c803","tgt_lang":"tr","translated":"Seçili zamanlanmış işi güncelleyin.","updated_at":"2026-04-05T17:16:06.352Z"} {"cache_key":"1211c7da5baba63d5e1ad506ec153746128e6793847924e6afd4f79de80d36af","model":"gpt-5.4","provider":"openai","segment_id":"usage.export.dailyCsv","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Daily CSV","text_hash":"84cace61dc7bdfca594e2a15b42e4325fb280c3dc02c4059b824fa01f485721d","tgt_lang":"tr","translated":"Günlük CSV","updated_at":"2026-04-05T17:15:18.153Z"} @@ -364,7 +365,6 @@ {"cache_key":"6d0a402dafc43a9b0100cb42ba94f31081d9cfd75bb77066c05e60ddc4a566fd","model":"gpt-5.4","provider":"openai","segment_id":"login.togglePasswordVisibility","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Toggle password visibility","text_hash":"1016c07b0f58d365790cc799fb215afd92fde1aeb5ac47cd17260e327465b2d6","tgt_lang":"tr","translated":"Parola görünürlüğünü değiştir","updated_at":"2026-04-20T06:30:07.350Z"} {"cache_key":"6d2e2d105ca034b9e4ef5ad588ba12edd8f126699fa3caa976472a683f61249f","model":"gpt-5.4","provider":"openai","segment_id":"channels.nostr.bio","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Bio","text_hash":"3933b1802161254f41c59f2909f61ac994c086e1cde03848c4c310f45b5b4999","tgt_lang":"tr","translated":"Biyografi","updated_at":"2026-04-06T02:50:10.967Z"} {"cache_key":"6d4b78f710f5712fa23650aaf1e8747e10cf582c321c53404e1ce5d8b5396799","model":"gpt-5.4","provider":"openai","segment_id":"usage.sessions.limitReached","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Showing first 1,000 sessions. Narrow date range for complete results.","text_hash":"677fc1d231d5e3a14126ba368b8c3c78db7b9ffafdd98259af67c64c07a4aa73","tgt_lang":"tr","translated":"İlk 1.000 oturum gösteriliyor. Tam sonuçlar için tarih aralığını daraltın.","updated_at":"2026-04-05T17:15:40.851Z"} -{"cache_key":"6dc80a91202a70204ce7beb533b63096760a174a88bfda378492b92f4313a98e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"tr","translated":"Son N dakika içinde güncellendi.","updated_at":"2026-05-04T07:16:49.792Z"} {"cache_key":"6e9c96719108bc2d77c46019b9fc41905f65de2e5a899b27808bd03fd23c56f2","model":"gpt-5.4","provider":"openai","segment_id":"overview.cards.modelAuthExpired","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{count} expired","text_hash":"eb1d6d89839cfe3ff9f5a383e80448eaaa75d0e285cbdb9d5182d1abe562147f","tgt_lang":"tr","translated":"{count} süresi dolmuş","updated_at":"2026-04-15T05:45:06.930Z"} {"cache_key":"6eb9aaee387d1218ef7ccb94efea01dc4f655dac6f216c622a9c835ac33d32d0","model":"gpt-5.4","provider":"openai","segment_id":"usage.metrics.cost","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Cost","text_hash":"204a5eb2cd28bcfdf3be9f8c765948e9e831609e3c57048cdbd6b8a94cf49126","tgt_lang":"tr","translated":"Maliyet","updated_at":"2026-04-05T17:15:14.133Z"} {"cache_key":"6f4b5958c2ef75795dd173934ec5fee271d31dcf041187a67756d29d5feb2e2e","model":"gpt-5.4","provider":"openai","segment_id":"channels.nostr.avatarHelp","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"HTTPS URL to your profile picture","text_hash":"47a318504f5730335750f1a2147910a74fe606f730bed716e5a401d7a8246877","tgt_lang":"tr","translated":"Profil resminizin HTTPS URL'si","updated_at":"2026-04-06T02:50:10.967Z"} diff --git a/ui/src/i18n/.i18n/uk.meta.json b/ui/src/i18n/.i18n/uk.meta.json index 97390565d3c..1a2af37ae97 100644 --- a/ui/src/i18n/.i18n/uk.meta.json +++ b/ui/src/i18n/.i18n/uk.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:16:57.851Z", + "generatedAt": "2026-05-04T07:44:18.980Z", "locale": "uk", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/uk.tm.jsonl b/ui/src/i18n/.i18n/uk.tm.jsonl index 12b16b964a8..e00bbf1f3a3 100644 --- a/ui/src/i18n/.i18n/uk.tm.jsonl +++ b/ui/src/i18n/.i18n/uk.tm.jsonl @@ -446,7 +446,6 @@ {"cache_key":"8457d317a6b5608182ff1b45324e60d3e19876266b4469f553999285245c6361","model":"gpt-5.4","provider":"openai","segment_id":"usage.daily.total","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Total","text_hash":"c9b3c38247f744e17dd26fda097d6a9ba9332586b6bdaa038bf8f313a863f2b8","tgt_lang":"uk","translated":"Усього","updated_at":"2026-04-05T17:22:48.249Z"} {"cache_key":"857af1f09b0ed6980a66675f5d4353c520e18c8db579f7395dfb2a19b8488a2b","model":"gpt-5.4","provider":"openai","segment_id":"cron.errors.webhookUrlInvalid","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Webhook URL must start with http:// or https://.","text_hash":"08a52ce0d5afdaa43d74ecefd749f61e6ecc3368a92a459f07bf85e612ac7dc1","tgt_lang":"uk","translated":"URL webhook має починатися з http:// або https://.","updated_at":"2026-04-05T17:23:56.109Z"} {"cache_key":"85edbdf68f8c7282523f4fee15384c76353f536ad7cc586b92987b96b5e6adf6","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.nameOptional","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Name (optional)","text_hash":"e09b37bcc4f301f48329f9e0c5ee747acf821d692e9aac10abea5970ba70b1b4","tgt_lang":"uk","translated":"Назва (необов’язково)","updated_at":"2026-04-29T20:15:28.360Z"} -{"cache_key":"864228a06c99a51e1a6f11af4ae65505e9860386c614ce444878f48196c0bdf9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"uk","translated":"Оновлено за останні N хвилин.","updated_at":"2026-05-04T07:16:57.699Z"} {"cache_key":"86d2c45fd2b36d9dee2485129c4ea44bdb9f275832afd930b7fa02bcbce88f28","model":"gpt-5.4","provider":"openai","segment_id":"overview.notes.tailscaleTitle","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Tailscale serve","text_hash":"a7446759d5c0164d0b327d23f369ff1bbe74a29611d1d5c0b763bc614b8e0d54","tgt_lang":"uk","translated":"Tailscale serve","updated_at":"2026-04-06T03:00:11.329Z"} {"cache_key":"874bedeb2a6252e5ded3656401d223f7b34ba3b728a5b1287fa821915b998296","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.status.nextSweepPrefix","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"next sweep","text_hash":"836b65b782a40d015ac29fa976e399ea979cc1c659c551f5de304c4004ed8dd4","tgt_lang":"uk","translated":"наступний цикл","updated_at":"2026-04-06T02:50:46.769Z"} {"cache_key":"8752259dba86f3d45fdc3d0587aac138768b904bdff5041d7c9a014c04433f9c","model":"gpt-5.4","provider":"openai","segment_id":"cron.errors.cronExprRequired","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Cron expression is required.","text_hash":"8fbe41c6aff5762238faf1f7bd7d9f99c0c82e7a932c3e9feeaf8d42c77f275d","tgt_lang":"uk","translated":"Вираз Cron є обов’язковим.","updated_at":"2026-04-05T17:23:53.283Z"} @@ -780,6 +779,7 @@ {"cache_key":"e6e16797dc7e464d40282a904840303c17678b14945bb813d96cf065682056f5","model":"gpt-5.4","provider":"openai","segment_id":"overview.access.password","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Password (not stored)","text_hash":"a693085108fe8ddea3acb78ba8ac0c275e593fc85db1c526006247ceb1372dda","tgt_lang":"uk","translated":"Пароль (не зберігається)","updated_at":"2026-04-05T17:22:27.181Z"} {"cache_key":"e70125c3c7fd6d8d3759fda4045523ff8284de6c2fe7e8c00c7be883bf52f884","model":"gpt-5.4","provider":"openai","segment_id":"usage.mosaic.dayOfWeek","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Day of Week","text_hash":"0f2148a98fb2064bb5194ba8ed3b453cd5e2bfdb8f1549509e16e8b9e94acb71","tgt_lang":"uk","translated":"День тижня","updated_at":"2026-04-05T17:23:17.582Z"} {"cache_key":"e76a7db6ba7bd165dace1931c04a5a59d7744eaa1652ff62d7a14cf0c4c643eb","model":"gpt-5.4","provider":"openai","segment_id":"overview.stats.sessionsHint","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Recent session keys tracked by the gateway.","text_hash":"83bd33680a568558e87978e9a13fac268dab203e5fc21ec61ecc04ee3b1c1fb5","tgt_lang":"uk","translated":"Нещодавні ключі сеансів, які відстежує шлюз.","updated_at":"2026-04-05T17:22:27.181Z"} +{"cache_key":"e806ac06c631540d73ef5212da182c2b16396d6c316981e9b49f89669267779a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"uk","translated":"Оновлено за останні {count} хвилин.","updated_at":"2026-05-04T07:16:57.699Z"} {"cache_key":"e82a0654dc01a86e14efdff9c0d6aeff58b84cf740d88efae6ea5c826f4ecd6d","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.unit","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Unit","text_hash":"4e545960f1bffc134026127ef92963e136ec84b24bb2a6103c0731a64843a40b","tgt_lang":"uk","translated":"Одиниця","updated_at":"2026-04-05T17:23:32.985Z"} {"cache_key":"e83eae8e9ded1d1633295991b0caf9efd8877172d7bf41f1e407df0664be1645","model":"gpt-5.4","provider":"openai","segment_id":"usage.presets.last30d","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"30d","text_hash":"e3ba17e322405f7f5887b350f7d398ab1c41fc5f7a758b7aab35bf23b1368ed6","tgt_lang":"uk","translated":"30 дн.","updated_at":"2026-04-05T17:22:39.086Z"} {"cache_key":"e9254220a367f4d4cec7db071078b69d8ef36fc580948423c1c4f581f0bb0f2a","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.whatHeading","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"What should it do?","text_hash":"1970bec54875f6bdd238a7b9e2889e98fb652edab85498a790d73c937e6cdb76","tgt_lang":"uk","translated":"Що потрібно зробити?","updated_at":"2026-04-29T20:15:28.359Z"} diff --git a/ui/src/i18n/.i18n/vi.meta.json b/ui/src/i18n/.i18n/vi.meta.json index 216c1dd12a4..ec150bc66e4 100644 --- a/ui/src/i18n/.i18n/vi.meta.json +++ b/ui/src/i18n/.i18n/vi.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:18:13.836Z", + "generatedAt": "2026-05-04T07:44:20.379Z", "locale": "vi", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/vi.tm.jsonl b/ui/src/i18n/.i18n/vi.tm.jsonl index e71bf240e3f..968bb0a9039 100644 --- a/ui/src/i18n/.i18n/vi.tm.jsonl +++ b/ui/src/i18n/.i18n/vi.tm.jsonl @@ -233,7 +233,6 @@ {"cache_key":"3e0142902a617f1a29c5cf95ebfb38306976e0f3f4dd630662d07dd79116a81e","model":"gpt-5.5","provider":"openai","segment_id":"usage.empty.hint","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Select a date range and click Refresh to load usage.","text_hash":"4dcf5dc94773068c4f25aea20473dffbbd254ea813f8890bd5bf233df13614a5","tgt_lang":"vi","translated":"Chọn khoảng ngày và nhấp Làm mới để tải mức sử dụng.","updated_at":"2026-04-29T17:40:51.798Z"} {"cache_key":"3e0d4fc15e5e098dbe559b67e73c0a0ad44fc69ad422800a5bdb1849a5c03f95","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.recentlyUpdated","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Recently updated","text_hash":"474b2a869ac1477d2c174d764815230c13edb7a9d194d5aa8ea349c6d0c9dee2","tgt_lang":"vi","translated":"Đã cập nhật gần đây","updated_at":"2026-04-29T17:41:32.334Z"} {"cache_key":"3ee5054dcd295974ec2432741e9d5d420ee6ac5f1dd82f9d65bac34c6402550c","model":"gpt-5.5","provider":"openai","segment_id":"chat.updateNow","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Update now","text_hash":"63bf045213cebbafc438a7a79e633015cbd047b8864eb2f9dffc45b641607048","tgt_lang":"vi","translated":"Cập nhật ngay","updated_at":"2026-04-29T20:16:47.550Z"} -{"cache_key":"3f0778d2a875f4f6a23d2564292d788568768b413a32bdf4efe94d42ced20bb6","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"vi","translated":"Đã cập nhật trong N phút gần đây.","updated_at":"2026-05-04T07:18:13.683Z"} {"cache_key":"3f0d97d68b6606f3f4606c04c4499f314e6cc78e3436b27364fffd54ef75e972","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.sessionsHint","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Distinct sessions in the range.","text_hash":"03ac814eb939f3f67105d4862c3c3b47a36dc5906b2fa1fbf50c8e2ff2ec1255","tgt_lang":"vi","translated":"Các phiên riêng biệt trong phạm vi.","updated_at":"2026-04-29T17:41:02.278Z"} {"cache_key":"3f4ec4a822b83fddb4f136d86ef1c0b9e8324e5a9cae0de91152be128dc1d65f","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.loading","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Loading...","text_hash":"47d2a515ef2f05b87d688656286a61e4f743da4b878684c7654969db17711c40","tgt_lang":"vi","translated":"Đang tải...","updated_at":"2026-04-29T17:41:37.285Z"} {"cache_key":"3f61ee30da9067019f0c81bbd0f8cf1920c428494822e798f0909006af4e51bd","model":"gpt-5.5","provider":"openai","segment_id":"channels.nostr.nip05Help","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Verifiable identifier (e.g., you@domain.com)","text_hash":"621809d0907c8a18fa79d4d21f7d41bed3ddccb2a2dd5cd134957ef4e7b3f0f3","tgt_lang":"vi","translated":"Mã định danh có thể xác minh (ví dụ: you@domain.com)","updated_at":"2026-04-29T17:39:43.419Z"} @@ -595,6 +594,7 @@ {"cache_key":"9a1bd19790962fa7baf05dd9d7b0123db53ff37e9b37381c19d081893a682dc5","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.tools","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Tools","text_hash":"ea93d6a262ecb87a9fa4d09edbd7654c046597936a8e235fc3949eb01775ff99","tgt_lang":"vi","translated":"Công cụ","updated_at":"2026-04-29T17:41:13.544Z"} {"cache_key":"9a7bfecfe0a3f124e1454eb519316cc0f57b78702a0b31a02fa1be133328b372","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.payloadKind","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"What should run?","text_hash":"f423c2d1d8d13f8f14f4da2f04d0e6182664f363edabbaddba2e82bc735989b1","tgt_lang":"vi","translated":"Cần chạy gì?","updated_at":"2026-04-29T17:41:55.225Z"} {"cache_key":"9a8d5810167e01229a56c6ba55ed89ab15b8eb73a92895fd0f876a4bd317781f","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.ascending","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Ascending","text_hash":"77184595bde3befc7f5a20efc97caea43f4858e4c97cd2ee406af2c61db3266c","tgt_lang":"vi","translated":"Tăng dần","updated_at":"2026-04-29T17:41:37.285Z"} +{"cache_key":"9a9fe755a279dabd22aa335904aad9cdde45ab304340ad7410febc8fbbb4b0b6","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"vi","translated":"Đã cập nhật trong {count} phút gần đây.","updated_at":"2026-05-04T07:18:13.683Z"} {"cache_key":"9ac30d39488bdad09e5e808562c15f88720dae47508b9a1482af4ec3804c0a59","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.systemEventHelp","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Sends your text to the gateway main timeline (good for reminders/triggers).","text_hash":"284a601bd74ca50e61fcf8ec9749af44936ad445a6098d38c63090b731b46508","tgt_lang":"vi","translated":"Gửi văn bản của bạn đến dòng thời gian chính của gateway (phù hợp cho lời nhắc/kích hoạt).","updated_at":"2026-04-29T17:41:55.225Z"} {"cache_key":"9aeb5dee40ee10c0ce3ef850f908b7dd671be257a158b53255172b3d5d09d02b","model":"gpt-5.5","provider":"openai","segment_id":"usage.sessions.noRecent","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"No recent sessions","text_hash":"100ac08064a6d5867a400a56b2949f9de3f6da4602a99461ee3a300c20273c1b","tgt_lang":"vi","translated":"Không có phiên gần đây","updated_at":"2026-04-29T17:41:10.252Z"} {"cache_key":"9af4d13c50c969a73bf2f8d3b632d954d25b637530db4f5fa06a00808c355911","model":"gpt-5.5","provider":"openai","segment_id":"cron.errors.invalidRunTime","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Invalid run time.","text_hash":"51465fa3cb94966411a49d8d1972fe997ac028fd249e05df55db8a2179975b48","tgt_lang":"vi","translated":"Thời gian chạy không hợp lệ.","updated_at":"2026-04-29T17:42:15.583Z"} diff --git a/ui/src/i18n/.i18n/zh-CN.meta.json b/ui/src/i18n/.i18n/zh-CN.meta.json index 511b574f8b2..14d293e226b 100644 --- a/ui/src/i18n/.i18n/zh-CN.meta.json +++ b/ui/src/i18n/.i18n/zh-CN.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:14:20.415Z", + "generatedAt": "2026-05-04T07:26:56.358Z", "locale": "zh-CN", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/zh-CN.tm.jsonl b/ui/src/i18n/.i18n/zh-CN.tm.jsonl index 33a7319e8ff..58af30c9b0e 100644 --- a/ui/src/i18n/.i18n/zh-CN.tm.jsonl +++ b/ui/src/i18n/.i18n/zh-CN.tm.jsonl @@ -270,6 +270,7 @@ {"cache_key":"7b77aa708aba597dfa4a4aeeda4895d4477fbc1c49ba7b35e39eca6dca492579","model":"gpt-5.4","provider":"openai","segment_id":"usage.details.modelMix","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Model Mix","text_hash":"4716263d5596745d99dafb4d7ce95bb8afd089368f8203741451c5915005293c","tgt_lang":"zh-CN","translated":"模型构成","updated_at":"2026-04-05T17:10:55.291Z"} {"cache_key":"7b817786b377cf2e8d48c719cb9f698017ffdc7d85039edc271341d9e64eb8d7","model":"gpt-5.4","provider":"openai","segment_id":"channels.nostr.bio","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Bio","text_hash":"3933b1802161254f41c59f2909f61ac994c086e1cde03848c4c310f45b5b4999","tgt_lang":"zh-CN","translated":"简介","updated_at":"2026-04-06T02:47:39.053Z"} {"cache_key":"7bb36e7232f269fa9a25b3ed662fd34dd7578e93f8e503f339d5c4c72dedd9be","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.stats.promoted","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Promoted","text_hash":"0cf04463c4276a6276986c22155bd4a32ce81e8dd162a657dedfa9afb97a7371","tgt_lang":"zh-CN","translated":"已提升","updated_at":"2026-04-08T18:36:23.701Z"} +{"cache_key":"7bf639c8f18ce9311294da25aebda48ca88847696cc6d882290acecda6cf3394","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"zh-CN","translated":"在过去 {count} 分钟内更新。","updated_at":"2026-05-04T07:14:20.263Z"} {"cache_key":"7c9fa3c7aa9280684daeb062533ef8def117bde212493bb7c9af5312a53fdf7a","model":"gpt-5.4","provider":"openai","segment_id":"common.lastInbound","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Last inbound","text_hash":"2df9c4ccfa36d15b18ab6a0d9268cc247a28626bda9566d4aecc2c3285f9c5b6","tgt_lang":"zh-CN","translated":"上次入站","updated_at":"2026-04-06T02:47:28.112Z"} {"cache_key":"7cc86ab76f2243d80b45da3364d42da5de909c57900313f899103dc0910ee4bd","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.noCheckpoints","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"No compaction checkpoints recorded for this session.","text_hash":"4fd4068bb85186ade93f7290efe22eaff1d648143f8ab6b0ee71cb2167bd9845","tgt_lang":"zh-CN","translated":"此会话没有记录压缩检查点。","updated_at":"2026-04-29T20:12:21.290Z"} {"cache_key":"7ce224c40d46495a4af92949b28512feee0c765ea6dced301f2f78c9d991613a","model":"gpt-5.4","provider":"openai","segment_id":"usage.empty.featureTimeline","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Timeline drilldown","text_hash":"f02787b793baa84fe08d54066fbe5cf694a7bfd5c3d5fbe4216e50f14d771db4","tgt_lang":"zh-CN","translated":"时间线钻取","updated_at":"2026-04-05T17:10:42.016Z"} @@ -459,7 +460,6 @@ {"cache_key":"cb95fcab358a86aace22102a91457c1f2c5ea61ba5cc5cb756d2c84a4649adde","model":"gpt-5.5","provider":"openai","segment_id":"chat.runningVersion","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"running v{version}","text_hash":"71317eeb4277d213186524c216b02af34724cd4ee7bd2531c24df009b373519e","tgt_lang":"zh-CN","translated":"正在运行 v{version}","updated_at":"2026-04-29T20:12:25.211Z"} {"cache_key":"cbefe314a3094c91c60651dfe795a0607076df2964f36c48cfb88b17fe530ebc","model":"gpt-5.4","provider":"openai","segment_id":"common.probe","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Probe","text_hash":"3bd51ab9c14f9514ea37fac91f5f245e93cf5733bd39ca1652e5525a1d67b5d1","tgt_lang":"zh-CN","translated":"探测","updated_at":"2026-04-06T02:47:28.112Z"} {"cache_key":"cc49b023a5228cbd2d3d586ab70bcfa2d4092ff6f3f205ed0cee47cd15bbbd00","model":"gpt-5.4","provider":"openai","segment_id":"usage.details.baseContextPerMessage","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Base context per message","text_hash":"f97ff4c2483a2174935304524775bc8191237e0bd314d05470c8b1f30ce435b6","tgt_lang":"zh-CN","translated":"每条消息的基础上下文","updated_at":"2026-04-05T17:10:59.816Z"} -{"cache_key":"ccf35b9c4b064a4013f3b28ff0d4015ceb9269267b0efcd55de4c80badfca1ea","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"zh-CN","translated":"在过去 N 分钟内更新。","updated_at":"2026-05-04T07:14:20.263Z"} {"cache_key":"cd3313de5701ba6c03a680f1440ca5f133180d85a9c09ac0b6b2735e200cc3fd","model":"gpt-5.4","provider":"openai","segment_id":"common.unsavedChanges","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"You have unsaved changes","text_hash":"a4b17bc7db59e76b073a344d84ce06457042dde8c293cf91b4a994db2de58da7","tgt_lang":"zh-CN","translated":"你有未保存的更改","updated_at":"2026-04-06T02:47:30.960Z"} {"cache_key":"cde0712b745bcbbcfd09ddb3d710a2356ca279a19531501b743c693dba437ec8","model":"gpt-5.4","provider":"openai","segment_id":"common.refreshing","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Refreshing…","text_hash":"1c0def7be0607b966b89e4974da38090472d8ada625f5b4c89f25b09d39683bd","tgt_lang":"zh-CN","translated":"刷新中…","updated_at":"2026-04-06T02:47:28.112Z"} {"cache_key":"ce1b92a0385fc8a6620f475e43957c6a66f93e7d7aef336865397f8634f954c4","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.user","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"user","text_hash":"04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb","tgt_lang":"zh-CN","translated":"用户","updated_at":"2026-04-05T17:10:45.876Z"} diff --git a/ui/src/i18n/.i18n/zh-TW.meta.json b/ui/src/i18n/.i18n/zh-TW.meta.json index 477acd88ec7..722b1ffcec2 100644 --- a/ui/src/i18n/.i18n/zh-TW.meta.json +++ b/ui/src/i18n/.i18n/zh-TW.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-04T07:14:18.066Z", + "generatedAt": "2026-05-04T07:26:57.083Z", "locale": "zh-TW", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "66b99bdd39fc9dae0476c0e2fd2c0392f068bc3f7f91b532b04a106f14a3f3df", + "sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6", "totalKeys": 1001, "translatedKeys": 1001, "workflow": 1 diff --git a/ui/src/i18n/.i18n/zh-TW.tm.jsonl b/ui/src/i18n/.i18n/zh-TW.tm.jsonl index 82c916dbe6d..8038c9aa26f 100644 --- a/ui/src/i18n/.i18n/zh-TW.tm.jsonl +++ b/ui/src/i18n/.i18n/zh-TW.tm.jsonl @@ -182,6 +182,7 @@ {"cache_key":"3fb640b1411a76a273b3e2868d0a3371adac856e2042d72b954d2bf399dbe9ac","model":"gpt-5.4","provider":"openai","segment_id":"usage.overview.throughput","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Throughput","text_hash":"960bcc4e48b929b89a54da1613c577f938e27adffd9fefc84b176a081eba5ae6","tgt_lang":"zh-TW","translated":"吞吐量","updated_at":"2026-04-05T17:10:47.369Z"} {"cache_key":"3fb9995e321eb70abcc8472416e22c7f1f95d05b34309c150eb6d96baa08a2e4","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.stats.phaseHits","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Phase Hits","text_hash":"7048bb922818ecab86930a1e134b4a9cd165faca3cbe48c9af93d7bc5bcf407d","tgt_lang":"zh-TW","translated":"階段命中","updated_at":"2026-04-06T02:47:44.708Z"} {"cache_key":"3fba4e3596456f81848384aaef8d6f895fbee0c98d355497f5dcc7352600d366","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.tokenDeltaUnavailable","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"token delta unavailable","text_hash":"0f6bf09152fcc457d482589f3ed28fcc8e7969943ed92e780d1b2f62f6bacc5d","tgt_lang":"zh-TW","translated":"token 差異無法使用","updated_at":"2026-04-29T20:12:27.417Z"} +{"cache_key":"3fd6b93564c27e24a546bdadced820f3ccaf33ff2dfae65782779e1b14c718c1","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Updated in the last {count} minutes.","text_hash":"834380b8003d966cdc8cd20851a68ecbd084fe0a8eb69fa5febf1b796e1c8001","tgt_lang":"zh-TW","translated":"在過去 {count} 分鐘內更新。","updated_at":"2026-05-04T07:14:17.913Z"} {"cache_key":"405913cff2ef67925f681880b86f7bdf1b4aa749bcc303ee57785caa2d94d89b","model":"gpt-5.4","provider":"openai","segment_id":"dreaming.advanced.promotedTitle","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Recent Promotions","text_hash":"85051af6bfc0dd7be0988540e19a83f9855e93be2642c8b39a3d9a352ede92ff","tgt_lang":"zh-TW","translated":"最近提升項目","updated_at":"2026-04-10T07:58:31.101Z"} {"cache_key":"406f5ba43c4524f334fb2462fa9f82f7c97a30717fb52857118570d29986463c","model":"gpt-5.4","provider":"openai","segment_id":"common.showAdvanced","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Show Advanced","text_hash":"365075d1bf3ed18878ba0bb50360278b7eaa5973d32ed92fa1544238c09254cb","tgt_lang":"zh-TW","translated":"顯示進階選項","updated_at":"2026-04-06T02:47:27.523Z"} {"cache_key":"40b49b0a3eca3f469347bd886320202ef5f003d2b48f9871a83f3330a7aa3015","model":"gpt-5.4","provider":"openai","segment_id":"cron.jobList.disabled","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"disabled","text_hash":"17eb3c0168d0d7b21ede5481150f17233427d89833ec121b4dbc4fb96cfab71e","tgt_lang":"zh-TW","translated":"已停用","updated_at":"2026-04-05T17:11:44.614Z"} @@ -533,7 +534,6 @@ {"cache_key":"b3d301cbf11eba583e11db3328a75544fbf072655e556e6af0e7c98343ef8ef7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.showArchivedTooltip","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Include archived sessions.","text_hash":"63efdebecf9e6329ec99dec54bc2515758ccf019aa6eefe48619356b17232223","tgt_lang":"zh-TW","translated":"包含已封存工作階段。","updated_at":"2026-05-04T07:14:17.913Z"} {"cache_key":"b3d605bc167ceb0536d65bec566f2397c44a2faf8e77ae98e248f4ef9cd1276e","model":"gpt-5.4","provider":"openai","segment_id":"sessionsView.filters","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Filters","text_hash":"546ebb8eb993ea561029d9febd84c363bdb09010bb2cb915a8287762b76b9a64","tgt_lang":"zh-TW","translated":"篩選條件","updated_at":"2026-04-05T17:10:31.705Z"} {"cache_key":"b44759978cb549a8f29d8066804c61830059f0b604049eba195f1d4334d26092","model":"gpt-5.4","provider":"openai","segment_id":"cron.form.timezonePlaceholder","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"America/Los_Angeles","text_hash":"2d4bbedff807854084b7855fd6e0d49ab55b41e8c9395debd40d0e8e1d3390cf","tgt_lang":"zh-TW","translated":"America/Los_Angeles","updated_at":"2026-04-06T02:59:17.485Z"} -{"cache_key":"b4a9d902b077a5d72edb2f437d8e0957e01dd186170370a198171a4f7e1aad8c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Updated in the last N minutes.","text_hash":"10658b360349f0f80236c593873412696d2bca2fad7ecd71b977b346697363fd","tgt_lang":"zh-TW","translated":"在過去 N 分鐘內更新。","updated_at":"2026-05-04T07:14:17.913Z"} {"cache_key":"b4cb2889b6ce8d4b3933053db7ac9586bfa640f87e40933783e388ed8e9d9c26","model":"gpt-5.4","provider":"openai","segment_id":"usage.metrics.tokens","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Tokens","text_hash":"a039dfb9628b53ddaebcfe8ef0793e3fdf19867601295f00d192acef59050869","tgt_lang":"zh-TW","translated":"Token","updated_at":"2026-04-05T17:10:31.705Z"} {"cache_key":"b5a677e7111689d2cac8ff845e690ca219e4fc96480dab57613facb3dc16376a","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.delivery.silent.label","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Silent","text_hash":"ddbcf06726488a43af36838754808ac5041b05ab6434735615979d820725b56f","tgt_lang":"zh-TW","translated":"靜默","updated_at":"2026-04-29T20:12:31.105Z"} {"cache_key":"b5d531c3d1c9187489cbca7ed756368ee0f2e672fc361741552c8c71639bfe58","model":"gpt-5.4","provider":"openai","segment_id":"usage.sessions.avg","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"avg","text_hash":"ca5c8585b0760a760e0b887800360306b60288aa8581d4800ab42bc2c0d591a5","tgt_lang":"zh-TW","translated":"平均","updated_at":"2026-04-05T17:10:54.968Z"} diff --git a/ui/src/i18n/locales/ar.ts b/ui/src/i18n/locales/ar.ts index 7d02795ded3..11760db6e30 100644 --- a/ui/src/i18n/locales/ar.ts +++ b/ui/src/i18n/locales/ar.ts @@ -166,7 +166,7 @@ export const ar: TranslationMap = { global: "عام", unknown: "غير معروف", showArchived: "إظهار المؤرشفة", - activeTooltip: "تم التحديث خلال آخر N دقيقة.", + activeTooltip: "تم التحديث خلال آخر {count} دقيقة.", limitTooltip: "الحد الأقصى للجلسات المراد تحميلها.", globalTooltip: "تضمين الجلسات العامة.", unknownTooltip: "تضمين الجلسات غير المعروفة.", diff --git a/ui/src/i18n/locales/de.ts b/ui/src/i18n/locales/de.ts index c574b453960..809f29e4d15 100644 --- a/ui/src/i18n/locales/de.ts +++ b/ui/src/i18n/locales/de.ts @@ -170,7 +170,7 @@ export const de: TranslationMap = { global: "Global", unknown: "Unbekannt", showArchived: "Archivierte anzeigen", - activeTooltip: "In den letzten N Minuten aktualisiert.", + activeTooltip: "In den letzten {count} Minuten aktualisiert.", limitTooltip: "Maximale Anzahl zu ladender Sitzungen.", globalTooltip: "Globale Sitzungen einbeziehen.", unknownTooltip: "Unbekannte Sitzungen einbeziehen.", diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 8d82b6c1b46..b69763475c5 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -165,7 +165,7 @@ export const en: TranslationMap = { global: "Global", unknown: "Unknown", showArchived: "Show archived", - activeTooltip: "Updated in the last N minutes.", + activeTooltip: "Updated in the last {count} minutes.", limitTooltip: "Max sessions to load.", globalTooltip: "Include global sessions.", unknownTooltip: "Include unknown sessions.", diff --git a/ui/src/i18n/locales/es.ts b/ui/src/i18n/locales/es.ts index df1cb8078c6..ee618ca1f1a 100644 --- a/ui/src/i18n/locales/es.ts +++ b/ui/src/i18n/locales/es.ts @@ -167,7 +167,7 @@ export const es: TranslationMap = { global: "Global", unknown: "Desconocido", showArchived: "Mostrar archivadas", - activeTooltip: "Actualizadas en los últimos N minutos.", + activeTooltip: "Actualizadas en los últimos {count} minutos.", limitTooltip: "Máximo de sesiones para cargar.", globalTooltip: "Incluir sesiones globales.", unknownTooltip: "Incluir sesiones desconocidas.", diff --git a/ui/src/i18n/locales/fa.ts b/ui/src/i18n/locales/fa.ts index 2d8225d31c7..443c5904aee 100644 --- a/ui/src/i18n/locales/fa.ts +++ b/ui/src/i18n/locales/fa.ts @@ -168,7 +168,7 @@ export const fa: TranslationMap = { global: "سراسری", unknown: "نامشخص", showArchived: "نمایش بایگانیشدهها", - activeTooltip: "در N دقیقهٔ گذشته بهروزرسانی شده است.", + activeTooltip: "در {count} دقیقهٔ گذشته بهروزرسانی شده است.", limitTooltip: "حداکثر جلسههایی که بارگیری میشوند.", globalTooltip: "شامل جلسههای سراسری شود.", unknownTooltip: "شامل جلسههای ناشناخته شود.", diff --git a/ui/src/i18n/locales/fr.ts b/ui/src/i18n/locales/fr.ts index b0c27e09b05..21d2cdc7e31 100644 --- a/ui/src/i18n/locales/fr.ts +++ b/ui/src/i18n/locales/fr.ts @@ -169,7 +169,7 @@ export const fr: TranslationMap = { global: "Global", unknown: "Inconnu", showArchived: "Afficher les sessions archivées", - activeTooltip: "Mis à jour au cours des N dernières minutes.", + activeTooltip: "Mis à jour au cours des {count} dernières minutes.", limitTooltip: "Nombre maximal de sessions à charger.", globalTooltip: "Inclure les sessions globales.", unknownTooltip: "Inclure les sessions inconnues.", diff --git a/ui/src/i18n/locales/id.ts b/ui/src/i18n/locales/id.ts index 3d335050084..80413085756 100644 --- a/ui/src/i18n/locales/id.ts +++ b/ui/src/i18n/locales/id.ts @@ -167,7 +167,7 @@ export const id: TranslationMap = { global: "Global", unknown: "Tidak diketahui", showArchived: "Tampilkan yang diarsipkan", - activeTooltip: "Diperbarui dalam N menit terakhir.", + activeTooltip: "Diperbarui dalam {count} menit terakhir.", limitTooltip: "Jumlah sesi maksimum untuk dimuat.", globalTooltip: "Sertakan sesi global.", unknownTooltip: "Sertakan sesi tidak dikenal.", diff --git a/ui/src/i18n/locales/it.ts b/ui/src/i18n/locales/it.ts index 0a35d3238e9..7a549639a0d 100644 --- a/ui/src/i18n/locales/it.ts +++ b/ui/src/i18n/locales/it.ts @@ -167,7 +167,7 @@ export const it: TranslationMap = { global: "Globale", unknown: "Sconosciuto", showArchived: "Mostra archiviate", - activeTooltip: "Aggiornate negli ultimi N minuti.", + activeTooltip: "Aggiornate negli ultimi {count} minuti.", limitTooltip: "Numero massimo di sessioni da caricare.", globalTooltip: "Includi sessioni globali.", unknownTooltip: "Includi sessioni sconosciute.", diff --git a/ui/src/i18n/locales/ja-JP.ts b/ui/src/i18n/locales/ja-JP.ts index b4a5fe3de70..ebf9d0d90f6 100644 --- a/ui/src/i18n/locales/ja-JP.ts +++ b/ui/src/i18n/locales/ja-JP.ts @@ -170,7 +170,7 @@ export const ja_JP: TranslationMap = { global: "グローバル", unknown: "不明", showArchived: "アーカイブ済みを表示", - activeTooltip: "過去 N 分以内に更新されました。", + activeTooltip: "過去 {count} 分以内に更新されました。", limitTooltip: "読み込むセッションの最大数。", globalTooltip: "グローバルセッションを含めます。", unknownTooltip: "不明なセッションを含めます。", diff --git a/ui/src/i18n/locales/ko.ts b/ui/src/i18n/locales/ko.ts index 80a659ec15d..36f0d0af1e2 100644 --- a/ui/src/i18n/locales/ko.ts +++ b/ui/src/i18n/locales/ko.ts @@ -166,7 +166,7 @@ export const ko: TranslationMap = { global: "전역", unknown: "알 수 없음", showArchived: "보관된 세션 표시", - activeTooltip: "최근 N분 이내에 업데이트됨.", + activeTooltip: "최근 {count}분 이내에 업데이트됨.", limitTooltip: "로드할 최대 세션 수.", globalTooltip: "전역 세션 포함.", unknownTooltip: "알 수 없는 세션 포함.", diff --git a/ui/src/i18n/locales/nl.ts b/ui/src/i18n/locales/nl.ts index 4f59a97bcb6..ee22b38c82e 100644 --- a/ui/src/i18n/locales/nl.ts +++ b/ui/src/i18n/locales/nl.ts @@ -169,7 +169,7 @@ export const nl: TranslationMap = { global: "Globaal", unknown: "Onbekend", showArchived: "Gearchiveerde tonen", - activeTooltip: "Bijgewerkt in de afgelopen N minuten.", + activeTooltip: "Bijgewerkt in de afgelopen {count} minuten.", limitTooltip: "Maximaal aantal te laden sessies.", globalTooltip: "Globale sessies opnemen.", unknownTooltip: "Onbekende sessies opnemen.", diff --git a/ui/src/i18n/locales/pl.ts b/ui/src/i18n/locales/pl.ts index ba7327642bd..bc9bee859f2 100644 --- a/ui/src/i18n/locales/pl.ts +++ b/ui/src/i18n/locales/pl.ts @@ -168,7 +168,7 @@ export const pl: TranslationMap = { global: "Globalne", unknown: "Nieznane", showArchived: "Pokaż zarchiwizowane", - activeTooltip: "Zaktualizowano w ciągu ostatnich N minut.", + activeTooltip: "Zaktualizowano w ciągu ostatnich {count} minut.", limitTooltip: "Maksymalna liczba sesji do wczytania.", globalTooltip: "Uwzględnij sesje globalne.", unknownTooltip: "Uwzględnij nieznane sesje.", diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index df928cd5cc2..25480c40010 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -167,7 +167,7 @@ export const pt_BR: TranslationMap = { global: "Global", unknown: "Desconhecido", showArchived: "Mostrar arquivadas", - activeTooltip: "Atualizadas nos últimos N minutos.", + activeTooltip: "Atualizadas nos últimos {count} minutos.", limitTooltip: "Máximo de sessões a carregar.", globalTooltip: "Incluir sessões globais.", unknownTooltip: "Incluir sessões desconhecidas.", diff --git a/ui/src/i18n/locales/th.ts b/ui/src/i18n/locales/th.ts index 7de07644dea..648427c7e59 100644 --- a/ui/src/i18n/locales/th.ts +++ b/ui/src/i18n/locales/th.ts @@ -165,7 +165,7 @@ export const th: TranslationMap = { global: "ส่วนกลาง", unknown: "ไม่ทราบ", showArchived: "แสดงที่เก็บถาวร", - activeTooltip: "อัปเดตในช่วง N นาทีที่ผ่านมา", + activeTooltip: "อัปเดตในช่วง {count} นาทีที่ผ่านมา", limitTooltip: "จำนวนเซสชันสูงสุดที่จะโหลด", globalTooltip: "รวมเซสชันส่วนกลาง", unknownTooltip: "รวมเซสชันที่ไม่รู้จัก", diff --git a/ui/src/i18n/locales/tr.ts b/ui/src/i18n/locales/tr.ts index 1a66139b3cc..67a50034d2d 100644 --- a/ui/src/i18n/locales/tr.ts +++ b/ui/src/i18n/locales/tr.ts @@ -169,7 +169,7 @@ export const tr: TranslationMap = { global: "Genel", unknown: "Bilinmiyor", showArchived: "Arşivlenmişleri göster", - activeTooltip: "Son N dakika içinde güncellendi.", + activeTooltip: "Son {count} dakika içinde güncellendi.", limitTooltip: "Yüklenecek maksimum oturum sayısı.", globalTooltip: "Genel oturumları dahil et.", unknownTooltip: "Bilinmeyen oturumları dahil et.", diff --git a/ui/src/i18n/locales/uk.ts b/ui/src/i18n/locales/uk.ts index c735879a524..7c5e18442fb 100644 --- a/ui/src/i18n/locales/uk.ts +++ b/ui/src/i18n/locales/uk.ts @@ -168,7 +168,7 @@ export const uk: TranslationMap = { global: "Глобально", unknown: "Невідомо", showArchived: "Показати архівовані", - activeTooltip: "Оновлено за останні N хвилин.", + activeTooltip: "Оновлено за останні {count} хвилин.", limitTooltip: "Максимальна кількість сеансів для завантаження.", globalTooltip: "Включити глобальні сеанси.", unknownTooltip: "Включити невідомі сеанси.", diff --git a/ui/src/i18n/locales/vi.ts b/ui/src/i18n/locales/vi.ts index f549f05d3e0..c17f0459576 100644 --- a/ui/src/i18n/locales/vi.ts +++ b/ui/src/i18n/locales/vi.ts @@ -167,7 +167,7 @@ export const vi: TranslationMap = { global: "Toàn cục", unknown: "Không rõ", showArchived: "Hiển thị đã lưu trữ", - activeTooltip: "Đã cập nhật trong N phút gần đây.", + activeTooltip: "Đã cập nhật trong {count} phút gần đây.", limitTooltip: "Số phiên tối đa cần tải.", globalTooltip: "Bao gồm các phiên toàn cục.", unknownTooltip: "Bao gồm các phiên không xác định.", diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index 075e0edc90f..11b381d1680 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -165,7 +165,7 @@ export const zh_CN: TranslationMap = { global: "全局", unknown: "未知", showArchived: "显示已归档", - activeTooltip: "在过去 N 分钟内更新。", + activeTooltip: "在过去 {count} 分钟内更新。", limitTooltip: "要加载的最大会话数。", globalTooltip: "包含全局会话。", unknownTooltip: "包含未知会话。", diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index 0e75e329909..f6b9a385d78 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -165,7 +165,7 @@ export const zh_TW: TranslationMap = { global: "全域", unknown: "未知", showArchived: "顯示已封存項目", - activeTooltip: "在過去 N 分鐘內更新。", + activeTooltip: "在過去 {count} 分鐘內更新。", limitTooltip: "要載入的工作階段上限。", globalTooltip: "包含全域工作階段。", unknownTooltip: "包含未知工作階段。", diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 6a1f1fd82f6..f13489efbda 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -1008,6 +1008,10 @@ align-items: start; } +.cron-workspace--form-collapsed { + grid-template-columns: minmax(0, 1fr) 64px; +} + .cron-workspace-main { display: grid; gap: 16px; @@ -1021,12 +1025,62 @@ overflow-y: auto; } +.cron-workspace-form--collapsed { + min-height: 180px; + overflow: hidden; + padding: 10px; +} + +.cron-form-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.cron-form-header__copy { + min-width: 0; +} + +.cron-form-collapse-toggle { + flex: 0 0 auto; + width: 34px; + height: 34px; + padding: 0; + justify-content: center; +} + +.cron-workspace-form--collapsed .cron-form-header { + min-height: 160px; + flex-direction: column; + align-items: center; + justify-content: space-between; +} + +.cron-workspace-form--collapsed .cron-form-header__copy { + display: flex; + justify-content: center; + writing-mode: vertical-rl; + transform: rotate(180deg); + white-space: nowrap; +} + +.cron-workspace-form--collapsed .card-title { + font-size: 13px; +} + .cron-form { margin-top: 16px; display: grid; gap: 14px; } +.cron-form[hidden], +.cron-form-status[hidden], +.cron-form-actions[hidden] { + display: none; +} + .cron-form-section { border: 1px solid var(--border); border-radius: var(--radius-md); @@ -1399,6 +1453,25 @@ order: -1; } + .cron-workspace--form-collapsed { + grid-template-columns: 1fr; + } + + .cron-workspace-form--collapsed { + min-height: 0; + } + + .cron-workspace-form--collapsed .cron-form-header { + min-height: 0; + flex-direction: row; + align-items: center; + } + + .cron-workspace-form--collapsed .cron-form-header__copy { + writing-mode: horizontal-tb; + transform: none; + } + .cron-form-grid { grid-template-columns: 1fr; gap: 12px; @@ -5578,7 +5651,18 @@ td.data-table-key-col { .ov-access-grid { display: grid; gap: 12px; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); +} + +.ov-access-grid .field { + min-width: 0; +} + +.ov-access-grid .field input, +.ov-access-grid .field select { + box-sizing: border-box; + min-width: 0; + width: 100%; } .ov-access-grid__full { diff --git a/ui/src/styles/components.test.ts b/ui/src/styles/components.test.ts index 12b05734409..a0a42249a8b 100644 --- a/ui/src/styles/components.test.ts +++ b/ui/src/styles/components.test.ts @@ -34,3 +34,17 @@ describe("sessions filter styles", () => { expect(css).toContain(".sessions-filter-bar {\n flex-direction: column;"); }); }); + +describe("overview access grid styles", () => { + it("keeps access fields and native controls within the card", () => { + const css = readComponentsCss(); + + expect(css).toContain( + "grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));", + ); + expect(css).toContain(".ov-access-grid .field {\n min-width: 0;"); + expect(css).toContain(".ov-access-grid .field input,\n.ov-access-grid .field select {"); + expect(css).toContain("box-sizing: border-box;"); + expect(css).toContain("width: 100%;"); + }); +}); diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts index 2992be4a0b3..36a16a3ff9e 100644 --- a/ui/src/ui/app-render.helpers.node.test.ts +++ b/ui/src/ui/app-render.helpers.node.test.ts @@ -813,9 +813,9 @@ describe("switchChatSession", () => { chatQueue: [{ id: "queued", text: "message B", createdAt: 1 }], chatQueueBySession: {}, chatRunId: "run-1", + sessionsShowArchived: false, chatSideResultTerminalRuns: new Set(["btw-run-1"]), chatStreamStartedAt: 1, - sessionsShowArchived: false, settings, applySettings(next: typeof settings) { state.settings = next; @@ -878,6 +878,7 @@ describe("switchChatSession", () => { chatQueue: [{ id: "queued-1", text: "message B", createdAt: 1 }], chatQueueBySession: {}, chatRunId: "run-1", + sessionsShowArchived: false, chatSideResultTerminalRuns: new Set(), chatStreamStartedAt: 1, settings, @@ -922,6 +923,7 @@ describe("switchChatSession", () => { chatQueue: [], chatQueueBySession: {}, chatRunId: null, + sessionsShowArchived: false, chatSideResultTerminalRuns: new Set(), chatStreamStartedAt: null, settings, @@ -952,19 +954,23 @@ describe("switchChatSession", () => { describe("dismissChatError", () => { it("clears persistent Talk error state", () => { + const stop = vi.fn(); const state = { lastError: 'Realtime voice provider "openai" is not configured', lastErrorCode: "UNAVAILABLE", - realtimeTalkActive: false, + realtimeTalkActive: true, + realtimeTalkSession: { stop }, realtimeTalkStatus: "error", realtimeTalkDetail: 'Realtime voice provider "openai" is not configured', realtimeTalkTranscript: "partial transcript", - } as AppViewState; + } as unknown as AppViewState & { realtimeTalkSession: { stop(): void } | null }; dismissChatError(state); expect(state.lastError).toBeNull(); expect(state.lastErrorCode).toBeNull(); + expect(stop).toHaveBeenCalledOnce(); + expect(state.realtimeTalkSession).toBeNull(); expect(state.realtimeTalkActive).toBe(false); expect(state.realtimeTalkStatus).toBe("idle"); expect(state.realtimeTalkDetail).toBeNull(); diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 468611f0efb..a6317c706dc 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -594,6 +594,11 @@ export function dismissChatError(state: AppViewState) { state.lastError = null; state.lastErrorCode = null; if (state.realtimeTalkStatus === "error") { + const talkHost = state as unknown as { + realtimeTalkSession?: { stop(): void } | null; + }; + talkHost.realtimeTalkSession?.stop(); + talkHost.realtimeTalkSession = null; state.realtimeTalkActive = false; state.realtimeTalkStatus = "idle"; state.realtimeTalkDetail = null; diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 950f767cd70..e6f25f99285 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -1820,6 +1820,7 @@ export function renderApp(state: AppViewState) { error: state.cronError, busy: state.cronBusy, form: state.cronForm, + cronFormCollapsed: state.cronFormCollapsed, channels: state.channelsSnapshot?.channelMeta?.length ? state.channelsSnapshot.channelMeta.map((entry) => entry.id) : (state.channelsSnapshot?.channelOrder ?? []), @@ -1850,9 +1851,18 @@ export function renderApp(state: AppViewState) { }, onRefresh: () => state.loadCron(), onAdd: () => addCronJob(state), - onEdit: (job) => startCronEdit(state, job), - onClone: (job) => startCronClone(state, job), + onEdit: (job) => { + state.cronFormCollapsed = false; + startCronEdit(state, job); + }, + onClone: (job) => { + state.cronFormCollapsed = false; + startCronClone(state, job); + }, onCancelEdit: () => cancelCronEdit(state), + onToggleFormCollapsed: (collapsed) => { + state.cronFormCollapsed = collapsed; + }, onToggle: (job, enabled) => toggleCronJob(state, job, enabled), onRun: (job, mode) => runCronJob(state, job, mode ?? "force"), onRemove: (job) => removeCronJob(state, job), diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 17970c77840..e9e0d9561a7 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -331,6 +331,7 @@ export type AppViewState = { | "cronStatus" | "cronError" | "cronForm" + | "cronFormCollapsed" | "cronFieldErrors" | "cronEditingJobId" | "cronRunsJobId" diff --git a/ui/src/ui/app.talk.test.ts b/ui/src/ui/app.talk.test.ts new file mode 100644 index 00000000000..fc702b3e9b9 --- /dev/null +++ b/ui/src/ui/app.talk.test.ts @@ -0,0 +1,57 @@ +/* @vitest-environment jsdom */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { realtimeTalkCtor, startMock, stopMock } = vi.hoisted(() => ({ + realtimeTalkCtor: vi.fn(), + startMock: vi.fn(), + stopMock: vi.fn(), +})); + +vi.mock("./chat/realtime-talk.ts", () => ({ + RealtimeTalkSession: realtimeTalkCtor, +})); + +describe("OpenClawApp Talk controls", () => { + beforeEach(() => { + realtimeTalkCtor.mockReset(); + startMock.mockReset(); + stopMock.mockReset(); + realtimeTalkCtor.mockImplementation( + function MockRealtimeTalkSession(this: { start: typeof startMock; stop: typeof stopMock }) { + this.start = startMock; + this.stop = stopMock; + }, + ); + startMock.mockResolvedValue(undefined); + }); + + it("retries Talk immediately when the previous session is already in error state", async () => { + const { OpenClawApp } = await import("./app.ts"); + const app = new OpenClawApp() as unknown as { + client: unknown; + connected: boolean; + realtimeTalkActive: boolean; + realtimeTalkStatus: string; + realtimeTalkSession: { stop(): void } | null; + sessionKey: string; + toggleRealtimeTalk(): Promise; + }; + const staleStop = vi.fn(); + app.client = { request: vi.fn() } as never; + app.connected = true; + app.sessionKey = "main"; + app.realtimeTalkActive = true; + app.realtimeTalkStatus = "error"; + app.realtimeTalkSession = { stop: staleStop }; + + await app.toggleRealtimeTalk(); + + expect(staleStop).toHaveBeenCalledOnce(); + expect(realtimeTalkCtor).toHaveBeenCalledOnce(); + expect(startMock).toHaveBeenCalledOnce(); + expect(stopMock).not.toHaveBeenCalled(); + expect(app.realtimeTalkStatus).toBe("connecting"); + expect(app.realtimeTalkSession).not.toBeNull(); + }); +}); diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 34d6ae3003c..20f1cda370a 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -472,6 +472,7 @@ export class OpenClawApp extends LitElement { @state() cronStatus: CronStatus | null = null; @state() cronError: string | null = null; @state() cronForm: CronFormState = { ...DEFAULT_CRON_FORM }; + @state() cronFormCollapsed = false; @state() cronFieldErrors: import("./controllers/cron.js").CronFieldErrors = {}; @state() cronEditingJobId: string | null = null; @state() cronRunsJobId: string | null = null; @@ -909,13 +910,18 @@ export class OpenClawApp extends LitElement { async toggleRealtimeTalk() { if (this.realtimeTalkSession) { - this.realtimeTalkSession.stop(); - this.realtimeTalkSession = null; - this.realtimeTalkActive = false; - this.realtimeTalkStatus = "idle"; - this.realtimeTalkDetail = null; - this.realtimeTalkTranscript = null; - return; + if (this.realtimeTalkStatus === "error") { + this.realtimeTalkSession.stop(); + this.realtimeTalkSession = null; + } else { + this.realtimeTalkSession.stop(); + this.realtimeTalkSession = null; + this.realtimeTalkActive = false; + this.realtimeTalkStatus = "idle"; + this.realtimeTalkDetail = null; + this.realtimeTalkTranscript = null; + return; + } } if (!this.client || !this.connected) { this.lastError = "Gateway not connected"; diff --git a/ui/src/ui/control-ui-performance.test.ts b/ui/src/ui/control-ui-performance.test.ts index d8688639e8b..80368a47af6 100644 --- a/ui/src/ui/control-ui-performance.test.ts +++ b/ui/src/ui/control-ui-performance.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import type { EventLogEntry } from "./app-events.ts"; import { recordControlUiPerformanceEvent, startControlUiResponsivenessObserver, @@ -46,8 +47,8 @@ function installPerformanceObserverMock(options: { function createHost() { return { tab: "chat" as const, - eventLog: [] as Array<{ payload: Record }>, - eventLogBuffer: [] as Array<{ payload: Record }>, + eventLog: [] as EventLogEntry[], + eventLogBuffer: [] as EventLogEntry[], }; } @@ -161,6 +162,35 @@ describe("startControlUiResponsivenessObserver", () => { ]); }); + it("caps responsiveness events so gateway events stay visible", () => { + vi.spyOn(console, "warn").mockImplementation(() => undefined); + const mock = installPerformanceObserverMock({ + supportedEntryTypes: ["longtask"], + }); + const host = createHost(); + + for (let i = 0; i < 225; i += 1) { + recordControlUiPerformanceEvent(host, "gateway.event", { i }, { console: false }); + } + + startControlUiResponsivenessObserver(host); + for (let i = 0; i < 80; i += 1) { + mock.emit([ + { + name: "self", + startTime: i, + duration: 51, + } as unknown as PerformanceEntry, + ]); + } + + expect(host.eventLogBuffer).toHaveLength(250); + expect( + host.eventLogBuffer.filter((entry) => entry.event === "control-ui.longtask"), + ).toHaveLength(50); + expect(host.eventLogBuffer.some((entry) => entry.event === "gateway.event")).toBe(true); + }); + it("returns null when responsiveness entries are unsupported or observe fails", () => { installPerformanceObserverMock({ supportedEntryTypes: [] }); expect(startControlUiResponsivenessObserver(createHost())).toBeNull(); diff --git a/ui/src/ui/control-ui-performance.ts b/ui/src/ui/control-ui-performance.ts index 32cfb7a96ca..e34e38b892a 100644 --- a/ui/src/ui/control-ui-performance.ts +++ b/ui/src/ui/control-ui-performance.ts @@ -22,6 +22,7 @@ export type ControlUiRefreshRun = { const EVENT_LOG_LIMIT = 250; const SLOW_RPC_MS = 1_000; const RESPONSIVENESS_ENTRY_MS = 50; +const RESPONSIVENESS_EVENT_LOG_LIMIT = 50; type ControlUiResponsivenessObserver = { disconnect: () => void; @@ -86,11 +87,19 @@ export function recordControlUiPerformanceEvent( host: ControlUiPerformanceHost, event: string, payload: Record, - opts?: { warn?: boolean; console?: boolean }, + opts?: { warn?: boolean; console?: boolean; maxBufferedEventsForType?: number }, ) { const entry: EventLogEntry = { ts: Date.now(), event, payload }; if (Array.isArray(host.eventLogBuffer)) { - host.eventLogBuffer = [entry, ...host.eventLogBuffer].slice(0, EVENT_LOG_LIMIT); + const existingBuffer = + typeof opts?.maxBufferedEventsForType === "number" + ? keepLatestBufferedEventsForType( + host.eventLogBuffer, + event, + Math.max(0, opts.maxBufferedEventsForType - 1), + ) + : host.eventLogBuffer; + host.eventLogBuffer = [entry, ...existingBuffer].slice(0, EVENT_LOG_LIMIT); if (host.tab === "debug" || host.tab === "overview") { host.eventLog = host.eventLogBuffer; } @@ -101,6 +110,26 @@ export function recordControlUiPerformanceEvent( logPerformanceEvent(event, payload, opts?.warn === true); } +function keepLatestBufferedEventsForType( + entries: unknown[], + event: string, + maxExistingForType: number, +): unknown[] { + let keptForType = 0; + return entries.filter((entry) => { + if ( + !entry || + typeof entry !== "object" || + !("event" in entry) || + (entry as { event?: unknown }).event !== event + ) { + return true; + } + keptForType += 1; + return keptForType <= maxExistingForType; + }); +} + export function scheduleControlUiTabVisibleTiming( host: ControlUiPerformanceHost, previousTab: Tab, @@ -256,7 +285,7 @@ function recordResponsivenessEntry( scriptCount: Array.isArray(entry.scripts) ? entry.scripts.length : undefined, topScript: getTopLongAnimationFrameScript(entry.scripts), }, - { warn: true }, + { warn: true, maxBufferedEventsForType: RESPONSIVENESS_EVENT_LOG_LIMIT }, ); } diff --git a/ui/src/ui/controllers/cron.test.ts b/ui/src/ui/controllers/cron.test.ts index 56f47e39fc9..3c8814caaf2 100644 --- a/ui/src/ui/controllers/cron.test.ts +++ b/ui/src/ui/controllers/cron.test.ts @@ -38,6 +38,7 @@ function createState(overrides: Partial = {}): CronState { cronStatus: null, cronError: null, cronForm: { ...DEFAULT_CRON_FORM }, + cronFormCollapsed: false, cronFieldErrors: {}, cronEditingJobId: null, cronRunsJobId: null, diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index 141b22d6801..e1aafd866f6 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -67,6 +67,7 @@ export type CronState = { cronStatus: CronStatus | null; cronError: string | null; cronForm: CronFormState; + cronFormCollapsed: boolean; cronFieldErrors: CronFieldErrors; cronEditingJobId: string | null; cronRunsJobId: string | null; diff --git a/ui/src/ui/views/cron.test.ts b/ui/src/ui/views/cron.test.ts index 51aff0ee793..e1fdf776e16 100644 --- a/ui/src/ui/views/cron.test.ts +++ b/ui/src/ui/views/cron.test.ts @@ -277,6 +277,45 @@ describe("cron view", () => { expect(container.querySelector('input[placeholder="https://example.com/cron"]')).toBeNull(); }); + it("collapses the new job sidebar without rendering the full form", () => { + const container = document.createElement("div"); + const onToggleFormCollapsed = vi.fn(); + const expandedProps = createProps() as CronProps & { + cronFormCollapsed: boolean; + onToggleFormCollapsed: (collapsed: boolean) => void; + }; + expandedProps.cronFormCollapsed = false; + expandedProps.onToggleFormCollapsed = onToggleFormCollapsed; + + render(renderCron(expandedProps), container); + + const collapseButton = container.querySelector('[data-test-id="cron-form-collapse-toggle"]'); + expect(collapseButton).not.toBeNull(); + expect(collapseButton?.getAttribute("aria-expanded")).toBe("true"); + collapseButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(onToggleFormCollapsed).toHaveBeenCalledWith(true); + expect(container.querySelector(".cron-form")).not.toBeNull(); + + const collapsedProps = createProps() as CronProps & { + cronFormCollapsed: boolean; + onToggleFormCollapsed: (collapsed: boolean) => void; + }; + collapsedProps.cronFormCollapsed = true; + collapsedProps.onToggleFormCollapsed = onToggleFormCollapsed; + + render(renderCron(collapsedProps), container); + + const collapsedButton = container.querySelector('[data-test-id="cron-form-collapse-toggle"]'); + expect(container.querySelector(".cron-workspace--form-collapsed")).not.toBeNull(); + expect(container.querySelector(".cron-workspace-form--collapsed")).not.toBeNull(); + expect(collapsedButton?.getAttribute("aria-expanded")).toBe("false"); + expect(container.querySelector(".cron-form")?.hasAttribute("hidden")).toBe(true); + expect(container.querySelector(".cron-form-actions")?.hasAttribute("hidden")).toBe(true); + + collapsedButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(onToggleFormCollapsed).toHaveBeenLastCalledWith(false); + }); + it("shows webhook delivery details for jobs", () => { const container = document.createElement("div"); const job = { diff --git a/ui/src/ui/views/cron.ts b/ui/src/ui/views/cron.ts index 73e932c9e6c..d37f7055cd9 100644 --- a/ui/src/ui/views/cron.ts +++ b/ui/src/ui/views/cron.ts @@ -45,6 +45,7 @@ export type CronProps = { fieldErrors: CronFieldErrors; canSubmit: boolean; editingJobId: string | null; + cronFormCollapsed?: boolean; channels: string[]; channelLabels?: Record; channelMeta?: ChannelUiMetaEntry[]; @@ -71,6 +72,7 @@ export type CronProps = { onEdit: (job: CronJob) => void; onClone: (job: CronJob) => void; onCancelEdit: () => void; + onToggleFormCollapsed?: (collapsed: boolean) => void; onToggle: (job: CronJob, enabled: boolean) => void; onRun: (job: CronJob, mode?: "force" | "due") => void; onRemove: (job: CronJob) => void; @@ -383,6 +385,9 @@ export function renderCron(props: CronProps) { props.form.sessionTarget !== "main" && props.form.payloadKind === "agentTurn"; const selectedDeliveryMode = props.form.deliveryMode === "announce" && !supportsAnnounce ? "none" : props.form.deliveryMode; + const formCollapsed = props.cronFormCollapsed === true; + const formTitle = isEditing ? t("cron.form.editJob") : t("cron.form.newJob"); + const toggleFormCollapsed = props.onToggleFormCollapsed; const blockingFields = collectBlockingFields(props.fieldErrors, props.form, selectedDeliveryMode); const blockedByValidation = !props.busy && blockingFields.length > 0; const hasActiveJobsFilters = @@ -437,7 +442,7 @@ export function renderCron(props: CronProps) { - + - - ${isEditing ? t("cron.form.editJob") : t("cron.form.newJob")} - - ${isEditing ? t("cron.form.updateSubtitle") : t("cron.form.createSubtitle")} + + + + ${formTitle} + ${formCollapsed + ? nothing + : html` + + ${isEditing ? t("cron.form.updateSubtitle") : t("cron.form.createSubtitle")} + + `} + + ${toggleFormCollapsed + ? html` + toggleFormCollapsed(!formCollapsed)} + > + ${formCollapsed ? "<" : ">"} + + ` + : nothing} - + * ${t( "cron.form.required", @@ -1317,7 +1347,12 @@ export function renderCron(props: CronProps) { ${blockedByValidation ? html` - + ${t("cron.form.cantAddYet")} ${t("cron.form.fillRequired")} @@ -1338,7 +1373,7 @@ export function renderCron(props: CronProps) { ` : nothing} - + ${submitDisabledReason - ? html`${submitDisabledReason}` + ? html` + ${submitDisabledReason} + ` : nothing} ${isEditing ? html` diff --git a/ui/src/ui/views/sessions.test.ts b/ui/src/ui/views/sessions.test.ts index cc1bf981b50..d9cf368590a 100644 --- a/ui/src/ui/views/sessions.test.ts +++ b/ui/src/ui/views/sessions.test.ts @@ -100,7 +100,13 @@ describe("sessions view", () => { it("uses one short styled tooltip per session filter", async () => { const container = document.createElement("div"); - render(renderSessions(buildProps(buildMultiResult([]))), container); + render( + renderSessions({ + ...buildProps(buildMultiResult([])), + activeMinutes: "120", + }), + container, + ); await Promise.resolve(); const filters = container.querySelector(".sessions-filter-bar"); @@ -120,7 +126,7 @@ describe("sessions view", () => { ?.querySelector(".session-filter-check__input[name=showArchived]") ?.closest("label"); - expect(activeField?.getAttribute("data-tooltip")).toBe("Updated in the last N minutes."); + expect(activeField?.getAttribute("data-tooltip")).toBe("Updated in the last 120 minutes."); expect(limitField?.getAttribute("data-tooltip")).toBe("Max sessions to load."); expect(globalToggle?.getAttribute("data-tooltip")).toBe("Include global sessions."); expect(unknownToggle?.getAttribute("data-tooltip")).toBe("Include unknown sessions."); diff --git a/ui/src/ui/views/sessions.ts b/ui/src/ui/views/sessions.ts index 11b8c9a0be9..068cc2a86b5 100644 --- a/ui/src/ui/views/sessions.ts +++ b/ui/src/ui/views/sessions.ts @@ -304,7 +304,7 @@ export function renderSessions(props: SessionsProps) { const totalPages = Math.max(1, Math.ceil(totalRows / props.pageSize)); const page = Math.min(props.page, totalPages - 1); const paginated = paginateRows(sorted, page, props.pageSize); - const activeTooltip = t("sessionsView.activeTooltip"); + const activeTooltip = t("sessionsView.activeTooltip", { count: props.activeMinutes.trim() }); const limitTooltip = t("sessionsView.limitTooltip"); const globalTooltip = t("sessionsView.globalTooltip"); const unknownTooltip = t("sessionsView.unknownTooltip");