mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
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 43a2b17243.
- 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>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -78,10 +78,13 @@ const invokeDoctorMemoryStatus = async (
|
||||
});
|
||||
};
|
||||
|
||||
const invokeDoctorMemoryDreamDiary = async (respond: ReturnType<typeof vi.fn>) => {
|
||||
const invokeDoctorMemoryDreamDiary = async (
|
||||
respond: ReturnType<typeof vi.fn>,
|
||||
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");
|
||||
|
||||
@@ -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<string, unknown>).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);
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
Generated
+4
@@ -753,6 +753,10 @@ export const ar: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "الوكيل",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "المشهد",
|
||||
diary: "اليوميات",
|
||||
|
||||
Generated
+4
@@ -765,6 +765,10 @@ export const de: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agent",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Szene",
|
||||
diary: "Tagebuch",
|
||||
|
||||
@@ -755,6 +755,10 @@ export const en: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agent",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Scene",
|
||||
diary: "Diary",
|
||||
|
||||
Generated
+4
@@ -759,6 +759,10 @@ export const es: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agente",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Escena",
|
||||
diary: "Diario",
|
||||
|
||||
Generated
+4
@@ -760,6 +760,10 @@ export const fa: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "عامل",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "صحنه",
|
||||
diary: "دفترچه",
|
||||
|
||||
Generated
+4
@@ -763,6 +763,10 @@ export const fr: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agent",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Scène",
|
||||
diary: "Journal",
|
||||
|
||||
Generated
+4
@@ -759,6 +759,10 @@ export const id: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agen",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Scene",
|
||||
diary: "Diary",
|
||||
|
||||
Generated
+4
@@ -761,6 +761,10 @@ export const it: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agente",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Scena",
|
||||
diary: "Diario",
|
||||
|
||||
Generated
+4
@@ -763,6 +763,10 @@ export const ja_JP: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "エージェント",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Scene",
|
||||
diary: "Diary",
|
||||
|
||||
Generated
+4
@@ -758,6 +758,10 @@ export const ko: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "에이전트",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "장면",
|
||||
diary: "일지",
|
||||
|
||||
Generated
+4
@@ -762,6 +762,10 @@ export const nl: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agent",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Scène",
|
||||
diary: "Dagboek",
|
||||
|
||||
Generated
+4
@@ -761,6 +761,10 @@ export const pl: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agent",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Scena",
|
||||
diary: "Dziennik",
|
||||
|
||||
Generated
+4
@@ -759,6 +759,10 @@ export const pt_BR: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agente",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Cena",
|
||||
diary: "Diário",
|
||||
|
||||
Generated
+4
@@ -751,6 +751,10 @@ export const th: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "เอเจนต์",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "ฉาก",
|
||||
diary: "ไดอารี",
|
||||
|
||||
Generated
+4
@@ -764,6 +764,10 @@ export const tr: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Aracı",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Sahne",
|
||||
diary: "Günlük",
|
||||
|
||||
Generated
+4
@@ -762,6 +762,10 @@ export const uk: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Агент",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Сцена",
|
||||
diary: "Щоденник",
|
||||
|
||||
Generated
+4
@@ -757,6 +757,10 @@ export const vi: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "Agent",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "Cảnh",
|
||||
diary: "Nhật ký",
|
||||
|
||||
Generated
+4
@@ -750,6 +750,10 @@ export const zh_CN: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "代理",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "场景",
|
||||
diary: "日记",
|
||||
|
||||
Generated
+4
@@ -750,6 +750,10 @@ export const zh_TW: TranslationMap = {
|
||||
},
|
||||
},
|
||||
dreaming: {
|
||||
agentSelect: {
|
||||
label: "代理",
|
||||
ariaLabel: "Dreaming agent",
|
||||
},
|
||||
tabs: {
|
||||
scene: "場景",
|
||||
diary: "日誌",
|
||||
|
||||
+43
-6
@@ -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}
|
||||
|
||||
@@ -42,6 +42,10 @@ const mocks = vi.hoisted(() => ({
|
||||
loadCronRunsMock: vi.fn<() => Promise<CronRunsLoadStatus>>(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<typeof globalThis.setTimeout> | 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<typeof refreshActiveTab>[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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -204,6 +204,7 @@ export type AppViewState = {
|
||||
configUiHints: ConfigUiHints;
|
||||
configForm: Record<string, unknown> | null;
|
||||
configFormOriginal: Record<string, unknown> | null;
|
||||
selectedAgentId: string | null;
|
||||
dreamingStatusLoading: boolean;
|
||||
dreamingStatusError: string | null;
|
||||
dreamingStatus: import("./controllers/dreaming.js").DreamingStatus | null;
|
||||
|
||||
@@ -344,6 +344,7 @@ export class OpenClawApp extends LitElement {
|
||||
@state() configUiHints: ConfigUiHints = {};
|
||||
@state() configForm: Record<string, unknown> | null = null;
|
||||
@state() configFormOriginal: Record<string, unknown> | null = null;
|
||||
@state() selectedAgentId: string | null = null;
|
||||
@state() dreamingStatusLoading = false;
|
||||
@state() dreamingStatusError: string | null = null;
|
||||
@state() dreamingStatus: DreamingStatus | null = null;
|
||||
|
||||
@@ -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<string>();
|
||||
const options: ChatAgentFilterOption[] = [];
|
||||
const add = (agentId: string) => {
|
||||
|
||||
@@ -25,6 +25,7 @@ function createState(): { state: DreamingState; request: ReturnType<typeof vi.fn
|
||||
hello: null,
|
||||
configSnapshot: { hash: "hash-1" },
|
||||
applySessionKey: "main",
|
||||
selectedAgentId: null,
|
||||
dreamingStatusLoading: false,
|
||||
dreamingStatusError: null,
|
||||
dreamingStatus: null,
|
||||
@@ -47,6 +48,19 @@ function createState(): { state: DreamingState; request: ReturnType<typeof vi.fn
|
||||
return { state, request };
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve: ((value: T | PromiseLike<T>) => void) | undefined;
|
||||
let reject: ((reason?: unknown) => void) | undefined;
|
||||
const promise = new Promise<T>((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<typeof vi.fn>): Record<string, unknown> {
|
||||
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<unknown>();
|
||||
const agentB = createDeferred<unknown>();
|
||||
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<unknown>();
|
||||
const agentB = createDeferred<unknown>();
|
||||
const secondAgentA = createDeferred<unknown>();
|
||||
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<unknown>();
|
||||
const agentB = createDeferred<unknown>();
|
||||
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<unknown>();
|
||||
const agentB = createDeferred<unknown>();
|
||||
const secondAgentA = createDeferred<unknown>();
|
||||
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) => {
|
||||
|
||||
@@ -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<string, never> {
|
||||
return agentId ? { agentId } : {};
|
||||
}
|
||||
|
||||
function buildSelectedAgentPayload(
|
||||
state: DreamingState,
|
||||
): { agentId: string } | Record<string, never> {
|
||||
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<void> {
|
||||
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<DoctorMemoryStatusPayload>(
|
||||
"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<void> {
|
||||
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<DoctorMemoryDreamDiaryPayload>(
|
||||
"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<void> {
|
||||
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<DoctorMemoryDreamActionPayload>(method, {});
|
||||
const payload = await state.client.request<DoctorMemoryDreamActionPayload>(
|
||||
method,
|
||||
buildSelectedAgentPayload(state),
|
||||
);
|
||||
if (options?.reloadDiary !== false) {
|
||||
await loadDreamDiary(state);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,8 +11,13 @@ import {
|
||||
} from "./dreaming.ts";
|
||||
|
||||
function buildProps(overrides?: Partial<DreamingProps>): 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>): DreamingProps {
|
||||
],
|
||||
},
|
||||
onRefresh: () => {},
|
||||
onSelectAgent: () => {},
|
||||
onRefreshDiary: () => {},
|
||||
onRefreshImports: () => {},
|
||||
onRefreshMemoryPalace: () => {},
|
||||
@@ -195,8 +201,8 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
|
||||
onResetDiary: () => {},
|
||||
onResetGroundedShortTerm: () => {},
|
||||
onRepairDreamingArtifacts: () => {},
|
||||
...overrides,
|
||||
};
|
||||
return { ...props, ...overrides };
|
||||
}
|
||||
|
||||
function renderInto(props: DreamingProps): HTMLDivElement {
|
||||
|
||||
+73
-39
@@ -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`
|
||||
<div class="dreams-page">
|
||||
<!-- ── Sub-tab bar ── -->
|
||||
<nav class="dreams__tabs">
|
||||
<button
|
||||
class="dreams__tab ${activeSubTab === "scene" ? "dreams__tab--active" : ""}"
|
||||
@click=${() => {
|
||||
activeSubTab = "scene";
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${t("dreaming.tabs.scene")}
|
||||
</button>
|
||||
<button
|
||||
class="dreams__tab ${activeSubTab === "diary" ? "dreams__tab--active" : ""}"
|
||||
@click=${() => {
|
||||
activeSubTab = "diary";
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${t("dreaming.tabs.diary")}
|
||||
</button>
|
||||
<button
|
||||
class="dreams__tab ${activeSubTab === "advanced" ? "dreams__tab--active" : ""}"
|
||||
@click=${() => {
|
||||
activeSubTab = "advanced";
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${t("dreaming.tabs.advanced")}
|
||||
</button>
|
||||
</nav>
|
||||
<div class="dreams__topbar">
|
||||
<nav class="dreams__tabs">
|
||||
<button
|
||||
class="dreams__tab ${activeSubTab === "scene" ? "dreams__tab--active" : ""}"
|
||||
@click=${() => {
|
||||
activeSubTab = "scene";
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${t("dreaming.tabs.scene")}
|
||||
</button>
|
||||
<button
|
||||
class="dreams__tab ${activeSubTab === "diary" ? "dreams__tab--active" : ""}"
|
||||
@click=${() => {
|
||||
activeSubTab = "diary";
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${t("dreaming.tabs.diary")}
|
||||
</button>
|
||||
<button
|
||||
class="dreams__tab ${activeSubTab === "advanced" ? "dreams__tab--active" : ""}"
|
||||
@click=${() => {
|
||||
activeSubTab = "advanced";
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${t("dreaming.tabs.advanced")}
|
||||
</button>
|
||||
</nav>
|
||||
${props.agentOptions.length > 1
|
||||
? html`<label class="field dreams__agent-select">
|
||||
<span class="sr-only">${t("dreaming.agentSelect.label")}</span>
|
||||
<select
|
||||
data-dreaming-agent-select="true"
|
||||
aria-label=${t("dreaming.agentSelect.ariaLabel")}
|
||||
.value=${props.selectedAgentId}
|
||||
@change=${(e: Event) => {
|
||||
const nextAgentId = (e.target as HTMLSelectElement).value;
|
||||
if (nextAgentId === props.selectedAgentId) {
|
||||
return;
|
||||
}
|
||||
props.onSelectAgent(nextAgentId);
|
||||
}}
|
||||
>
|
||||
${repeat(
|
||||
props.agentOptions,
|
||||
(entry) => entry.id,
|
||||
(entry) =>
|
||||
html`<option value=${entry.id} ?selected=${entry.id === props.selectedAgentId}>
|
||||
${entry.label}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
</label>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
${activeSubTab === "scene"
|
||||
? renderScene(props, idle, dreamText)
|
||||
@@ -579,14 +615,6 @@ function toggleExpandedCard(bucket: Set<string>, 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<void> {
|
||||
wikiPreviewOpen = true;
|
||||
wikiPreviewLoading = true;
|
||||
@@ -1253,7 +1281,13 @@ function renderMemoryPalaceSection(props: DreamingProps) {
|
||||
<article
|
||||
class="dreams-diary__insight-card dreams-diary__insight-card--clickable"
|
||||
data-palace-page=${item.pagePath}
|
||||
@click=${() => handleMemoryPalaceCardClick(item, props)}
|
||||
@click=${() => {
|
||||
if (item.kind === "report") {
|
||||
void openWikiPreview(item.pagePath, props);
|
||||
return;
|
||||
}
|
||||
toggleExpandedCard(expandedPalaceCards, item.pagePath, props.onRequestUpdate);
|
||||
}}
|
||||
>
|
||||
<div class="dreams-diary__insight-topline">
|
||||
<div class="dreams-diary__insight-title">${item.title}</div>
|
||||
|
||||
Reference in New Issue
Block a user