mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
Merge branch 'main' into meow/control-chat-responsive
This commit is contained in:
15
CHANGELOG.md
15
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
<Warning>
|
||||
`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.
|
||||
</Warning>
|
||||
|
||||
@@ -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.
|
||||
</Accordion>
|
||||
<Accordion title="7b. Plugin install cleanup">
|
||||
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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
<Warning>
|
||||
Bundled plugins only in this release.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -23,6 +23,9 @@ type GoogleAuthTransport = InstanceType<GaxiosModule["Gaxios"]>;
|
||||
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<GoogleAuthRuntime> | null = null;
|
||||
let googleAuthTransportPromise: Promise<GoogleAuthTransport> | null = null;
|
||||
|
||||
function normalizeGoogleAuthPreparedRequestHeaders<T extends GoogleAuthRequestWithUnknownHeaders>(
|
||||
config: T,
|
||||
@@ -79,12 +81,24 @@ function normalizeGoogleAuthPreparedRequestHeaders<T extends GoogleAuthRequestWi
|
||||
return config as T & { headers: Headers };
|
||||
}
|
||||
|
||||
function normalizeGoogleAuthResponseHeaders<T extends GoogleAuthResponseWithUnknownHeaders>(
|
||||
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<GoogleAuthRuntime> {
|
||||
}
|
||||
|
||||
export async function getGoogleAuthTransport(): Promise<GoogleAuthTransport> {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<string, string>,
|
||||
translatedFlat: ReadonlyMap<string, string>,
|
||||
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<string, string>,
|
||||
translatedFlat: ReadonlyMap<string, string>,
|
||||
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<unknown> = Promise.resolve();
|
||||
|
||||
private constructor(processHandle: ReturnType<typeof spawn>) {
|
||||
private constructor(processHandle: ChildProcessWithoutNullStreams) {
|
||||
this.process = processHandle;
|
||||
this.stdin = processHandle.stdin;
|
||||
}
|
||||
@@ -1174,7 +1238,7 @@ class PiRpcClient {
|
||||
}
|
||||
|
||||
async prompt(message: string, label: string): Promise<string> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
63
scripts/run-node-watch-paths.mjs
Normal file
63
scripts/run-node-watch-paths.mjs
Normal file
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<PluginRegistryInstallMigrationParam
|
||||
prompter: Pick<DoctorPrompter, "shouldRepair">;
|
||||
};
|
||||
|
||||
type StaleManagedNpmBundledPlugin = {
|
||||
pluginId: string;
|
||||
packageName: string;
|
||||
packageDir: string;
|
||||
npmRoot: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readJsonObject(filePath: string): Record<string, unknown> | 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<string, string> {
|
||||
if (!isRecord(value)) {
|
||||
return {};
|
||||
}
|
||||
const result: Record<string, string> = {};
|
||||
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<OpenClawConfig> {
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -111,6 +111,63 @@ describe("attachGatewayWsConnectionHandler", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the gateway TLS scheme for canvas host URLs", async () => {
|
||||
const listeners = new Map<string, (...args: unknown[]) => 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<string, (...args: unknown[]) => void>();
|
||||
const wss = {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<T>(
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -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<number>("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<number>("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<number>("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([
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
pluginStateEntries,
|
||||
pluginStateLookup,
|
||||
pluginStateRegister,
|
||||
pluginStateRegisterIfAbsent,
|
||||
} from "./plugin-state-store.sqlite.js";
|
||||
import type {
|
||||
OpenKeyedStoreOptions,
|
||||
@@ -217,20 +218,44 @@ function createKeyedStoreForPluginId<T>(
|
||||
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) {
|
||||
|
||||
@@ -7,6 +7,7 @@ export type PluginStateEntry<T> = {
|
||||
|
||||
export type PluginStateKeyedStore<T> = {
|
||||
register(key: string, value: T, opts?: { ttlMs?: number }): Promise<void>;
|
||||
registerIfAbsent(key: string, value: T, opts?: { ttlMs?: number }): Promise<boolean>;
|
||||
lookup(key: string): Promise<T | undefined>;
|
||||
consume(key: string): Promise<T | undefined>;
|
||||
delete(key: string): Promise<boolean>;
|
||||
|
||||
35
src/scripts/control-ui-i18n.test.ts
Normal file
35
src/scripts/control-ui-i18n.test.ts
Normal file
@@ -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"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -166,7 +166,7 @@ export const ar: TranslationMap = {
|
||||
global: "عام",
|
||||
unknown: "غير معروف",
|
||||
showArchived: "إظهار المؤرشفة",
|
||||
activeTooltip: "تم التحديث خلال آخر N دقيقة.",
|
||||
activeTooltip: "تم التحديث خلال آخر {count} دقيقة.",
|
||||
limitTooltip: "الحد الأقصى للجلسات المراد تحميلها.",
|
||||
globalTooltip: "تضمين الجلسات العامة.",
|
||||
unknownTooltip: "تضمين الجلسات غير المعروفة.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -168,7 +168,7 @@ export const fa: TranslationMap = {
|
||||
global: "سراسری",
|
||||
unknown: "نامشخص",
|
||||
showArchived: "نمایش بایگانیشدهها",
|
||||
activeTooltip: "در N دقیقهٔ گذشته بهروزرسانی شده است.",
|
||||
activeTooltip: "در {count} دقیقهٔ گذشته بهروزرسانی شده است.",
|
||||
limitTooltip: "حداکثر جلسههایی که بارگیری میشوند.",
|
||||
globalTooltip: "شامل جلسههای سراسری شود.",
|
||||
unknownTooltip: "شامل جلسههای ناشناخته شود.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -170,7 +170,7 @@ export const ja_JP: TranslationMap = {
|
||||
global: "グローバル",
|
||||
unknown: "不明",
|
||||
showArchived: "アーカイブ済みを表示",
|
||||
activeTooltip: "過去 N 分以内に更新されました。",
|
||||
activeTooltip: "過去 {count} 分以内に更新されました。",
|
||||
limitTooltip: "読み込むセッションの最大数。",
|
||||
globalTooltip: "グローバルセッションを含めます。",
|
||||
unknownTooltip: "不明なセッションを含めます。",
|
||||
|
||||
@@ -166,7 +166,7 @@ export const ko: TranslationMap = {
|
||||
global: "전역",
|
||||
unknown: "알 수 없음",
|
||||
showArchived: "보관된 세션 표시",
|
||||
activeTooltip: "최근 N분 이내에 업데이트됨.",
|
||||
activeTooltip: "최근 {count}분 이내에 업데이트됨.",
|
||||
limitTooltip: "로드할 최대 세션 수.",
|
||||
globalTooltip: "전역 세션 포함.",
|
||||
unknownTooltip: "알 수 없는 세션 포함.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -165,7 +165,7 @@ export const th: TranslationMap = {
|
||||
global: "ส่วนกลาง",
|
||||
unknown: "ไม่ทราบ",
|
||||
showArchived: "แสดงที่เก็บถาวร",
|
||||
activeTooltip: "อัปเดตในช่วง N นาทีที่ผ่านมา",
|
||||
activeTooltip: "อัปเดตในช่วง {count} นาทีที่ผ่านมา",
|
||||
limitTooltip: "จำนวนเซสชันสูงสุดที่จะโหลด",
|
||||
globalTooltip: "รวมเซสชันส่วนกลาง",
|
||||
unknownTooltip: "รวมเซสชันที่ไม่รู้จัก",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -168,7 +168,7 @@ export const uk: TranslationMap = {
|
||||
global: "Глобально",
|
||||
unknown: "Невідомо",
|
||||
showArchived: "Показати архівовані",
|
||||
activeTooltip: "Оновлено за останні N хвилин.",
|
||||
activeTooltip: "Оновлено за останні {count} хвилин.",
|
||||
limitTooltip: "Максимальна кількість сеансів для завантаження.",
|
||||
globalTooltip: "Включити глобальні сеанси.",
|
||||
unknownTooltip: "Включити невідомі сеанси.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -165,7 +165,7 @@ export const zh_CN: TranslationMap = {
|
||||
global: "全局",
|
||||
unknown: "未知",
|
||||
showArchived: "显示已归档",
|
||||
activeTooltip: "在过去 N 分钟内更新。",
|
||||
activeTooltip: "在过去 {count} 分钟内更新。",
|
||||
limitTooltip: "要加载的最大会话数。",
|
||||
globalTooltip: "包含全局会话。",
|
||||
unknownTooltip: "包含未知会话。",
|
||||
|
||||
@@ -165,7 +165,7 @@ export const zh_TW: TranslationMap = {
|
||||
global: "全域",
|
||||
unknown: "未知",
|
||||
showArchived: "顯示已封存項目",
|
||||
activeTooltip: "在過去 N 分鐘內更新。",
|
||||
activeTooltip: "在過去 {count} 分鐘內更新。",
|
||||
limitTooltip: "要載入的工作階段上限。",
|
||||
globalTooltip: "包含全域工作階段。",
|
||||
unknownTooltip: "包含未知工作階段。",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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%;");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string>(),
|
||||
chatStreamStartedAt: 1,
|
||||
settings,
|
||||
@@ -922,6 +923,7 @@ describe("switchChatSession", () => {
|
||||
chatQueue: [],
|
||||
chatQueueBySession: {},
|
||||
chatRunId: null,
|
||||
sessionsShowArchived: false,
|
||||
chatSideResultTerminalRuns: new Set<string>(),
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -331,6 +331,7 @@ export type AppViewState = {
|
||||
| "cronStatus"
|
||||
| "cronError"
|
||||
| "cronForm"
|
||||
| "cronFormCollapsed"
|
||||
| "cronFieldErrors"
|
||||
| "cronEditingJobId"
|
||||
| "cronRunsJobId"
|
||||
|
||||
57
ui/src/ui/app.talk.test.ts
Normal file
57
ui/src/ui/app.talk.test.ts
Normal file
@@ -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<void>;
|
||||
};
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string, unknown> }>,
|
||||
eventLogBuffer: [] as Array<{ payload: Record<string, unknown> }>,
|
||||
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();
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
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 },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ function createState(overrides: Partial<CronState> = {}): CronState {
|
||||
cronStatus: null,
|
||||
cronError: null,
|
||||
cronForm: { ...DEFAULT_CRON_FORM },
|
||||
cronFormCollapsed: false,
|
||||
cronFieldErrors: {},
|
||||
cronEditingJobId: null,
|
||||
cronRunsJobId: null,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user