From cd3d960ec5f73a7d8c3f77b797146cecbfd6aff0 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 30 May 2026 14:58:00 -0700 Subject: [PATCH] fix(ui): add agent selector to dreaming tab (#78748) Fixes #63558. Adds a Dreaming-tab agent selector and propagates the selected agent through Dreaming status, diary, and diary actions while preserving default-agent fallback when agentId is omitted. Also keeps report Memory Palace cards in the Control UI wiki-preview flow and documents the optional Dreaming agentId gateway parameters. Verification: - GitHub CI run 26693682975 passed on 43a2b1724305a4d30dc29aefcbc0d762d1a90af9. - CodeQL Critical Quality run 26693682971 passed. - CodeQL / Security High run 26693682957 passed. - Workflow Sanity run 26693682949 passed. - OpenGrep PR Diff run 26693682947 passed. - Dependency Guard run 26693682003 passed. - Real behavior proof run 26693860539 passed. - git diff --check origin/main...refs/remotes/origin/pr/78748 passed. - git merge-tree --write-tree origin/main refs/remotes/origin/pr/78748 passed. Thanks @stevenepalmer. Co-authored-by: Steven Palmer <6134396+stevenepalmer@users.noreply.github.com> --- docs/gateway/protocol.md | 3 +- scripts/check-gateway-watch-regression.mjs | 50 ++++- src/gateway/server-methods/doctor.test.ts | 131 ++++++++++- src/gateway/server-methods/doctor.ts | 53 +++-- ui/src/i18n/.i18n/ar.meta.json | 8 +- ui/src/i18n/.i18n/de.meta.json | 8 +- ui/src/i18n/.i18n/es.meta.json | 8 +- ui/src/i18n/.i18n/fa.meta.json | 8 +- ui/src/i18n/.i18n/fr.meta.json | 8 +- ui/src/i18n/.i18n/id.meta.json | 8 +- ui/src/i18n/.i18n/it.meta.json | 8 +- ui/src/i18n/.i18n/ja-JP.meta.json | 8 +- ui/src/i18n/.i18n/ko.meta.json | 8 +- ui/src/i18n/.i18n/nl.meta.json | 8 +- ui/src/i18n/.i18n/pl.meta.json | 8 +- ui/src/i18n/.i18n/pt-BR.meta.json | 8 +- ui/src/i18n/.i18n/th.meta.json | 8 +- ui/src/i18n/.i18n/tr.meta.json | 8 +- ui/src/i18n/.i18n/uk.meta.json | 8 +- ui/src/i18n/.i18n/vi.meta.json | 8 +- ui/src/i18n/.i18n/zh-CN.meta.json | 8 +- ui/src/i18n/.i18n/zh-TW.meta.json | 8 +- ui/src/i18n/locales/ar.ts | 4 + ui/src/i18n/locales/de.ts | 4 + ui/src/i18n/locales/en.ts | 4 + ui/src/i18n/locales/es.ts | 4 + ui/src/i18n/locales/fa.ts | 4 + ui/src/i18n/locales/fr.ts | 4 + ui/src/i18n/locales/id.ts | 4 + ui/src/i18n/locales/it.ts | 4 + ui/src/i18n/locales/ja-JP.ts | 4 + ui/src/i18n/locales/ko.ts | 4 + ui/src/i18n/locales/nl.ts | 4 + ui/src/i18n/locales/pl.ts | 4 + ui/src/i18n/locales/pt-BR.ts | 4 + ui/src/i18n/locales/th.ts | 4 + ui/src/i18n/locales/tr.ts | 4 + ui/src/i18n/locales/uk.ts | 4 + ui/src/i18n/locales/vi.ts | 4 + ui/src/i18n/locales/zh-CN.ts | 4 + ui/src/i18n/locales/zh-TW.ts | 4 + ui/src/ui/app-render.ts | 49 +++- ...p-settings.refresh-active-tab.node.test.ts | 32 +++ ui/src/ui/app-settings.ts | 9 + ui/src/ui/app-view-state.ts | 1 + ui/src/ui/app.ts | 1 + ui/src/ui/chat/session-controls.ts | 6 +- ui/src/ui/controllers/dreaming.test.ts | 211 ++++++++++++++++++ ui/src/ui/controllers/dreaming.ts | 105 ++++++++- ui/src/ui/e2e/chat-flow.e2e.test.ts | 8 +- ui/src/ui/views/dreaming.test.ts | 10 +- ui/src/ui/views/dreaming.ts | 112 ++++++---- 52 files changed, 842 insertions(+), 159 deletions(-) diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index 92ae9e4b5cc3..14fbe69e22ad 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -348,7 +348,8 @@ enumeration of `src/gateway/server-methods/*.ts`. - `usage.status` returns provider usage windows/remaining quota summaries. - `usage.cost` returns aggregated cost usage summaries for a date range. Pass `agentId` for one agent, or `agentScope: "all"` to aggregate configured agents. - - `doctor.memory.status` returns vector-memory / cached embedding readiness for the active default agent workspace. Pass `{ "probe": true }` or `{ "deep": true }` only when the caller explicitly wants a live embedding provider ping. + - `doctor.memory.status` returns vector-memory / cached embedding readiness for the active default agent workspace. Pass `{ "probe": true }` or `{ "deep": true }` only when the caller explicitly wants a live embedding provider ping. Dreaming-aware clients may also pass `{ "agentId": "agent-id" }` to scope Dreaming store stats to a selected agent workspace; omitting `agentId` keeps the default-agent fallback and aggregates configured Dreaming workspaces. + - `doctor.memory.dreamDiary`, `doctor.memory.backfillDreamDiary`, `doctor.memory.resetDreamDiary`, `doctor.memory.resetGroundedShortTerm`, `doctor.memory.repairDreamingArtifacts`, and `doctor.memory.dedupeDreamDiary` accept optional `{ "agentId": "agent-id" }` params for selected-agent Dreaming views/actions. When `agentId` is omitted, they operate on the configured default agent workspace. - `doctor.memory.remHarness` returns a bounded, read-only REM harness preview for remote control-plane clients. It can include workspace paths, memory snippets, rendered grounded markdown, and deep promotion candidates, so callers need `operator.read`. - `sessions.usage` returns per-session usage summaries. Pass `agentId` for one agent, or `agentScope: "all"` to list configured agents together. diff --git a/scripts/check-gateway-watch-regression.mjs b/scripts/check-gateway-watch-regression.mjs index cc3a90751ce3..202a48dc5855 100644 --- a/scripts/check-gateway-watch-regression.mjs +++ b/scripts/check-gateway-watch-regression.mjs @@ -412,16 +412,21 @@ async function allocateLoopbackPort() { } function buildTimedWatchCommand(pidFilePath, timeFilePath, isolatedHomeDir, port) { + const isolatedStateDir = path.join(isolatedHomeDir, ".openclaw"); + const isolatedConfigPath = path.join(isolatedStateDir, "openclaw.json"); const shellSource = [ 'echo "$$" > "$OPENCLAW_WATCH_PID_FILE"', - 'mkdir -p "$OPENCLAW_HOME/.openclaw"', - `printf '%s\n' '{"gateway":{"controlUi":{"enabled":false}},"plugins":{"enabled":false}}' > "$OPENCLAW_HOME/.openclaw/openclaw.json"`, + 'mkdir -p "$OPENCLAW_STATE_DIR"', + `printf '%s\n' '{"gateway":{"controlUi":{"enabled":false}},"plugins":{"enabled":false}}' > "$OPENCLAW_CONFIG_PATH"`, `exec node scripts/watch-node.mjs gateway --force --allow-unconfigured --port ${String(port)} --token watch-regression-token`, ].join("\n"); const env = { OPENCLAW_WATCH_PID_FILE: pidFilePath, HOME: isolatedHomeDir, OPENCLAW_HOME: isolatedHomeDir, + OPENCLAW_CONFIG_PATH: isolatedConfigPath, + OPENCLAW_STATE_DIR: isolatedStateDir, + XDG_CONFIG_HOME: path.join(isolatedHomeDir, ".config"), ...WATCH_GATEWAY_SKIP_ENV, }; @@ -433,6 +438,14 @@ function buildTimedWatchCommand(pidFilePath, timeFilePath, isolatedHomeDir, port }; } + if (!fs.existsSync("/usr/bin/time")) { + return { + command: "/bin/sh", + args: ["-lc", shellSource], + env, + }; + } + return { command: "/usr/bin/time", args: [ @@ -511,6 +524,14 @@ async function runTimedWatch(options, outputDir) { buildDetection = updateWatchBuildDetection(buildDetection, chunk); }); + let spawnError = null; + const spawnErrorExit = new Promise((resolve) => { + child.once("error", (error) => { + spawnError = error; + resolve({ code: null, signal: null, error: error.message }); + }); + }); + let watchPid = null; for (let attempt = 0; attempt < 50; attempt += 1) { if (fs.existsSync(pidFilePath)) { @@ -531,15 +552,18 @@ async function runTimedWatch(options, outputDir) { await sleep(options.windowMs); const idleCpuEndMs = watchPid ? readProcessTreeCpuMs(watchPid) : null; - const exit = await stopTimedWatchChild(child, watchPid, options); + const exit = await Promise.race([stopTimedWatchChild(child, watchPid, options), spawnErrorExit]); fs.writeFileSync(stdoutPath, formatCapturedWatchLog(stdout, stdoutTruncated), "utf8"); fs.writeFileSync(stderrPath, formatCapturedWatchLog(stderr, stderrTruncated), "utf8"); - const timing = fs.existsSync(timeFilePath) - ? parseTimingFile(timeFilePath) - : { userSeconds: Number.NaN, sysSeconds: Number.NaN, elapsedSeconds: Number.NaN }; + const timingFileMissing = !fs.existsSync(timeFilePath); + const timing = timingFileMissing + ? { userSeconds: Number.NaN, sysSeconds: Number.NaN, elapsedSeconds: Number.NaN } + : parseTimingFile(timeFilePath); return { exit, + spawnError: spawnError ? spawnError.message : null, + timingFileMissing, timing, readyBeforeWindow, idleCpuMs: @@ -768,6 +792,8 @@ async function main() { addedPaths: diff.added.length, removedPaths: diff.removed.length, watchExit: watchResult.exit, + spawnError: watchResult.spawnError, + timingFileMissing: watchResult.timingFileMissing, timing: watchResult.timing, }; fs.writeFileSync( @@ -779,6 +805,18 @@ async function main() { const failures = []; const warnings = []; + if (watchResult.spawnError) { + failures.push(`gateway:watch failed to start: ${watchResult.spawnError}`); + } + if (watchResult.timingFileMissing && !Number.isFinite(watchResult.idleCpuMs)) { + failures.push( + "failed to collect CPU timing from the bounded gateway:watch run; timing artifact is missing", + ); + } else if (watchResult.timingFileMissing) { + warnings.push( + "bounded gateway:watch timing artifact is missing; using process-tree idle CPU sample", + ); + } if (watchTriggeredBuild && watchBuildReason === "dirty_watched_tree") { failures.push( "gateway:watch invalid local run: dirty watched source tree forced a rebuild during the watch window", diff --git a/src/gateway/server-methods/doctor.test.ts b/src/gateway/server-methods/doctor.test.ts index b082543df92f..80ffc1eccb86 100644 --- a/src/gateway/server-methods/doctor.test.ts +++ b/src/gateway/server-methods/doctor.test.ts @@ -78,10 +78,13 @@ const invokeDoctorMemoryStatus = async ( }); }; -const invokeDoctorMemoryDreamDiary = async (respond: ReturnType) => { +const invokeDoctorMemoryDreamDiary = async ( + respond: ReturnType, + params: unknown = {}, +) => { await doctorHandlers["doctor.memory.dreamDiary"]({ req: {} as never, - params: {} as never, + params: params as never, respond: respond as never, context: makeRuntimeContext() as never, client: null, @@ -263,6 +266,33 @@ describe("doctor.memory.status", () => { expect(close).toHaveBeenCalled(); }); + it("returns gateway embedding probe status for the requested agent", async () => { + const close = vi.fn().mockResolvedValue(undefined); + getMemorySearchManager.mockResolvedValue({ + manager: { + status: () => ({ provider: "gemini", workspaceDir: "/tmp/research-workspace" }), + probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }), + close, + }, + }); + const respond = vi.fn(); + + await invokeDoctorMemoryStatus(respond, { + params: { agentId: "research-analyst", probe: true }, + }); + + expectRecordFields(mockCallArg(getMemorySearchManager), { + agentId: "research-analyst", + purpose: "status", + }); + const payload = respondPayload(respond); + expectRecordFields(payload, { + agentId: "research-analyst", + provider: "gemini", + embedding: { ok: true }, + }); + }); + it("does not live-probe embedding readiness by default", async () => { const close = vi.fn().mockResolvedValue(undefined); const probeEmbeddingAvailability = vi.fn().mockResolvedValue({ ok: true }); @@ -609,6 +639,80 @@ describe("doctor.memory.status", () => { } }); + it("scopes dreaming status to the requested agent workspace", async () => { + const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-selected-")); + const mainWorkspaceDir = path.join(workspaceRoot, "main"); + const alphaWorkspaceDir = path.join(workspaceRoot, "alpha"); + const writeStore = async (workspaceDir: string, snippet: string) => { + const storePath = path.join(workspaceDir, "memory", ".dreams", "short-term-recall.json"); + await fs.mkdir(path.dirname(storePath), { recursive: true }); + const store = { + version: 1, + updatedAt: "2026-04-04T00:00:00.000Z", + entries: { + "memory:memory/2026-04-04.md:1:2": { + path: "memory/2026-04-04.md", + startLine: 1, + endLine: 2, + snippet, + source: "memory", + promotedAt: "2026-04-04T00:00:00.000Z", + }, + }, + }; + await fs.writeFile(storePath, JSON.stringify(store, null, 2) + "\n", "utf-8"); + }; + await writeStore(mainWorkspaceDir, "main agent memory"); + await writeStore(alphaWorkspaceDir, "alpha agent memory"); + getRuntimeConfig.mockReturnValue({ + agents: { + list: [{ id: "alpha", workspace: alphaWorkspaceDir }], + }, + plugins: { + entries: { + "memory-core": { + config: { + dreaming: {}, + }, + }, + }, + }, + } as OpenClawConfig); + resolveAgentWorkspaceDir.mockImplementation((_cfg: OpenClawConfig, agentId: string) => { + if (agentId === "alpha") { + return alphaWorkspaceDir; + } + return mainWorkspaceDir; + }); + + const close = vi.fn().mockResolvedValue(undefined); + getMemorySearchManager.mockResolvedValue({ + manager: { + status: () => ({ provider: "gemini", workspaceDir: alphaWorkspaceDir }), + probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }), + close, + }, + }); + const respond = vi.fn(); + + try { + await invokeDoctorMemoryStatus(respond, { params: { agentId: "alpha" } }); + const payload = respondPayload(respond); + expectRecordFields(payload, { + agentId: "alpha", + }); + const dreaming = expectRecordFields(payload.dreaming, { + shortTermCount: 0, + promotedTotal: 1, + }); + expectRecordFields((dreaming.promotedEntries as unknown[])[0], { + snippet: "alpha agent memory", + }); + } finally { + await fs.rm(workspaceRoot, { recursive: true, force: true }); + } + }); + it("falls back to the manager workspace when no configured dreaming workspaces resolve", async () => { const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-fallback-")); const storePath = path.join(workspaceDir, "memory", ".dreams", "short-term-recall.json"); @@ -939,6 +1043,29 @@ describe("doctor.memory.dreamDiary", () => { } }); + it("reads DREAMS.md for the requested agent", async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-dream-diary-agent-")); + await fs.writeFile(path.join(workspaceDir, "DREAMS.md"), "## Research Dreams\n", "utf-8"); + resolveAgentWorkspaceDir.mockImplementation((_cfg, agentId) => + agentId === "research-analyst" ? workspaceDir : "/tmp/openclaw", + ); + const respond = vi.fn(); + + try { + await invokeDoctorMemoryDreamDiary(respond, { agentId: "research-analyst" }); + expect(resolveAgentWorkspaceDir).toHaveBeenCalledWith(expect.anything(), "research-analyst"); + const payload = respondPayload(respond); + expectRecordFields(payload, { + agentId: "research-analyst", + found: true, + path: "DREAMS.md", + content: "## Research Dreams\n", + }); + } finally { + await fs.rm(workspaceDir, { recursive: true, force: true }); + } + }); + it("reads lowercase dreams.md when present", async () => { const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-dream-diary-lower-")); await fs.writeFile(path.join(workspaceDir, "dreams.md"), "lowercase diary\n", "utf-8"); diff --git a/src/gateway/server-methods/doctor.ts b/src/gateway/server-methods/doctor.ts index e1b58ffeb5af..0fe5e6d969c6 100644 --- a/src/gateway/server-methods/doctor.ts +++ b/src/gateway/server-methods/doctor.ts @@ -12,6 +12,7 @@ import { resolveMemoryRemDreamingConfig, } from "../../memory-host-sdk/dreaming.js"; import { getActiveMemorySearchManager } from "../../plugins/memory-runtime.js"; +import { normalizeAgentId } from "../../routing/session-key.js"; import { formatError } from "../server-utils.js"; import { dedupeDreamDiaryEntries, @@ -893,7 +894,9 @@ const SKIPPED_MEMORY_EMBEDDING_PROBE = { export const doctorHandlers: GatewayRequestHandlers = { "doctor.memory.status": async ({ respond, context, params }) => { const cfg = context.getRuntimeConfig(); - const agentId = resolveDefaultAgentId(cfg); + const requestedAgentId = + typeof params?.agentId === "string" ? normalizeAgentId(params.agentId) : null; + const agentId = requestedAgentId || resolveDefaultAgentId(cfg); const { manager, error } = await getActiveMemorySearchManager({ cfg, agentId, @@ -923,10 +926,14 @@ export const doctorHandlers: GatewayRequestHandlers = { const nowMs = Date.now(); const dreamingConfig = resolveDreamingConfig(cfg); const workspaceDir = normalizeTrimmedString((status as Record).workspaceDir); - const configuredWorkspaces = resolveMemoryDreamingWorkspaces(cfg, { - primaryWorkspaceDir: workspaceDir, - primaryAgentId: resolveDefaultAgentId(cfg), - }).map((entry) => entry.workspaceDir); + const configuredWorkspaces = requestedAgentId + ? workspaceDir + ? [workspaceDir] + : [] + : resolveMemoryDreamingWorkspaces(cfg, { + primaryWorkspaceDir: workspaceDir, + primaryAgentId: agentId, + }).map((entry) => entry.workspaceDir); const allWorkspaces = configuredWorkspaces.length > 0 ? configuredWorkspaces : workspaceDir ? [workspaceDir] : []; const storeStats = @@ -988,9 +995,11 @@ export const doctorHandlers: GatewayRequestHandlers = { await manager.close?.().catch(() => {}); } }, - "doctor.memory.dreamDiary": async ({ respond, context }) => { + "doctor.memory.dreamDiary": async ({ respond, context, params }) => { const cfg = context.getRuntimeConfig(); - const agentId = resolveDefaultAgentId(cfg); + const requestedAgentId = + typeof params?.agentId === "string" ? normalizeAgentId(params.agentId) : null; + const agentId = requestedAgentId || resolveDefaultAgentId(cfg); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const dreamDiary = await readDreamDiary(workspaceDir); const payload: DoctorMemoryDreamDiaryPayload = { @@ -999,9 +1008,11 @@ export const doctorHandlers: GatewayRequestHandlers = { }; respond(true, payload, undefined); }, - "doctor.memory.backfillDreamDiary": async ({ respond, context }) => { + "doctor.memory.backfillDreamDiary": async ({ respond, context, params }) => { const cfg = context.getRuntimeConfig(); - const agentId = resolveDefaultAgentId(cfg); + const requestedAgentId = + typeof params?.agentId === "string" ? normalizeAgentId(params.agentId) : null; + const agentId = requestedAgentId || resolveDefaultAgentId(cfg); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const memoryDir = path.join(workspaceDir, "memory"); const sourceFiles = await listWorkspaceDailyFiles(memoryDir); @@ -1057,9 +1068,11 @@ export const doctorHandlers: GatewayRequestHandlers = { }; respond(true, payload, undefined); }, - "doctor.memory.resetDreamDiary": async ({ respond, context }) => { + "doctor.memory.resetDreamDiary": async ({ respond, context, params }) => { const cfg = context.getRuntimeConfig(); - const agentId = resolveDefaultAgentId(cfg); + const requestedAgentId = + typeof params?.agentId === "string" ? normalizeAgentId(params.agentId) : null; + const agentId = requestedAgentId || resolveDefaultAgentId(cfg); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const removed = await removeBackfillDiaryEntries({ workspaceDir }); const dreamDiary = await readDreamDiary(workspaceDir); @@ -1072,9 +1085,11 @@ export const doctorHandlers: GatewayRequestHandlers = { }; respond(true, payload, undefined); }, - "doctor.memory.resetGroundedShortTerm": async ({ respond, context }) => { + "doctor.memory.resetGroundedShortTerm": async ({ respond, context, params }) => { const cfg = context.getRuntimeConfig(); - const agentId = resolveDefaultAgentId(cfg); + const requestedAgentId = + typeof params?.agentId === "string" ? normalizeAgentId(params.agentId) : null; + const agentId = requestedAgentId || resolveDefaultAgentId(cfg); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const removed = await removeGroundedShortTermCandidates({ workspaceDir }); const payload: DoctorMemoryDreamActionPayload = { @@ -1084,9 +1099,11 @@ export const doctorHandlers: GatewayRequestHandlers = { }; respond(true, payload, undefined); }, - "doctor.memory.repairDreamingArtifacts": async ({ respond, context }) => { + "doctor.memory.repairDreamingArtifacts": async ({ respond, context, params }) => { const cfg = context.getRuntimeConfig(); - const agentId = resolveDefaultAgentId(cfg); + const requestedAgentId = + typeof params?.agentId === "string" ? normalizeAgentId(params.agentId) : null; + const agentId = requestedAgentId || resolveDefaultAgentId(cfg); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const repair = await repairDreamingArtifacts({ workspaceDir }); const payload: DoctorMemoryDreamActionPayload = { @@ -1101,9 +1118,11 @@ export const doctorHandlers: GatewayRequestHandlers = { }; respond(true, payload, undefined); }, - "doctor.memory.dedupeDreamDiary": async ({ respond, context }) => { + "doctor.memory.dedupeDreamDiary": async ({ respond, context, params }) => { const cfg = context.getRuntimeConfig(); - const agentId = resolveDefaultAgentId(cfg); + const requestedAgentId = + typeof params?.agentId === "string" ? normalizeAgentId(params.agentId) : null; + const agentId = requestedAgentId || resolveDefaultAgentId(cfg); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const dedupe = await dedupeDreamDiaryEntries({ workspaceDir }); const dreamDiary = await readDreamDiary(workspaceDir); diff --git a/ui/src/i18n/.i18n/ar.meta.json b/ui/src/i18n/.i18n/ar.meta.json index 831838412857..9f528de8392a 100644 --- a/ui/src/i18n/.i18n/ar.meta.json +++ b/ui/src/i18n/.i18n/ar.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:27.122Z", + "generatedAt": "2026-05-30T19:58:53.789Z", "locale": "ar", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/de.meta.json b/ui/src/i18n/.i18n/de.meta.json index 0023a388f290..052a4111fcf2 100644 --- a/ui/src/i18n/.i18n/de.meta.json +++ b/ui/src/i18n/.i18n/de.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:11.512Z", + "generatedAt": "2026-05-30T19:58:50.267Z", "locale": "de", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/es.meta.json b/ui/src/i18n/.i18n/es.meta.json index a148a1b94588..0f1a3e0815f1 100644 --- a/ui/src/i18n/.i18n/es.meta.json +++ b/ui/src/i18n/.i18n/es.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:14.091Z", + "generatedAt": "2026-05-30T19:58:50.968Z", "locale": "es", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/fa.meta.json b/ui/src/i18n/.i18n/fa.meta.json index 4be0fa50d701..44c1226d119c 100644 --- a/ui/src/i18n/.i18n/fa.meta.json +++ b/ui/src/i18n/.i18n/fa.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:53.299Z", + "generatedAt": "2026-05-30T19:59:00.128Z", "locale": "fa", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/fr.meta.json b/ui/src/i18n/.i18n/fr.meta.json index b73c0c0e93fd..895b8fd1a916 100644 --- a/ui/src/i18n/.i18n/fr.meta.json +++ b/ui/src/i18n/.i18n/fr.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:24.002Z", + "generatedAt": "2026-05-30T19:58:53.086Z", "locale": "fr", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/id.meta.json b/ui/src/i18n/.i18n/id.meta.json index f9ae89623d4a..ef353a3a036b 100644 --- a/ui/src/i18n/.i18n/id.meta.json +++ b/ui/src/i18n/.i18n/id.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:39.713Z", + "generatedAt": "2026-05-30T19:58:56.596Z", "locale": "id", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/it.meta.json b/ui/src/i18n/.i18n/it.meta.json index e671a6437164..77eed646e86d 100644 --- a/ui/src/i18n/.i18n/it.meta.json +++ b/ui/src/i18n/.i18n/it.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:31.726Z", + "generatedAt": "2026-05-30T19:58:54.491Z", "locale": "it", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/ja-JP.meta.json b/ui/src/i18n/.i18n/ja-JP.meta.json index 5df8ed9346c4..e7476eab2e49 100644 --- a/ui/src/i18n/.i18n/ja-JP.meta.json +++ b/ui/src/i18n/.i18n/ja-JP.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:16.651Z", + "generatedAt": "2026-05-30T19:58:51.672Z", "locale": "ja-JP", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/ko.meta.json b/ui/src/i18n/.i18n/ko.meta.json index d485ea179b49..952f7b86eb61 100644 --- a/ui/src/i18n/.i18n/ko.meta.json +++ b/ui/src/i18n/.i18n/ko.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:20.922Z", + "generatedAt": "2026-05-30T19:58:52.390Z", "locale": "ko", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/nl.meta.json b/ui/src/i18n/.i18n/nl.meta.json index d6c1b7c1d37c..26b5c5e41413 100644 --- a/ui/src/i18n/.i18n/nl.meta.json +++ b/ui/src/i18n/.i18n/nl.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:50.261Z", + "generatedAt": "2026-05-30T19:58:59.409Z", "locale": "nl", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/pl.meta.json b/ui/src/i18n/.i18n/pl.meta.json index 1d6c2993146f..4c660e527480 100644 --- a/ui/src/i18n/.i18n/pl.meta.json +++ b/ui/src/i18n/.i18n/pl.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:42.058Z", + "generatedAt": "2026-05-30T19:58:57.290Z", "locale": "pl", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/pt-BR.meta.json b/ui/src/i18n/.i18n/pt-BR.meta.json index 5c4f28d7d54c..eebec7c66a6a 100644 --- a/ui/src/i18n/.i18n/pt-BR.meta.json +++ b/ui/src/i18n/.i18n/pt-BR.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:08.361Z", + "generatedAt": "2026-05-30T19:58:49.558Z", "locale": "pt-BR", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/th.meta.json b/ui/src/i18n/.i18n/th.meta.json index 42b55d4448a8..3e0b0607afb0 100644 --- a/ui/src/i18n/.i18n/th.meta.json +++ b/ui/src/i18n/.i18n/th.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:44.768Z", + "generatedAt": "2026-05-30T19:58:57.996Z", "locale": "th", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/tr.meta.json b/ui/src/i18n/.i18n/tr.meta.json index 41ab574046c9..fff85bce8031 100644 --- a/ui/src/i18n/.i18n/tr.meta.json +++ b/ui/src/i18n/.i18n/tr.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:34.872Z", + "generatedAt": "2026-05-30T19:58:55.193Z", "locale": "tr", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/uk.meta.json b/ui/src/i18n/.i18n/uk.meta.json index 96a102970f43..4ba0358bd027 100644 --- a/ui/src/i18n/.i18n/uk.meta.json +++ b/ui/src/i18n/.i18n/uk.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:37.448Z", + "generatedAt": "2026-05-30T19:58:55.895Z", "locale": "uk", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/vi.meta.json b/ui/src/i18n/.i18n/vi.meta.json index e8e2a0eff237..3b16a2e0d668 100644 --- a/ui/src/i18n/.i18n/vi.meta.json +++ b/ui/src/i18n/.i18n/vi.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:47.757Z", + "generatedAt": "2026-05-30T19:58:58.704Z", "locale": "vi", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/zh-CN.meta.json b/ui/src/i18n/.i18n/zh-CN.meta.json index cfeda000b88d..6e0fb37afb7b 100644 --- a/ui/src/i18n/.i18n/zh-CN.meta.json +++ b/ui/src/i18n/.i18n/zh-CN.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:03.159Z", + "generatedAt": "2026-05-30T19:58:48.092Z", "locale": "zh-CN", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/zh-TW.meta.json b/ui/src/i18n/.i18n/zh-TW.meta.json index 6bcbfa525b05..90f54011fd84 100644 --- a/ui/src/i18n/.i18n/zh-TW.meta.json +++ b/ui/src/i18n/.i18n/zh-TW.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-05-30T15:38:05.611Z", + "generatedAt": "2026-05-30T19:58:48.848Z", "locale": "zh-TW", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "d2195849e06087f13cb1c7ff6afd46aa15649304779e3cbfe6ab704c603693a7", - "totalKeys": 1287, - "translatedKeys": 1287, + "sourceHash": "6a593959229f82b0f1b312c7d387f1095137a71067734613f0ef98d8085dea5a", + "totalKeys": 1289, + "translatedKeys": 1289, "workflow": 1 } diff --git a/ui/src/i18n/locales/ar.ts b/ui/src/i18n/locales/ar.ts index a896c795559b..b26a05d084c9 100644 --- a/ui/src/i18n/locales/ar.ts +++ b/ui/src/i18n/locales/ar.ts @@ -753,6 +753,10 @@ export const ar: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "الوكيل", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "المشهد", diary: "اليوميات", diff --git a/ui/src/i18n/locales/de.ts b/ui/src/i18n/locales/de.ts index 4622cfc4ba37..66a7ede269a3 100644 --- a/ui/src/i18n/locales/de.ts +++ b/ui/src/i18n/locales/de.ts @@ -765,6 +765,10 @@ export const de: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agent", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Szene", diary: "Tagebuch", diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 45c668ee34c6..b232e5fa17f1 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -755,6 +755,10 @@ export const en: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agent", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Scene", diary: "Diary", diff --git a/ui/src/i18n/locales/es.ts b/ui/src/i18n/locales/es.ts index cb6f2ba083a9..59b2166f4bf2 100644 --- a/ui/src/i18n/locales/es.ts +++ b/ui/src/i18n/locales/es.ts @@ -759,6 +759,10 @@ export const es: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agente", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Escena", diary: "Diario", diff --git a/ui/src/i18n/locales/fa.ts b/ui/src/i18n/locales/fa.ts index 9d1e3228251d..e75966c1c3fe 100644 --- a/ui/src/i18n/locales/fa.ts +++ b/ui/src/i18n/locales/fa.ts @@ -760,6 +760,10 @@ export const fa: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "عامل", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "صحنه", diary: "دفترچه", diff --git a/ui/src/i18n/locales/fr.ts b/ui/src/i18n/locales/fr.ts index 2ee909b6b427..bb3f80e3cec0 100644 --- a/ui/src/i18n/locales/fr.ts +++ b/ui/src/i18n/locales/fr.ts @@ -763,6 +763,10 @@ export const fr: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agent", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Scène", diary: "Journal", diff --git a/ui/src/i18n/locales/id.ts b/ui/src/i18n/locales/id.ts index 55eb460045ba..98ff595cebe7 100644 --- a/ui/src/i18n/locales/id.ts +++ b/ui/src/i18n/locales/id.ts @@ -759,6 +759,10 @@ export const id: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agen", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Scene", diary: "Diary", diff --git a/ui/src/i18n/locales/it.ts b/ui/src/i18n/locales/it.ts index 962f17c9be88..6cddba27d58e 100644 --- a/ui/src/i18n/locales/it.ts +++ b/ui/src/i18n/locales/it.ts @@ -761,6 +761,10 @@ export const it: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agente", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Scena", diary: "Diario", diff --git a/ui/src/i18n/locales/ja-JP.ts b/ui/src/i18n/locales/ja-JP.ts index f127817b716a..8111850330c6 100644 --- a/ui/src/i18n/locales/ja-JP.ts +++ b/ui/src/i18n/locales/ja-JP.ts @@ -763,6 +763,10 @@ export const ja_JP: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "エージェント", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Scene", diary: "Diary", diff --git a/ui/src/i18n/locales/ko.ts b/ui/src/i18n/locales/ko.ts index b65efa089a76..02de285fbfbc 100644 --- a/ui/src/i18n/locales/ko.ts +++ b/ui/src/i18n/locales/ko.ts @@ -758,6 +758,10 @@ export const ko: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "에이전트", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "장면", diary: "일지", diff --git a/ui/src/i18n/locales/nl.ts b/ui/src/i18n/locales/nl.ts index e7d7e33c9344..948b590b9c1d 100644 --- a/ui/src/i18n/locales/nl.ts +++ b/ui/src/i18n/locales/nl.ts @@ -762,6 +762,10 @@ export const nl: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agent", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Scène", diary: "Dagboek", diff --git a/ui/src/i18n/locales/pl.ts b/ui/src/i18n/locales/pl.ts index cea9daefd4c8..1de1436e62b4 100644 --- a/ui/src/i18n/locales/pl.ts +++ b/ui/src/i18n/locales/pl.ts @@ -761,6 +761,10 @@ export const pl: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agent", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Scena", diary: "Dziennik", diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index e430fb9fcc96..504203545763 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -759,6 +759,10 @@ export const pt_BR: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agente", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Cena", diary: "Diário", diff --git a/ui/src/i18n/locales/th.ts b/ui/src/i18n/locales/th.ts index 97b24cf02c36..3977573c0df8 100644 --- a/ui/src/i18n/locales/th.ts +++ b/ui/src/i18n/locales/th.ts @@ -751,6 +751,10 @@ export const th: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "เอเจนต์", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "ฉาก", diary: "ไดอารี", diff --git a/ui/src/i18n/locales/tr.ts b/ui/src/i18n/locales/tr.ts index 2740eca1d4b9..9b6613008a05 100644 --- a/ui/src/i18n/locales/tr.ts +++ b/ui/src/i18n/locales/tr.ts @@ -764,6 +764,10 @@ export const tr: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Aracı", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Sahne", diary: "Günlük", diff --git a/ui/src/i18n/locales/uk.ts b/ui/src/i18n/locales/uk.ts index 753b5cffd8fb..0a035201e7b8 100644 --- a/ui/src/i18n/locales/uk.ts +++ b/ui/src/i18n/locales/uk.ts @@ -762,6 +762,10 @@ export const uk: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Агент", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Сцена", diary: "Щоденник", diff --git a/ui/src/i18n/locales/vi.ts b/ui/src/i18n/locales/vi.ts index e637fb44cd27..0aa1bddf3987 100644 --- a/ui/src/i18n/locales/vi.ts +++ b/ui/src/i18n/locales/vi.ts @@ -757,6 +757,10 @@ export const vi: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "Agent", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "Cảnh", diary: "Nhật ký", diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index 7a5e5381b455..e6484e1c26e0 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -750,6 +750,10 @@ export const zh_CN: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "代理", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "场景", diary: "日记", diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index 996621510d14..1c357ea6ddde 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -750,6 +750,10 @@ export const zh_TW: TranslationMap = { }, }, dreaming: { + agentSelect: { + label: "代理", + ariaLabel: "Dreaming agent", + }, tabs: { scene: "場景", diary: "日誌", diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 8b676f5f5d9c..120d401d30ac 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -27,6 +27,11 @@ import { import { hasOperatorWriteAccess, warnQueryToken } from "./app-settings.ts"; import type { AppViewState } from "./app-view-state.ts"; import { reconcileChatRunLifecycle } from "./chat/run-lifecycle.ts"; +import { + resolveChatAgentFilterId, + resolveChatAgentFilterOptions, + resolvePreferredSessionForAgent, +} from "./chat/session-controls.ts"; import { controlUiNowMs, recordControlUiRenderTiming, @@ -1028,10 +1033,16 @@ export function renderApp(state: AppViewState) { const configuredDreaming = resolveConfiguredDreaming(configValue); const dreamingOn = state.dreamingStatus?.enabled ?? configuredDreaming.enabled; const dreamingNextCycle = resolveDreamingNextCycle(state.dreamingStatus); + const dreamingAgentOptions = resolveChatAgentFilterOptions(state); + const dreamingSelectedAgentId = resolveChatAgentFilterId(state, state.sessionKey); + const syncDreamingSelectedAgent = () => { + state.selectedAgentId = dreamingSelectedAgentId; + }; const dreamingLoading = state.dreamingStatusLoading || state.dreamingModeSaving; const dreamingRefreshLoading = state.dreamingStatusLoading || state.dreamDiaryLoading; const refreshDreaming = () => { void (async () => { + syncDreamingSelectedAgent(); await loadConfig(state); await Promise.all([ loadDreamingStatus(state), @@ -3085,6 +3096,8 @@ export function renderApp(state: AppViewState) { ${state.tab === "dreams" ? renderDreaming({ active: dreamingOn, + selectedAgentId: dreamingSelectedAgentId, + agentOptions: dreamingAgentOptions, shortTermCount: state.dreamingStatus?.shortTermCount ?? 0, groundedSignalCount: state.dreamingStatus?.groundedSignalCount ?? 0, totalSignalCount: state.dreamingStatus?.totalSignalCount ?? 0, @@ -3117,7 +3130,16 @@ export function renderApp(state: AppViewState) { wikiMemoryPalaceError: state.wikiMemoryPalaceError, wikiMemoryPalace: state.wikiMemoryPalace, onRefresh: refreshDreaming, - onRefreshDiary: () => loadDreamDiary(state), + onSelectAgent: (agentId: string) => { + state.selectedAgentId = agentId; + switchChatSession(state, resolvePreferredSessionForAgent(state, agentId)); + void loadDreamingStatus(state); + void loadDreamDiary(state); + }, + onRefreshDiary: () => { + syncDreamingSelectedAgent(); + void loadDreamDiary(state); + }, onRefreshImports: () => { void (async () => { await loadConfig(state); @@ -3132,14 +3154,29 @@ export function renderApp(state: AppViewState) { }, onOpenConfig: () => openConfigFile(state), onOpenWikiPage: (lookup: string) => openWikiPage(lookup), - onBackfillDiary: () => backfillDreamDiary(state), + onBackfillDiary: () => { + syncDreamingSelectedAgent(); + void backfillDreamDiary(state); + }, onCopyDreamingArchivePath: () => { void copyDreamingArchivePath(state); }, - onDedupeDreamDiary: () => dedupeDreamDiary(state), - onResetDiary: () => resetDreamDiary(state), - onResetGroundedShortTerm: () => resetGroundedShortTerm(state), - onRepairDreamingArtifacts: () => repairDreamingArtifacts(state), + onDedupeDreamDiary: () => { + syncDreamingSelectedAgent(); + void dedupeDreamDiary(state); + }, + onResetDiary: () => { + syncDreamingSelectedAgent(); + void resetDreamDiary(state); + }, + onResetGroundedShortTerm: () => { + syncDreamingSelectedAgent(); + void resetGroundedShortTerm(state); + }, + onRepairDreamingArtifacts: () => { + syncDreamingSelectedAgent(); + void repairDreamingArtifacts(state); + }, onRequestUpdate: requestHostUpdate, }) : nothing} diff --git a/ui/src/ui/app-settings.refresh-active-tab.node.test.ts b/ui/src/ui/app-settings.refresh-active-tab.node.test.ts index 12b3d6e13ddc..9282b5422177 100644 --- a/ui/src/ui/app-settings.refresh-active-tab.node.test.ts +++ b/ui/src/ui/app-settings.refresh-active-tab.node.test.ts @@ -42,6 +42,10 @@ const mocks = vi.hoisted(() => ({ loadCronRunsMock: vi.fn<() => Promise>(async () => "ok"), loadDebugMock: vi.fn(async () => {}), loadDevicesMock: vi.fn(async () => {}), + loadDreamDiaryMock: vi.fn(async () => {}), + loadDreamingStatusMock: vi.fn(async () => {}), + loadWikiImportInsightsMock: vi.fn(async () => {}), + loadWikiMemoryPalaceMock: vi.fn(async () => {}), loadExecApprovalsMock: vi.fn(async () => {}), loadLogsMock: vi.fn(async () => {}), loadModelAuthStatusStateMock: vi.fn(async () => {}), @@ -105,6 +109,12 @@ vi.mock("./controllers/debug.ts", () => ({ vi.mock("./controllers/devices.ts", () => ({ loadDevices: mocks.loadDevicesMock, })); +vi.mock("./controllers/dreaming.ts", () => ({ + loadDreamDiary: mocks.loadDreamDiaryMock, + loadDreamingStatus: mocks.loadDreamingStatusMock, + loadWikiImportInsights: mocks.loadWikiImportInsightsMock, + loadWikiMemoryPalace: mocks.loadWikiMemoryPalaceMock, +})); vi.mock("./controllers/exec-approvals.ts", () => ({ loadExecApprovals: mocks.loadExecApprovalsMock, })); @@ -157,6 +167,7 @@ function createHost() { cronRunsJobId: null as string | null, sessionsChangedReloadTimer: null as number | ReturnType | null, sessionKey: "main", + selectedAgentId: null as string | null, settings: {}, basePath: "", }; @@ -223,6 +234,27 @@ describe("refreshActiveTab", () => { tools: null, } as const; + it("syncs selected agent before refreshing the Dreams tab", async () => { + const host = createHost(); + host.tab = "dreams"; + host.sessionKey = "agent:research:main"; + mocks.loadDreamingStatusMock.mockImplementationOnce(async () => { + expect(host.selectedAgentId).toBe("research"); + }); + mocks.loadDreamDiaryMock.mockImplementationOnce(async () => { + expect(host.selectedAgentId).toBe("research"); + }); + + await refreshActiveTab(host as unknown as Parameters[0]); + + expect(host.selectedAgentId).toBe("research"); + expect(mocks.loadConfigMock).toHaveBeenCalledOnce(); + expect(mocks.loadDreamingStatusMock).toHaveBeenCalledWith(host); + expect(mocks.loadDreamDiaryMock).toHaveBeenCalledWith(host); + expect(mocks.loadWikiImportInsightsMock).toHaveBeenCalledWith(host); + expect(mocks.loadWikiMemoryPalaceMock).toHaveBeenCalledWith(host); + }); + for (const panel of ["files", "skills", "channels", "tools"] as const) { it(`routes agents ${panel} panel refresh through the expected loaders`, async () => { const host = createHost(); diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index f3631331379f..d277c7229a15 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -66,6 +66,7 @@ import { tabFromPath, type Tab, } from "./navigation.ts"; +import { normalizeAgentId, parseAgentSessionKey } from "./session-key.ts"; import { normalizeTextScale, saveLocalUserIdentity, @@ -100,6 +101,7 @@ type SettingsHost = { eventLogBuffer: unknown[]; basePath: string; agentsList?: AgentsListResult | null; + selectedAgentId?: string | null; agentsSelectedId?: string | null; agentsPanel?: "overview" | "files" | "tools" | "skills" | "channels" | "cron"; pendingGatewayUrl?: string | null; @@ -127,6 +129,12 @@ type LocalUserIdentityHost = { userAvatar?: string | null; }; +function resolveDreamingAgentIdForSession(host: SettingsHost): string { + return normalizeAgentId( + parseAgentSessionKey(host.sessionKey)?.agentId ?? host.agentsList?.defaultId ?? "main", + ); +} + type SettingsAppHost = SettingsHost & AgentFilesState & AgentIdentityState & @@ -466,6 +474,7 @@ export async function refreshActiveTab(host: SettingsHost) { await Promise.allSettled([loadDevices(app), loadConfig(app), loadExecApprovals(app)]); break; case "dreams": + host.selectedAgentId = resolveDreamingAgentIdForSession(host); await loadConfig(app); await Promise.all([ loadDreamingStatus(app), diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index b8859694b965..40c2bac1dfbc 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -204,6 +204,7 @@ export type AppViewState = { configUiHints: ConfigUiHints; configForm: Record | null; configFormOriginal: Record | null; + selectedAgentId: string | null; dreamingStatusLoading: boolean; dreamingStatusError: string | null; dreamingStatus: import("./controllers/dreaming.js").DreamingStatus | null; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 95a2c9848fc9..f9fff6c47310 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -344,6 +344,7 @@ export class OpenClawApp extends LitElement { @state() configUiHints: ConfigUiHints = {}; @state() configForm: Record | null = null; @state() configFormOriginal: Record | null = null; + @state() selectedAgentId: string | null = null; @state() dreamingStatusLoading = false; @state() dreamingStatusError: string | null = null; @state() dreamingStatus: DreamingStatus | null = null; diff --git a/ui/src/ui/chat/session-controls.ts b/ui/src/ui/chat/session-controls.ts index c78df4e5ee1d..f73eaa8a2e92 100644 --- a/ui/src/ui/chat/session-controls.ts +++ b/ui/src/ui/chat/session-controls.ts @@ -1120,7 +1120,7 @@ type ChatAgentFilterOption = { label: string; }; -function resolveChatAgentFilterId(state: AppViewState, sessionKey: string): string { +export function resolveChatAgentFilterId(state: AppViewState, sessionKey: string): string { const parsed = parseAgentSessionKey(sessionKey); return normalizeAgentId(parsed?.agentId ?? state.agentsList?.defaultId ?? "main"); } @@ -1186,7 +1186,7 @@ function rowsForPreferredAgentSession( return [...byKey.values()]; } -function resolvePreferredSessionForAgent(state: AppViewState, agentId: string): string { +export function resolvePreferredSessionForAgent(state: AppViewState, agentId: string): string { const normalizedAgentId = normalizeAgentId(agentId); if (resolveChatAgentFilterId(state, state.sessionKey) === normalizedAgentId) { return state.sessionKey; @@ -1206,7 +1206,7 @@ function resolvePreferredSessionForAgent(state: AppViewState, agentId: string): return buildAgentMainSessionKey({ agentId: normalizedAgentId }); } -function resolveChatAgentFilterOptions(state: AppViewState): ChatAgentFilterOption[] { +export function resolveChatAgentFilterOptions(state: AppViewState): ChatAgentFilterOption[] { const seen = new Set(); const options: ChatAgentFilterOption[] = []; const add = (agentId: string) => { diff --git a/ui/src/ui/controllers/dreaming.test.ts b/ui/src/ui/controllers/dreaming.test.ts index 909ba522c26f..1ab7834ac2a1 100644 --- a/ui/src/ui/controllers/dreaming.test.ts +++ b/ui/src/ui/controllers/dreaming.test.ts @@ -25,6 +25,7 @@ function createState(): { state: DreamingState; request: ReturnType() { + let resolve: ((value: T | PromiseLike) => void) | undefined; + let reject: ((reason?: unknown) => void) | undefined; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + if (!resolve || !reject) { + throw new Error("Expected deferred promise callbacks to be initialized"); + } + return { promise, resolve, reject }; +} + function getConfigPatchRawPayload(request: ReturnType): Record { const patchCall = request.mock.calls.find((entry) => entry[0] === "config.patch"); if (!patchCall) { @@ -211,6 +225,90 @@ describe("dreaming controller", () => { expect(state.dreamingStatusError).toBeNull(); }); + it("loads dreaming status for the selected agent", async () => { + const { state, request } = createState(); + state.selectedAgentId = "research-analyst"; + request.mockResolvedValue({ + dreaming: { + enabled: true, + shortTermCount: 1, + }, + }); + + await loadDreamingStatus(state); + + expect(request).toHaveBeenCalledWith("doctor.memory.status", { + agentId: "research-analyst", + }); + }); + + it("starts a new selected-agent status load and ignores stale completions", async () => { + const { state, request } = createState(); + const agentA = createDeferred(); + const agentB = createDeferred(); + request.mockImplementation(async (_method: string, payload?: { agentId?: string }) => { + return payload?.agentId === "agent-b" ? agentB.promise : agentA.promise; + }); + + state.selectedAgentId = "agent-a"; + const firstLoad = loadDreamingStatus(state); + state.selectedAgentId = "agent-b"; + const secondLoad = loadDreamingStatus(state); + + expect(request).toHaveBeenCalledWith("doctor.memory.status", { agentId: "agent-a" }); + expect(request).toHaveBeenCalledWith("doctor.memory.status", { agentId: "agent-b" }); + + agentB.resolve({ dreaming: { enabled: true, shortTermCount: 2 } }); + await secondLoad; + + expect(state.dreamingStatus?.shortTermCount).toBe(2); + expect(state.dreamingStatusLoading).toBe(false); + + agentA.resolve({ dreaming: { enabled: true, shortTermCount: 1 } }); + await firstLoad; + + expect(state.dreamingStatus?.shortTermCount).toBe(2); + expect(state.dreamingStatusLoading).toBe(false); + expect(state.dreamingStatusError).toBeNull(); + }); + + it("ignores older same-agent status completions after switching back", async () => { + const { state, request } = createState(); + const firstAgentA = createDeferred(); + const agentB = createDeferred(); + const secondAgentA = createDeferred(); + request + .mockImplementationOnce(async () => firstAgentA.promise) + .mockImplementationOnce(async () => agentB.promise) + .mockImplementationOnce(async () => secondAgentA.promise); + + state.selectedAgentId = "agent-a"; + const firstLoad = loadDreamingStatus(state); + state.selectedAgentId = "agent-b"; + const secondLoad = loadDreamingStatus(state); + state.selectedAgentId = "agent-a"; + const thirdLoad = loadDreamingStatus(state); + + expect(request).toHaveBeenCalledWith("doctor.memory.status", { agentId: "agent-a" }); + expect(request).toHaveBeenCalledWith("doctor.memory.status", { agentId: "agent-b" }); + expect(request).toHaveBeenCalledTimes(3); + + secondAgentA.resolve({ dreaming: { enabled: true, shortTermCount: 3 } }); + await thirdLoad; + + expect(state.dreamingStatus?.shortTermCount).toBe(3); + expect(state.dreamingStatusLoading).toBe(false); + + firstAgentA.resolve({ dreaming: { enabled: true, shortTermCount: 1 } }); + agentB.resolve({ dreaming: { enabled: true, shortTermCount: 2 } }); + await firstLoad; + await secondLoad; + + expect(state.dreamingStatus?.shortTermCount).toBe(3); + expect(state.dreamingStatusLoading).toBe(false); + expect(state.dreamingStatusError).toBeNull(); + }); + it("preserves unknown phase state when status omits phase metadata", async () => { const { state, request } = createState(); request.mockResolvedValue({ @@ -849,6 +947,89 @@ describe("dreaming controller", () => { expect(state.dreamDiaryError).toBeNull(); }); + it("loads dream diary content for the selected agent", async () => { + const { state, request } = createState(); + state.selectedAgentId = "infra-sre"; + request.mockResolvedValue({ + found: true, + path: "DREAMS.md", + content: "infra dreams", + }); + + await loadDreamDiary(state); + + expect(request).toHaveBeenCalledWith("doctor.memory.dreamDiary", { + agentId: "infra-sre", + }); + }); + + it("starts a new selected-agent diary load and ignores stale completions", async () => { + const { state, request } = createState(); + const agentA = createDeferred(); + const agentB = createDeferred(); + request.mockImplementation(async (_method: string, payload?: { agentId?: string }) => { + return payload?.agentId === "agent-b" ? agentB.promise : agentA.promise; + }); + + state.selectedAgentId = "agent-a"; + const firstLoad = loadDreamDiary(state); + state.selectedAgentId = "agent-b"; + const secondLoad = loadDreamDiary(state); + + expect(request).toHaveBeenCalledWith("doctor.memory.dreamDiary", { agentId: "agent-a" }); + expect(request).toHaveBeenCalledWith("doctor.memory.dreamDiary", { agentId: "agent-b" }); + + agentB.resolve({ found: true, path: "DREAMS.md", content: "agent-b diary" }); + await secondLoad; + + expect(state.dreamDiaryContent).toBe("agent-b diary"); + expect(state.dreamDiaryLoading).toBe(false); + + agentA.resolve({ found: true, path: "DREAMS.md", content: "agent-a diary" }); + await firstLoad; + + expect(state.dreamDiaryContent).toBe("agent-b diary"); + expect(state.dreamDiaryLoading).toBe(false); + expect(state.dreamDiaryError).toBeNull(); + }); + + it("ignores older same-agent diary completions after switching back", async () => { + const { state, request } = createState(); + const firstAgentA = createDeferred(); + const agentB = createDeferred(); + const secondAgentA = createDeferred(); + request + .mockImplementationOnce(async () => firstAgentA.promise) + .mockImplementationOnce(async () => agentB.promise) + .mockImplementationOnce(async () => secondAgentA.promise); + + state.selectedAgentId = "agent-a"; + const firstLoad = loadDreamDiary(state); + state.selectedAgentId = "agent-b"; + const secondLoad = loadDreamDiary(state); + state.selectedAgentId = "agent-a"; + const thirdLoad = loadDreamDiary(state); + + expect(request).toHaveBeenCalledWith("doctor.memory.dreamDiary", { agentId: "agent-a" }); + expect(request).toHaveBeenCalledWith("doctor.memory.dreamDiary", { agentId: "agent-b" }); + expect(request).toHaveBeenCalledTimes(3); + + secondAgentA.resolve({ found: true, path: "DREAMS.md", content: "new agent-a diary" }); + await thirdLoad; + + expect(state.dreamDiaryContent).toBe("new agent-a diary"); + expect(state.dreamDiaryLoading).toBe(false); + + firstAgentA.resolve({ found: true, path: "DREAMS.md", content: "old agent-a diary" }); + agentB.resolve({ found: true, path: "DREAMS.md", content: "agent-b diary" }); + await firstLoad; + await secondLoad; + + expect(state.dreamDiaryContent).toBe("new agent-a diary"); + expect(state.dreamDiaryLoading).toBe(false); + expect(state.dreamDiaryError).toBeNull(); + }); + it("handles missing dream diary without error", async () => { const { state, request } = createState(); request.mockResolvedValue({ @@ -941,6 +1122,36 @@ describe("dreaming controller", () => { expect(state.dreamDiaryActionLoading).toBe(false); }); + it("runs dream diary actions and reloads state for the selected agent", async () => { + const { state, request } = createState(); + state.selectedAgentId = "fishing-bot"; + request.mockImplementation(async (method: string) => { + if (method === "doctor.memory.backfillDreamDiary") { + return { action: "backfill", written: 1 }; + } + if (method === "doctor.memory.dreamDiary") { + return { found: true, path: "DREAMS.md", content: "fish dreams" }; + } + if (method === "doctor.memory.status") { + return { dreaming: null }; + } + return {}; + }); + + const ok = await backfillDreamDiary(state); + + expect(ok).toBe(true); + expect(request).toHaveBeenCalledWith("doctor.memory.backfillDreamDiary", { + agentId: "fishing-bot", + }); + expect(request).toHaveBeenCalledWith("doctor.memory.dreamDiary", { + agentId: "fishing-bot", + }); + expect(request).toHaveBeenCalledWith("doctor.memory.status", { + agentId: "fishing-bot", + }); + }); + it("resets and reloads dream diary state", async () => { const { state, request } = createState(); request.mockImplementation(async (method: string) => { diff --git a/ui/src/ui/controllers/dreaming.ts b/ui/src/ui/controllers/dreaming.ts index 2a535f4e968c..512d7fb8679f 100644 --- a/ui/src/ui/controllers/dreaming.ts +++ b/ui/src/ui/controllers/dreaming.ts @@ -211,10 +211,19 @@ export type DreamingState = { hello: GatewayHelloOk | null; configSnapshot: ConfigSnapshot | null; applySessionKey: string; + selectedAgentId: string | null; + dreamingStatusRequestAgentId?: string | null; + dreamingStatusRequestGeneration?: number; + dreamingStatusActiveRequestGeneration?: number | null; + dreamingStatusAgentId?: string | null; dreamingStatusLoading: boolean; dreamingStatusError: string | null; dreamingStatus: DreamingStatus | null; dreamingModeSaving: boolean; + dreamDiaryRequestAgentId?: string | null; + dreamDiaryRequestGeneration?: number; + dreamDiaryActiveRequestGeneration?: number | null; + dreamDiaryAgentId?: string | null; dreamDiaryLoading: boolean; dreamDiaryActionLoading: boolean; dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null; @@ -326,6 +335,22 @@ function normalizeTrimmedString(value: unknown): string | undefined { return trimmed.length > 0 ? trimmed : undefined; } +function resolveSelectedAgentId(state: DreamingState): string | null { + return normalizeTrimmedString(state.selectedAgentId) ?? null; +} + +function buildSelectedAgentPayloadForAgentId( + agentId: string | null, +): { agentId: string } | Record { + return agentId ? { agentId } : {}; +} + +function buildSelectedAgentPayload( + state: DreamingState, +): { agentId: string } | Record { + return buildSelectedAgentPayloadForAgentId(resolveSelectedAgentId(state)); +} + function normalizeBoolean(value: unknown, fallback = false): boolean { return typeof value === "boolean" ? value : fallback; } @@ -775,35 +800,83 @@ function normalizeDreamingStatus(raw: unknown): DreamingStatus | null { } export async function loadDreamingStatus(state: DreamingState): Promise { - if (!state.client || !state.connected || state.dreamingStatusLoading) { + if (!state.client || !state.connected) { return; } + const agentId = resolveSelectedAgentId(state); + if (state.dreamingStatusLoading && state.dreamingStatusRequestAgentId === agentId) { + return; + } + if (state.dreamingStatusAgentId !== agentId) { + state.dreamingStatus = null; + } + const requestGeneration = (state.dreamingStatusRequestGeneration ?? 0) + 1; + state.dreamingStatusRequestGeneration = requestGeneration; + state.dreamingStatusActiveRequestGeneration = requestGeneration; + state.dreamingStatusRequestAgentId = agentId; state.dreamingStatusLoading = true; state.dreamingStatusError = null; try { const payload = await state.client.request( "doctor.memory.status", - {}, + buildSelectedAgentPayloadForAgentId(agentId), ); + if ( + state.dreamingStatusActiveRequestGeneration !== requestGeneration || + state.dreamingStatusRequestAgentId !== agentId || + resolveSelectedAgentId(state) !== agentId + ) { + return; + } state.dreamingStatus = normalizeDreamingStatus(payload?.dreaming); + state.dreamingStatusAgentId = agentId; } catch (err) { - state.dreamingStatusError = String(err); + if ( + state.dreamingStatusActiveRequestGeneration === requestGeneration && + state.dreamingStatusRequestAgentId === agentId && + resolveSelectedAgentId(state) === agentId + ) { + state.dreamingStatusError = String(err); + } } finally { - state.dreamingStatusLoading = false; + if (state.dreamingStatusActiveRequestGeneration === requestGeneration) { + state.dreamingStatusLoading = false; + state.dreamingStatusRequestAgentId = null; + state.dreamingStatusActiveRequestGeneration = null; + } } } export async function loadDreamDiary(state: DreamingState): Promise { - if (!state.client || !state.connected || state.dreamDiaryLoading) { + if (!state.client || !state.connected) { return; } + const agentId = resolveSelectedAgentId(state); + if (state.dreamDiaryLoading && state.dreamDiaryRequestAgentId === agentId) { + return; + } + if (state.dreamDiaryAgentId !== agentId) { + state.dreamDiaryPath = null; + state.dreamDiaryContent = null; + } + const requestGeneration = (state.dreamDiaryRequestGeneration ?? 0) + 1; + state.dreamDiaryRequestGeneration = requestGeneration; + state.dreamDiaryActiveRequestGeneration = requestGeneration; + state.dreamDiaryRequestAgentId = agentId; state.dreamDiaryLoading = true; state.dreamDiaryError = null; try { const payload = await state.client.request( "doctor.memory.dreamDiary", - {}, + buildSelectedAgentPayloadForAgentId(agentId), ); + if ( + state.dreamDiaryActiveRequestGeneration !== requestGeneration || + state.dreamDiaryRequestAgentId !== agentId || + resolveSelectedAgentId(state) !== agentId + ) { + return; + } const path = normalizeTrimmedString(payload?.path) ?? DEFAULT_DREAM_DIARY_PATH; const found = payload?.found === true; if (found) { @@ -813,10 +886,21 @@ export async function loadDreamDiary(state: DreamingState): Promise { state.dreamDiaryPath = path; state.dreamDiaryContent = null; } + state.dreamDiaryAgentId = agentId; } catch (err) { - state.dreamDiaryError = String(err); + if ( + state.dreamDiaryActiveRequestGeneration === requestGeneration && + state.dreamDiaryRequestAgentId === agentId && + resolveSelectedAgentId(state) === agentId + ) { + state.dreamDiaryError = String(err); + } } finally { - state.dreamDiaryLoading = false; + if (state.dreamDiaryActiveRequestGeneration === requestGeneration) { + state.dreamDiaryLoading = false; + state.dreamDiaryRequestAgentId = null; + state.dreamDiaryActiveRequestGeneration = null; + } } } @@ -902,7 +986,10 @@ async function runDreamDiaryAction( state.dreamDiaryActionMessage = null; state.dreamDiaryActionArchivePath = null; try { - const payload = await state.client.request(method, {}); + const payload = await state.client.request( + method, + buildSelectedAgentPayload(state), + ); if (options?.reloadDiary !== false) { await loadDreamDiary(state); } diff --git a/ui/src/ui/e2e/chat-flow.e2e.test.ts b/ui/src/ui/e2e/chat-flow.e2e.test.ts index 7ba234ad91b0..2faebe6691f0 100644 --- a/ui/src/ui/e2e/chat-flow.e2e.test.ts +++ b/ui/src/ui/e2e/chat-flow.e2e.test.ts @@ -264,7 +264,9 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { await main.getByRole("button", { name: "Chat session" }).click(); await page - .locator('button[data-chat-session-picker-option="true"][data-session-key="agent:main:session-b"]') + .locator( + 'button[data-chat-session-picker-option="true"][data-session-key="agent:main:session-b"]', + ) .click(); await main.getByRole("button", { name: "Chat session" }).getByText("Session B").waitFor({ timeout: 10_000, @@ -273,7 +275,9 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { await main.getByRole("button", { name: "Chat session" }).click(); await page - .locator('button[data-chat-session-picker-option="true"][data-session-key="agent:main:session-a"]') + .locator( + 'button[data-chat-session-picker-option="true"][data-session-key="agent:main:session-a"]', + ) .click(); await main.getByRole("button", { name: "Chat session" }).getByText("Session A").waitFor({ timeout: 10_000, diff --git a/ui/src/ui/views/dreaming.test.ts b/ui/src/ui/views/dreaming.test.ts index ba4bbd7b5b0c..04f35a97be81 100644 --- a/ui/src/ui/views/dreaming.test.ts +++ b/ui/src/ui/views/dreaming.test.ts @@ -11,8 +11,13 @@ import { } from "./dreaming.ts"; function buildProps(overrides?: Partial): DreamingProps { - return { + const props: DreamingProps = { active: true, + selectedAgentId: "main", + agentOptions: [ + { id: "main", label: "main" }, + { id: "ceo", label: "ceo" }, + ], shortTermCount: 47, groundedSignalCount: 9, totalSignalCount: 182, @@ -184,6 +189,7 @@ function buildProps(overrides?: Partial): DreamingProps { ], }, onRefresh: () => {}, + onSelectAgent: () => {}, onRefreshDiary: () => {}, onRefreshImports: () => {}, onRefreshMemoryPalace: () => {}, @@ -195,8 +201,8 @@ function buildProps(overrides?: Partial): DreamingProps { onResetDiary: () => {}, onResetGroundedShortTerm: () => {}, onRepairDreamingArtifacts: () => {}, - ...overrides, }; + return { ...props, ...overrides }; } function renderInto(props: DreamingProps): HTMLDivElement { diff --git a/ui/src/ui/views/dreaming.ts b/ui/src/ui/views/dreaming.ts index edbebe6a50cd..8b9e9a63d5c1 100644 --- a/ui/src/ui/views/dreaming.ts +++ b/ui/src/ui/views/dreaming.ts @@ -1,11 +1,11 @@ import { html, nothing } from "lit"; +import { repeat } from "lit/directives/repeat.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { t } from "../../i18n/index.ts"; import type { DreamingEntry, WikiImportInsights, WikiMemoryPalace, - WikiMemoryPalaceItem, } from "../controllers/dreaming.ts"; import { toSanitizedMarkdownHtml } from "../markdown.ts"; @@ -92,8 +92,15 @@ type DreamingPhaseInfo = { nextRunAtMs?: number; }; +type DreamingAgentOption = { + id: string; + label: string; +}; + export type DreamingProps = { active: boolean; + selectedAgentId: string; + agentOptions: DreamingAgentOption[]; shortTermCount: number; groundedSignalCount: number; totalSignalCount: number; @@ -126,6 +133,7 @@ export type DreamingProps = { wikiMemoryPalaceError: string | null; wikiMemoryPalace: WikiMemoryPalace | null; onRefresh: () => void; + onSelectAgent: (agentId: string) => void; onRefreshDiary: () => void; onRefreshImports: () => void; onRefreshMemoryPalace: () => void; @@ -292,35 +300,63 @@ export function renderDreaming(props: DreamingProps) { return html`
- +
+ + ${props.agentOptions.length > 1 + ? html`` + : nothing} +
${activeSubTab === "scene" ? renderScene(props, idle, dreamText) @@ -579,14 +615,6 @@ function toggleExpandedCard(bucket: Set, key: string, requestUpdate?: () requestUpdate?.(); } -function handleMemoryPalaceCardClick(item: WikiMemoryPalaceItem, props: DreamingProps): void { - if (item.kind === "report") { - void openWikiPreview(item.pagePath, props); - return; - } - toggleExpandedCard(expandedPalaceCards, item.pagePath, props.onRequestUpdate); -} - async function openWikiPreview(lookup: string, props: DreamingProps): Promise { wikiPreviewOpen = true; wikiPreviewLoading = true; @@ -1253,7 +1281,13 @@ function renderMemoryPalaceSection(props: DreamingProps) {
handleMemoryPalaceCardClick(item, props)} + @click=${() => { + if (item.kind === "report") { + void openWikiPreview(item.pagePath, props); + return; + } + toggleExpandedCard(expandedPalaceCards, item.pagePath, props.onRequestUpdate); + }} >
${item.title}