@tea-agent/loop-agent 0.35.1 → 0.35.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +51 -0
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +37 -1
  5. package/dist/build-stamp.json +6 -0
  6. package/dist/cli/program.js +2 -2
  7. package/dist/executors/dag-pi-executor.js +44 -0
  8. package/dist/shared/package-metadata.js +42 -0
  9. package/dist/worker/console/chat/assistant-content.js +121 -0
  10. package/dist/worker/console/chat/pi-runtime.js +17 -32
  11. package/dist/worker/console/chat/routes.js +9 -4
  12. package/dist/worker/console/chat/session-store.js +3 -0
  13. package/dist/worker/console/chat/shortcuts.js +9 -7
  14. package/dist/worker/console/chat/turn-process.js +70 -15
  15. package/dist/worker/console/chat/workspace-landing.js +1 -1
  16. package/dist/worker/console/static/assets/index-DRqZiQ7J.css +1 -0
  17. package/dist/worker/console/static/assets/index-DuVLjCIT.js +57 -0
  18. package/dist/worker/console/static/index.html +2 -2
  19. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -0
  20. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +24 -3
  21. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  22. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +14 -6
  23. package/dist/worker/console/static-src/operator-chat/useChatThread.js +12 -6
  24. package/dist/worker/console/static-src/operator-chat/useComposer.js +15 -1
  25. package/dist/worker/loop-agent/loop-agent-client.js +17 -3
  26. package/dist/worker/observability/read-model.js +20 -0
  27. package/dist/worker/observe/spec-evidence.js +3 -8
  28. package/dist/worker/observe/static/views/dag-inspector.js +6 -71
  29. package/dist/worker/preflight.js +2 -1
  30. package/dist/workflows/dag/backend-test-scenario-param.js +33 -23
  31. package/dist/workflows/dag/contract-output-registry.js +14 -0
  32. package/dist/workflows/dag/contract-validator-registrations.js +8 -0
  33. package/dist/workflows/dag/dynamic-runtime/shared.js +9 -1
  34. package/dist/workflows/dag/frontend-implementation-contract.js +233 -39
  35. package/dist/workflows/dag/frontend-prewrite-gate.js +364 -61
  36. package/dist/workflows/dag/frontend-recovery-plan.js +73 -0
  37. package/dist/workflows/dag/frontend-recovery-root-manifest.js +123 -0
  38. package/dist/workflows/dag/frontend-recovery-run.js +539 -0
  39. package/dist/workflows/dag/frontend-repair.js +219 -18
  40. package/dist/workflows/dag/frontend-verification-trace.js +47 -32
  41. package/dist/workflows/dag/frontend-writer-recovery.js +106 -0
  42. package/dist/workflows/dag/frontend-writer-rollback.js +821 -0
  43. package/dist/workflows/dag/init-hybrid.js +49 -24
  44. package/dist/workflows/dag/node-execution.js +89 -0
  45. package/dist/workflows/dag/recovery-recommendation.js +58 -0
  46. package/dist/workflows/dag/runner.js +245 -11
  47. package/dist/workflows/dag/scheduler.js +257 -3
  48. package/dist/workflows/dag/types.js +130 -2
  49. package/harness.json +1 -1
  50. package/package.json +4 -3
  51. package/dist/worker/console/static/assets/index-HX1pbOyl.css +0 -1
  52. package/dist/worker/console/static/assets/index-M0BLEBfh.js +0 -56
package/AGENTS.md CHANGED
@@ -94,6 +94,8 @@ npm run build
94
94
  node bin/loop-agent.js --help
95
95
  ```
96
96
 
97
+ - 改 `src/**` 后必须先 `npm run build` 再用 `bin/loop-agent.js`:bin 内置 stale-dist 守卫(src 比 dist 新即硬失败,`--ignore-stale-dist` 逃生),`--version` 会显示构建 git SHA,发布/自测前用它确认跑的是目标改动。
98
+
97
99
  文档站变更:`npm run docs:build`。init / architecture / skill entry / pack 定向验证见 verification-matrix。
98
100
 
99
101
  ## 交接
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.35.3] - 2026-08-15
6
+
7
+ ### 新增
8
+
9
+ - chat as default landing + analyze-skills-first new-task flow
10
+ - guide plan verification targets to writeSet-internal files, command-level checks to static
11
+ - console recovery display — surface frontend recovery lineage + outcome
12
+ - frontend-implementation DAG phase 6 — recovery root manifest + statistics dedup
13
+ - frontend-implementation DAG phase 5 — writer partial-write rollback + child recovery
14
+ - frontend-implementation DAG phase 4 — writer transient rollback base
15
+ - frontend-implementation DAG phase 3c — scheduler activation gate + recovery trigger
16
+ - frontend-implementation DAG phase 3b — staged child-creation kernel
17
+ - frontend-implementation DAG phase 3a — recovery state/result types + terminal recovery intent
18
+ - producing-node contract self-check and run robustness hardening
19
+ - frontend-implementation DAG phase 2 — unified writer helper + structured repair assessment
20
+ - slim frontend-implementation DAG phase 1 — prewrite merge + writer admission gate
21
+
22
+ ### 修复
23
+
24
+ - 规范证据面板不再单列 openspec,统一为「规范已读取/规范检索」
25
+ - green full test suite — python3 fallback, build-stamp version normalization, DEP0205 suppression
26
+ - update candidate-continuation fixtures for failureCode (rebase semantic fix)
27
+
28
+ ## [0.35.2] - 2026-08-15
29
+
30
+ ### 重点更新
31
+
32
+ - Operator Chat 处理详情对齐 pi-web:回合尾部的思考内容与工具调用摘要切分为可折叠「处理详情」,流式中思考可见,回合结束后自动折叠,最终回答单独成气泡
33
+
34
+ ### 新增
35
+
36
+ - 对话时间线新增内联可折叠「处理详情」区块,集中展示模型思考与工具调用摘要,回合结束后自动折叠
37
+ - turn-process 保留 SDK thinking blocks,流式传输与持久化均携带思考内容
38
+
39
+ ### 改进
40
+
41
+ - 命令面板 shortcut 优化:Operator 组单行展示,/task、/contract、/dag 分别跳转到任务与恢复页面
42
+
43
+ ### 修复
44
+
45
+ - 修复「处理详情」思考折叠块背景引用未定义样式变量 --panel 的问题,改为确定性透明背景,消除 Console 样式变量完整性门禁对发布的拦截(视觉无变化)
46
+ - 移除自指的 /help 命令项:输入 / 已弹出命令面板,不再单独提供重复的帮助入口
47
+
5
48
  ## [0.35.1] - 2026-08-15
6
49
 
7
50
  ### 重点更新
@@ -31,6 +74,14 @@
31
74
  - 修复文字录入「请参照这三个prd路径XXX」只物化一句话、引用文件从未导入的问题
32
75
  - 修复 semantic intake attempt marker 一次性失败后永不重试的问题:新的 PRD 导入会重置 marker,失败可随新证据重试
33
76
 
77
+ ## [0.35.1-beta.4] - 2026-08-15
78
+
79
+ > 观察面板规范证据不再单列 openspec:读取与检索统一为「规范已读取 / 规范检索」,空态文案同步中性化(beta,发布到 `beta` dist-tag)。
80
+
81
+ ## [0.35.1-beta.3] - 2026-08-14
82
+
83
+ > frontend-implementation DAG 优化 v9 全六阶段 + Console 展示层(beta,发布到 `beta` dist-tag)。
84
+
34
85
  ## [0.35.0] - 2026-08-13
35
86
 
36
87
  ### 重点更新
package/README.md CHANGED
@@ -110,7 +110,7 @@ agent-worker console doctor --repo .
110
110
 
111
111
  ```
112
112
 
113
- 访问 Console <http://127.0.0.1:8790/>;**Operator Chat Official** 是 Console 内的主引导交互。构建时设置 `VITE_OPERATOR_CHAT_DEFAULT_LANDING=chat` 可将 Chat 作为默认 landingflag 未开启时仍默认进入 Tasks,旧 Task Ops tab 始终保留。顶部「检视」以及 `/inspect/` 提供只读 Dashboard、DAG、时间线与 artifacts;`loop-agent …` / `agent-worker …` CLI direct path 仍是受治理 fallback。当前状态为 **code ready, dogfood pending/unmet**,不宣称已替代 openCode 或其他外部主会话。
113
+ 访问 Console <http://127.0.0.1:8790/>;**Operator Chat Official** 是 Console 内的主引导交互,默认打开「对话」工作区。构建时设置 `VITE_OPERATOR_CHAT_DEFAULT_LANDING=tasks` 可改回 TasksURL hash(`#/tasks`)与 localStorage 仍优先于默认值。顶部「检视」以及 `/inspect/` 提供只读 Dashboard、DAG、时间线与 artifacts;`loop-agent …` / `agent-worker …` CLI direct path 仍是受治理 fallback。当前状态为 **code ready, dogfood pending/unmet**,不宣称已替代 openCode 或其他外部主会话。
114
114
 
115
115
  ## 核心边界
116
116
 
package/bin/loop-agent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
 
@@ -13,6 +13,42 @@ if (!existsSync(cliEntry)) {
13
13
  process.exit(1);
14
14
  }
15
15
 
16
+ /**
17
+ * Stale-dist guard: when running from a source checkout (src/ present), refuse
18
+ * to silently execute a dist/ that predates the latest source edit. The
19
+ * classic failure mode is editing src/** and running the bin without
20
+ * `npm run build`, which executes the previous build. Suppress with
21
+ * --ignore-stale-dist when intentionally running a stale build.
22
+ */
23
+ const srcDir = join(packageRoot, "src");
24
+ const ignoreStale = process.argv.includes("--ignore-stale-dist");
25
+ if (ignoreStale) {
26
+ process.argv = process.argv.filter((arg) => arg !== "--ignore-stale-dist");
27
+ }
28
+ if (!ignoreStale && existsSync(srcDir)) {
29
+ const distCliMtime = statSync(cliEntry).mtimeMs;
30
+ let newestSrcMtime = 0;
31
+ const walk = (dir) => {
32
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
33
+ const full = join(dir, entry.name);
34
+ if (entry.isDirectory()) {
35
+ walk(full);
36
+ } else {
37
+ const mtime = statSync(full).mtimeMs;
38
+ if (mtime > newestSrcMtime) newestSrcMtime = mtime;
39
+ }
40
+ }
41
+ };
42
+ walk(srcDir);
43
+ if (newestSrcMtime > distCliMtime) {
44
+ console.error(
45
+ `loop-agent: dist/ is stale relative to src/ (a source file is newer than ${cliEntry}).\n` +
46
+ ` Run \`npm run build\` before using the bin, or pass --ignore-stale-dist to run the stale build intentionally.`,
47
+ );
48
+ process.exit(1);
49
+ }
50
+ }
51
+
16
52
  try {
17
53
  await import(pathToFileURL(cliEntry).href);
18
54
  } catch (error) {
@@ -0,0 +1,6 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "0.35.3",
4
+ "gitSha": "71e2505bbabe052302d753f25b4971e0bdb0f953",
5
+ "builtAt": "2026-08-15T11:42:48.937Z"
6
+ }
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { Command } from "commander";
4
- import { readPackageVersion } from "../shared/package-metadata.js";
4
+ import { readCliVersionLabel } from "../shared/package-metadata.js";
5
5
  import { resolveAdapter } from "../adapters/index.js";
6
6
  import { runCoverageAudit } from "../commands/coverage-audit.js";
7
7
  import { runCoverageReport } from "../commands/coverage-report.js";
@@ -547,7 +547,7 @@ export function buildLoopAgentProgram(options) {
547
547
  program
548
548
  .name("loop-agent")
549
549
  .description("Loop Agent task and DAG automation CLI")
550
- .version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
550
+ .version(readCliVersionLabel(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
551
551
  .showHelpAfterError()
552
552
  .option("--repo-root <path>", "repository root")
553
553
  .option("--adapter <name>", "harness adapter name")
@@ -520,6 +520,33 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
520
520
  }
521
521
  let result;
522
522
  try {
523
+ // Phase 5 (P1-9): capture the writeSet baseline BEFORE the writer provider
524
+ // call so a transient partial write can be rolled back and recovered via a
525
+ // staged child. Only frontend-implementation writers; a failed capture is a
526
+ // hard fail (no baseline → no rollback → no recovery). Dynamic import avoids
527
+ // an executor ↔ scheduler static cycle.
528
+ if (isWriteTask &&
529
+ (input.task.id === "frontend-implement-pi" ||
530
+ input.task.id === "frontend-repair-pi") &&
531
+ (input.task.writeSet?.length ?? 0) > 0) {
532
+ try {
533
+ const { captureFrontendWriterAttemptIntent } = await import("../workflows/dag/frontend-writer-recovery.js");
534
+ await captureFrontendWriterAttemptIntent({
535
+ cwd: input.cwd,
536
+ runDir: meta.runDir,
537
+ writeSet: input.task.writeSet,
538
+ });
539
+ }
540
+ catch (error) {
541
+ return {
542
+ ok: false,
543
+ stdout: "",
544
+ stderr: `frontend writer attempt baseline capture failed: ${error instanceof Error ? error.message : String(error)}`,
545
+ failureCategory: "write-guard",
546
+ durationMs: Date.now() - started,
547
+ };
548
+ }
549
+ }
523
550
  result = await piStepFn({
524
551
  attachedFiles: [],
525
552
  modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
@@ -691,6 +718,23 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
691
718
  const changedFiles = pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), afterSnapshot, beforePathFingerprints, afterPathFingerprints);
692
719
  changeManifestAfterStatus = afterStatus;
693
720
  changeManifestChangedFiles = changedFiles;
721
+ // Phase 5: record the attempt changed paths/hashes into the rollback
722
+ // intent so a transient partial write can be CAS-restored.
723
+ if ((input.task.id === "frontend-implement-pi" ||
724
+ input.task.id === "frontend-repair-pi") &&
725
+ (input.task.writeSet?.length ?? 0) > 0) {
726
+ try {
727
+ const { recordFrontendWriterAttempt } = await import("../workflows/dag/frontend-writer-recovery.js");
728
+ await recordFrontendWriterAttempt({
729
+ cwd: input.cwd,
730
+ runDir: meta.runDir,
731
+ changedPaths: changedFiles,
732
+ });
733
+ }
734
+ catch {
735
+ // best-effort: a missing intent (no baseline captured) is a no-op
736
+ }
737
+ }
694
738
  const guard = validateShellWriteGuardFromDiff({
695
739
  changedFiles,
696
740
  task: input.task,
@@ -45,6 +45,48 @@ export function readPackageName(packageDir) {
45
45
  }
46
46
  return undefined;
47
47
  }
48
+ /**
49
+ * Read the build stamp (git SHA + build time) written by the build pipeline
50
+ * into `<packageDir>/dist/build-stamp.json`. `undefined` when absent (e.g.
51
+ * running from source via tsx, or a hand-assembled package), which callers
52
+ * treat as "no stamp available".
53
+ */
54
+ export function readBuildStamp(packageDir) {
55
+ try {
56
+ const raw = readFileSync(path.join(packageDir, "dist", "build-stamp.json"), "utf-8");
57
+ const parsed = JSON.parse(raw);
58
+ if (parsed.schemaVersion !== 1 ||
59
+ typeof parsed.version !== "string" ||
60
+ typeof parsed.builtAt !== "string")
61
+ return undefined;
62
+ return parsed;
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ }
68
+ /**
69
+ * Human-readable CLI version label: plain version, enriched with the build
70
+ * stamp (short git SHA + build date) when available so a stale dist/ that was
71
+ * built from an old commit is immediately recognizable in `--version`.
72
+ */
73
+ export function readCliVersionLabel(packageDir) {
74
+ const version = readPackageVersion(packageDir) ?? "0.0.0";
75
+ const stamp = readBuildStamp(packageDir);
76
+ if (!stamp?.gitSha)
77
+ return version;
78
+ return `${version} (build ${stamp.gitSha.slice(0, 8)} ${stamp.builtAt.slice(0, 10)})`;
79
+ }
80
+ /**
81
+ * Strip the build-stamp suffix from a CLI `--version` label, returning the bare
82
+ * SemVer. Controller-identity/preflight comparisons must use this rather than
83
+ * the raw label so a build-stamped `0.35.0 (build 7f8c5b5b …)` still matches
84
+ * `packageVersion === "0.35.0"`.
85
+ */
86
+ export function normalizeCliVersionLabel(label) {
87
+ const match = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/.exec(label.trim());
88
+ return match ? match[0] : label.trim();
89
+ }
48
90
  /**
49
91
  * Compute the SHA-256 hash of a file on disk.
50
92
  *
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Assistant content-block helpers shared by Pi event mapping and Operator Chat UI.
3
+ * Mirrors pi-web `lib/message-display.ts` (process vs answer split) without deferred fetch.
4
+ */
5
+ function asRecord(value) {
6
+ if (!value || typeof value !== "object")
7
+ return undefined;
8
+ return value;
9
+ }
10
+ function readString(value) {
11
+ return typeof value === "string" ? value : undefined;
12
+ }
13
+ /** Parse a persisted/SSE content array; unknown shapes are dropped. */
14
+ export function parseAssistantContent(value) {
15
+ if (!Array.isArray(value))
16
+ return undefined;
17
+ const blocks = [];
18
+ for (const item of value) {
19
+ const rec = asRecord(item);
20
+ if (!rec)
21
+ continue;
22
+ const type = readString(rec.type) ?? "";
23
+ if (type === "thinking" || type === "reasoning") {
24
+ const thinking = readString(rec.thinking) ?? readString(rec.text) ?? "";
25
+ blocks.push({ type: "thinking", thinking });
26
+ continue;
27
+ }
28
+ if (type === "toolCall" ||
29
+ type === "tool_call" ||
30
+ type === "toolUse" ||
31
+ type === "tool_use") {
32
+ const toolCallId = readString(rec.toolCallId) ?? readString(rec.id) ?? "";
33
+ if (!toolCallId)
34
+ continue;
35
+ const toolName = readString(rec.toolName) ?? readString(rec.name);
36
+ blocks.push({
37
+ type: "toolCall",
38
+ toolCallId,
39
+ ...(toolName ? { toolName } : {}),
40
+ });
41
+ continue;
42
+ }
43
+ if (type === "text" || type === "output_text" || type === "") {
44
+ const text = readString(rec.text);
45
+ if (text !== undefined)
46
+ blocks.push({ type: "text", text });
47
+ }
48
+ }
49
+ return blocks;
50
+ }
51
+ /**
52
+ * Extract ordered assistant content blocks from a Pi SDK message object.
53
+ * Thinking/reasoning blocks keep their own type so they never leak into answer text.
54
+ */
55
+ export function extractAssistantContentBlocks(message) {
56
+ const rec = asRecord(message);
57
+ if (!rec)
58
+ return [];
59
+ const contents = Array.isArray(rec.content) ? rec.content : [];
60
+ const parts = Array.isArray(rec.parts) ? rec.parts : [];
61
+ const fromArrays = parseAssistantContent([...contents, ...parts]) ?? [];
62
+ if (fromArrays.length > 0)
63
+ return fromArrays;
64
+ const top = readString(rec.text);
65
+ return top ? [{ type: "text", text: top }] : [];
66
+ }
67
+ /**
68
+ * Pi emits message_end for user / toolResult / custom too. Only assistant
69
+ * payloads belong in the Operator Chat assistant bubble (pi-web same split).
70
+ */
71
+ export function isAssistantSdkMessage(message) {
72
+ const rec = asRecord(message);
73
+ if (!rec)
74
+ return false;
75
+ const role = readString(rec.role);
76
+ return role === undefined || role === "assistant";
77
+ }
78
+ /**
79
+ * Latest assistant answer text (text blocks only). Thinking is excluded.
80
+ * Preserves the historical `{ text }` / `{ parts: [{ text }] }` fallbacks.
81
+ */
82
+ export function extractAssistantText(message) {
83
+ return extractAssistantContentBlocks(message)
84
+ .filter((block) => block.type === "text")
85
+ .map((block) => block.text)
86
+ .join("");
87
+ }
88
+ export function isEmptyThinkingBlock(block, options = {}) {
89
+ return (block.type === "thinking" &&
90
+ !options.isStreaming &&
91
+ block.thinking.trim() === "");
92
+ }
93
+ export function getDisplayableAssistantBlocks(blocks, options = {}) {
94
+ return blocks.filter((block) => !isEmptyThinkingBlock(block, options));
95
+ }
96
+ function isFinalAnswerBlock(block) {
97
+ return block.type === "text";
98
+ }
99
+ export function splitFinalAssistantBlocks(blocks, options = {}) {
100
+ const displayable = getDisplayableAssistantBlocks(blocks, options);
101
+ let lastProcessIndex = -1;
102
+ for (let i = displayable.length - 1; i >= 0; i--) {
103
+ if (!isFinalAnswerBlock(displayable[i])) {
104
+ lastProcessIndex = i;
105
+ break;
106
+ }
107
+ }
108
+ if (lastProcessIndex === -1) {
109
+ return { answerBlocks: displayable, processBlocks: [] };
110
+ }
111
+ return {
112
+ answerBlocks: displayable.slice(lastProcessIndex + 1),
113
+ processBlocks: displayable.slice(0, lastProcessIndex + 1),
114
+ };
115
+ }
116
+ export function joinAssistantText(blocks) {
117
+ return blocks
118
+ .filter((block) => block.type === "text")
119
+ .map((block) => block.text)
120
+ .join("");
121
+ }
@@ -28,6 +28,7 @@
28
28
  import { createHash, randomBytes } from "node:crypto";
29
29
  import { access } from "node:fs/promises";
30
30
  import path from "node:path";
31
+ import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
31
32
  import { projectCompactSnapshot, } from "./chat-event-store.js";
32
33
  import { extractUsageSample } from "./usage.js";
33
34
  import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
@@ -563,28 +564,7 @@ export function projectRuntimeSnapshot(input) {
563
564
  hasBash: input.hasBash,
564
565
  };
565
566
  }
566
- /**
567
- * Extract the latest assistant text fragment from an SDK message object.
568
- * Tolerant of multiple message shapes the SDK emits.
569
- */
570
- export function extractAssistantText(message) {
571
- if (!message || typeof message !== "object")
572
- return "";
573
- const m = message;
574
- if (typeof m.text === "string")
575
- return m.text;
576
- const contents = Array.isArray(m.content) ? m.content : [];
577
- const parts = Array.isArray(m.parts) ? m.parts : [];
578
- const segments = [];
579
- for (const c of [...contents, ...parts]) {
580
- if (!c || typeof c !== "object")
581
- continue;
582
- const seg = c;
583
- if (typeof seg.text === "string")
584
- segments.push(seg.text);
585
- }
586
- return segments.join("");
587
- }
567
+ export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
588
568
  function readSessionManagerId(manager) {
589
569
  if (!manager || typeof manager !== "object")
590
570
  return undefined;
@@ -2116,6 +2096,15 @@ export class ConsolePiRuntime {
2116
2096
  this.dispose(id);
2117
2097
  }
2118
2098
  }
2099
+ function mapAssistantMessageEvent(type, sessionId, message) {
2100
+ const content = extractAssistantContentBlocks(message);
2101
+ return {
2102
+ type,
2103
+ sessionId,
2104
+ text: extractAssistantText(message),
2105
+ ...(content.length > 0 ? { content } : {}),
2106
+ };
2107
+ }
2119
2108
  function mapSdkEvent(sessionId, event) {
2120
2109
  switch (event.type) {
2121
2110
  case "agent_start":
@@ -2127,17 +2116,13 @@ function mapSdkEvent(sessionId, event) {
2127
2116
  willRetry: Boolean(event.willRetry),
2128
2117
  };
2129
2118
  case "message_update":
2130
- return {
2131
- type: "message_update",
2132
- sessionId,
2133
- text: extractAssistantText(event.message),
2134
- };
2119
+ if (!isAssistantSdkMessage(event.message))
2120
+ return undefined;
2121
+ return mapAssistantMessageEvent("message_update", sessionId, event.message);
2135
2122
  case "message_end":
2136
- return {
2137
- type: "message_end",
2138
- sessionId,
2139
- text: extractAssistantText(event.message),
2140
- };
2123
+ if (!isAssistantSdkMessage(event.message))
2124
+ return undefined;
2125
+ return mapAssistantMessageEvent("message_end", sessionId, event.message);
2141
2126
  case "turn_end":
2142
2127
  // Usage is emitted from message_end (see prompt subscribe). turn_end
2143
2128
  // carries the same final assistant message and would double-count.
@@ -12,6 +12,7 @@
12
12
  * Mutation routes go through the Console mutation gate (boot token + Origin).
13
13
  * Read routes (GET capabilities / sessions) are open to loopback.
14
14
  */
15
+ import { parseAssistantContent } from "./assistant-content.js";
15
16
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
16
17
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
17
18
  import { cleanChatTitle } from "./session-store.js";
@@ -83,12 +84,16 @@ function turnEventToStorePartial(event) {
83
84
  case "agent_start":
84
85
  return { kind: "agent_start", data: {} };
85
86
  case "message_update":
87
+ case "message_end": {
88
+ const content = parseAssistantContent(event.content);
86
89
  return {
87
- kind: "message_update",
88
- data: { text: String(event.text ?? "") },
90
+ kind: event.type,
91
+ data: {
92
+ text: String(event.text ?? ""),
93
+ ...(content ? { content } : {}),
94
+ },
89
95
  };
90
- case "message_end":
91
- return { kind: "message_end", data: { text: String(event.text ?? "") } };
96
+ }
92
97
  case "usage":
93
98
  return { kind: "usage", data: (event.usage ?? {}) };
94
99
  case "tool_call":
@@ -368,6 +368,9 @@ export async function runChatTurn(options) {
368
368
  pending.push(store.appendMessage(sessionId, {
369
369
  role: "assistant",
370
370
  text: event.text,
371
+ ...(event.content && event.content.length > 0
372
+ ? { content: event.content }
373
+ : {}),
371
374
  }));
372
375
  }
373
376
  break;
@@ -41,13 +41,6 @@ export const SHORTCUTS = [
41
41
  action: "reload",
42
42
  source: "operator",
43
43
  },
44
- {
45
- command: "/help",
46
- label: "Shortcuts",
47
- description: "显示可用 shortcut",
48
- action: "show-help",
49
- source: "operator",
50
- },
51
44
  ];
52
45
  /** Pi Web commands with real browser-equivalent behavior (UI-11). */
53
46
  export const WEB_SLASH_COMMANDS = [
@@ -103,6 +96,15 @@ export function resolveShortcut(input) {
103
96
  return { command: local.command, action: local.action };
104
97
  return { command: token, action: "unknown" };
105
98
  }
99
+ /** Map Operator panel shortcuts onto Console workspace routes. */
100
+ export function workspaceForShortcut(action) {
101
+ if (action === "focus-panel:task" || action === "focus-panel:contract") {
102
+ return "tasks";
103
+ }
104
+ if (action === "focus-panel:dag")
105
+ return "recovery";
106
+ return undefined;
107
+ }
106
108
  /**
107
109
  * Merge Operator, Web, and dynamic Session commands with conflict dedupe.
108
110
  * Priority: operator/web > extension > skill > prompt. /reload is single-card.