@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.0

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 (118) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +72 -163
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/infrastructure/harness/atomic-write.js +1 -2
  18. package/dist/shared/operator/capabilities.js +13 -200
  19. package/dist/shared/package-metadata.js +0 -42
  20. package/dist/task/frontend-project-capability.js +12 -162
  21. package/dist/task/source-prepare/completeness.js +0 -17
  22. package/dist/task/source-prepare/parse-intent.js +1 -15
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -61
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -86
  29. package/dist/worker/console/chat/routes.js +8 -36
  30. package/dist/worker/console/chat/session-store.js +0 -3
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -97
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +13 -373
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +1 -6
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/failure-routing.js +4 -9
  70. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  71. package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
  72. package/dist/workflows/dag/frontend-repair.js +18 -219
  73. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  74. package/dist/workflows/dag/init-hybrid.js +86 -238
  75. package/dist/workflows/dag/lifecycle.js +0 -4
  76. package/dist/workflows/dag/node-execution.js +6 -105
  77. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  78. package/dist/workflows/dag/report.js +0 -6
  79. package/dist/workflows/dag/retry-policy.js +0 -11
  80. package/dist/workflows/dag/runner.js +12 -314
  81. package/dist/workflows/dag/scheduler.js +3 -257
  82. package/dist/workflows/dag/types.js +9 -132
  83. package/dist/workflows/dag/validate.js +2 -7
  84. package/docs/README.md +3 -3
  85. package/docs/architecture/evolution.md +67 -102
  86. package/docs/templates/backend-test-dag.json +50 -32
  87. package/docs/templates/frontend-design-contract.md +14 -34
  88. package/docs/templates/frontend-task-constraints.md +13 -33
  89. package/docs/templates/frontend-task-requirement.md +20 -54
  90. package/docs/templates/init-managed-agents.md +2 -5
  91. package/harness.json +2 -2
  92. package/package.json +3 -4
  93. package/skills/loop-agent/SKILL.md +0 -1
  94. package/skills/loop-agent/references/command-reference.md +0 -3
  95. package/dist/build-stamp.json +0 -6
  96. package/dist/commands/dag-request-interrupt.js +0 -20
  97. package/dist/worker/console/chat/assistant-content.js +0 -121
  98. package/dist/worker/console/chat/semantic-activity.js +0 -471
  99. package/dist/worker/console/operation-run-facts.js +0 -190
  100. package/dist/worker/console/operation-wait.js +0 -355
  101. package/dist/worker/console/recovery-error-copy.js +0 -198
  102. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  103. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
  104. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  105. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  106. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  107. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  108. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  109. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  110. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  111. package/dist/workflows/dag/contract-output-registry.js +0 -14
  112. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  113. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  114. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  115. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  116. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  117. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  118. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -28,7 +28,6 @@
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";
32
31
  import { projectCompactSnapshot, } from "./chat-event-store.js";
33
32
  import { extractUsageSample } from "./usage.js";
34
33
  import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
@@ -58,12 +57,12 @@ import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.
58
57
  * (AC-01: ≤ 1900 chars and ≤ 80% of the 2498-char baseline).
59
58
  */
60
59
  export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
61
- "You are General Operator Chat.",
62
- "Use operator_* tools first. Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for sensitive probes.",
63
- "No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
64
- "Long DAGs: prepareDagExecution runDag operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait with 60s 90s 120s 180s backoff; reset to 60s on state change; 30-60s re-checks when stall suspected. Every operationWait continuation MUST use afterSeq=previous nextSeq (prefer recommendedNextCall); never reuse an older cursor. Heartbeats are not meaningful progress.",
65
- "On failure consume terminal runSummary first. If progress exists, wait; else use a fresh dagRerunPlan; when ineligible use same-task rerun/advance, resume, or Worker retry. A run-failed task without an open gate is terminal: do not re-advance a run-failed task. Before run start, two identical lifecycleState/blocker/revision/DAG-hash observations mean no progress: stop and report after one bounded probe; never loop identical bash/read/grep. Auto-approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
66
- "Continue until success, user stop, or no safe recovery remains; then stop and report failure/evidence. Never create a new task for a transient failure or replace repair-pi with direct edits.",
60
+ "You are the General Operator Chat for loop-agent / agent-worker.",
61
+ "Operate through operator_* tools first. Pi read/write/edit/bash/grep/find/ls are available, but obey the loaded repository AGENTS.md. apply_patch, full-tools, shell, and coding-chat are denied alternate runtimes. Prefer safe-read/safe-grep for sensitive probes.",
62
+ "If a repository has no loop-agent harness and the user requests initialization, run `loop-agent init instructions --repo-root .` then `loop-agent init --repo-root . --profile full --merge`; finish the generated setup, init doctor, inspect, docs audit, and quick verification. For updates use `loop-agent init upgrade --repo-root . --json` until stable; use init check-update only for an explicitly read-only request.",
63
+ "After a DAG is started or accepted, do not end on accepted/queued/running or an operationId. Supervise it to a terminal outcome. Poll operationGet, task status, dagReport, and dagDoctor after about 15 seconds on start/change, every 30 seconds during progress, and every 60 seconds after 3 minutes unchanged. Report only meaningful node/rank changes, review/verify/closeout, recovery, liveness concerns, and terminal outcomes.",
64
+ "On failure read primaryFailure, primaryRecovery, and doctor evidence. If meaningful progress exists, wait. Otherwise use a fresh eligible dagRerunPlan and rerun its safe node; when ineligible follow AGENTS.md/runtime recovery for same-task rerun/advance, resume, or Worker retry. Auto-approve only a bounded writeSet inside allowedPaths, outside forbiddenPaths, without broad/destructive risk, and with structured verification.",
65
+ "Continue until success, user stop, or no safe eligible recovery remains because limits, bindings, auth/quota recovery, or required external authorization are exhausted. Never create a new task for a transient failure or replace repair-pi with direct edits.",
67
66
  ].join("\n");
68
67
  /** Compose the inspectable system prompt actually injected into Operator Chat. */
69
68
  export function composeOperatorChatSystemPrompt(input) {
@@ -564,7 +563,28 @@ export function projectRuntimeSnapshot(input) {
564
563
  hasBash: input.hasBash,
565
564
  };
566
565
  }
567
- export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
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
+ }
568
588
  function readSessionManagerId(manager) {
569
589
  if (!manager || typeof manager !== "object")
570
590
  return undefined;
@@ -678,12 +698,13 @@ export class ConsolePiRuntime {
678
698
  * closure state).
679
699
  */
680
700
  async buildResourceLoaderOptions(base = {}) {
701
+ const runtime = this;
681
702
  return {
682
703
  ...base,
683
704
  skillsOverride: (current) => {
684
705
  // Synchronous override surface: use last-known prefs via a sync
685
706
  // fail-closed path. Prefer async cache filled by ensurePrefs.
686
- const prefs = this.cachedSkillPrefs ?? {
707
+ const prefs = runtime.cachedSkillPrefs ?? {
687
708
  schemaVersion: 1,
688
709
  skills: {},
689
710
  };
@@ -903,27 +924,10 @@ export class ConsolePiRuntime {
903
924
  const customTools = await this.loadCustomTools();
904
925
  if (this.disposedSessions.has(sessionId))
905
926
  return;
906
- // The SDK filters BOTH built-in tools and customTools against
907
- // `tools` (allowed-tool names) inside _refreshToolRegistry:
908
- // isAllowedTool = (!allowedToolNames || allowedToolNames.has(name))
909
- // && !excludedToolNames.has(name)
910
- // Passing only OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS here silently dropped
911
- // every custom operator tool (prepareDagExecution / runDag /
912
- // operationWait / safe-read / git-status / ...) from the registry, so the
913
- // model could only ever see bash/read/write/edit/grep/find/ls. The
914
- // allowlist must cover the full tool surface that is later activated via
915
- // setActiveToolsByName(computeOperatorChatActiveToolNames(session)) —
916
- // operator actions + built-in explore tools + safe explore tools.
917
- // Hard write/coding channels stay blocked via excludeTools below (they
918
- // are removed even when present in the allowed list).
919
927
  const { session } = await this.bindings.createSessionFromServices({
920
928
  services,
921
929
  sessionManager: init.sessionManager,
922
- tools: [
923
- ...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
924
- ...OPERATOR_CHAT_ALLOWED_TOOLS,
925
- ...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
926
- ],
930
+ tools: [...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS],
927
931
  excludeTools: [
928
932
  "apply_patch",
929
933
  "apply-patch",
@@ -1276,7 +1280,6 @@ export class ConsolePiRuntime {
1276
1280
  * session is not active — callers fail closed instead of fabricating data.
1277
1281
  */
1278
1282
  /**
1279
- * ConsolePiRuntime.listSlashCommands
1280
1283
  * Browser-safe slash command projection for UI-11.
1281
1284
  * Sources: current Session extension commands, prompt templates, skills.
1282
1285
  * Never returns prompt/skill file bodies.
@@ -1289,36 +1292,29 @@ export class ConsolePiRuntime {
1289
1292
  .replace(/[A-Za-z]:\\Users\\[^\s]+/gi, "~")
1290
1293
  .slice(0, 240);
1291
1294
  };
1292
- // Fail soft at the HTTP layer: bubble readiness errors so routes can attach
1293
- // a compact warning while the UI keeps local Operator/Pi Web commands.
1294
- await this.ensureSessionReady(sessionId);
1295
+ try {
1296
+ await this.ensureSessionReady(sessionId);
1297
+ }
1298
+ catch {
1299
+ return [];
1300
+ }
1295
1301
  const session = this.sessions.get(sessionId);
1296
1302
  if (!session)
1297
- return { commands: [] };
1298
- // Same-name conflict priority (AC-3): extension > skill > prompt.
1299
- // Enum order must not decide the winner — prompt-before-skill still loses.
1300
- const SOURCE_RANK = {
1301
- extension: 0,
1302
- skill: 1,
1303
- prompt: 2,
1304
- };
1305
- const byKey = new Map();
1306
- const sourceWarnings = [];
1303
+ return [];
1304
+ const out = [];
1305
+ const seen = new Set();
1307
1306
  const push = (command, label, description, source) => {
1308
1307
  const name = command.startsWith("/") ? command : `/${command}`;
1309
1308
  const key = name.toLowerCase();
1310
- if (!key || key === "/")
1309
+ if (!key || key === "/" || seen.has(key))
1311
1310
  return;
1312
- const next = {
1311
+ seen.add(key);
1312
+ out.push({
1313
1313
  command: name,
1314
1314
  label: (label || name).slice(0, 80),
1315
1315
  description: scrub(description),
1316
1316
  source,
1317
- };
1318
- const existing = byKey.get(key);
1319
- if (!existing || SOURCE_RANK[source] < SOURCE_RANK[existing.source]) {
1320
- byKey.set(key, next);
1321
- }
1317
+ });
1322
1318
  };
1323
1319
  try {
1324
1320
  const cmds = session.extensionRunner?.getRegisteredCommands?.() ?? [];
@@ -1329,20 +1325,18 @@ export class ConsolePiRuntime {
1329
1325
  push(String(name), String(cmd.name || name), String(cmd.description || ""), "extension");
1330
1326
  }
1331
1327
  }
1332
- catch (error) {
1333
- // Source isolation: keep other sources; surface compact warning (AC-3).
1334
- sourceWarnings.push(`extension: ${scrub(error instanceof Error ? error.message : String(error))}`);
1328
+ catch {
1329
+ // non-blocking
1335
1330
  }
1336
1331
  try {
1337
- // Enumerate prompts before skills on purpose: priority must still let skill win.
1338
1332
  for (const tpl of session.promptTemplates ?? []) {
1339
1333
  if (!tpl?.name)
1340
1334
  continue;
1341
1335
  push(String(tpl.name), String(tpl.name), String(tpl.description || ""), "prompt");
1342
1336
  }
1343
1337
  }
1344
- catch (error) {
1345
- sourceWarnings.push(`prompt: ${scrub(error instanceof Error ? error.message : String(error))}`);
1338
+ catch {
1339
+ // non-blocking
1346
1340
  }
1347
1341
  try {
1348
1342
  const services = this.serviceScopes.get(sessionId);
@@ -1357,16 +1351,10 @@ export class ConsolePiRuntime {
1357
1351
  push(command, skill.name, String(skill.description || ""), "skill");
1358
1352
  }
1359
1353
  }
1360
- catch (error) {
1361
- sourceWarnings.push(`skill: ${scrub(error instanceof Error ? error.message : String(error))}`);
1354
+ catch {
1355
+ // non-blocking
1362
1356
  }
1363
- const warning = sourceWarnings.length > 0
1364
- ? sourceWarnings.join("; ").slice(0, 160)
1365
- : undefined;
1366
- return {
1367
- commands: [...byKey.values()],
1368
- ...(warning ? { warning } : {}),
1369
- };
1357
+ return out;
1370
1358
  }
1371
1359
  async getRuntimeSnapshot(sessionId, options) {
1372
1360
  try {
@@ -1833,17 +1821,10 @@ export class ConsolePiRuntime {
1833
1821
  : undefined;
1834
1822
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
1835
1823
  const customTools = await this.loadCustomTools();
1836
- // Same allowlist rationale as materializeSession: the SDK filters
1837
- // customTools against `tools`, so the allowed names must cover the full
1838
- // operator + explore surface or every custom tool is silently dropped.
1839
1824
  const { session } = await this.bindings.createSessionFromServices({
1840
1825
  services,
1841
1826
  sessionManager: init.sessionManager,
1842
- tools: [
1843
- ...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
1844
- ...OPERATOR_CHAT_ALLOWED_TOOLS,
1845
- ...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
1846
- ],
1827
+ tools: [...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS],
1847
1828
  excludeTools: [
1848
1829
  "apply_patch",
1849
1830
  "apply-patch",
@@ -2135,15 +2116,6 @@ export class ConsolePiRuntime {
2135
2116
  this.dispose(id);
2136
2117
  }
2137
2118
  }
2138
- function mapAssistantMessageEvent(type, sessionId, message) {
2139
- const content = extractAssistantContentBlocks(message);
2140
- return {
2141
- type,
2142
- sessionId,
2143
- text: extractAssistantText(message),
2144
- ...(content.length > 0 ? { content } : {}),
2145
- };
2146
- }
2147
2119
  function mapSdkEvent(sessionId, event) {
2148
2120
  switch (event.type) {
2149
2121
  case "agent_start":
@@ -2155,13 +2127,17 @@ function mapSdkEvent(sessionId, event) {
2155
2127
  willRetry: Boolean(event.willRetry),
2156
2128
  };
2157
2129
  case "message_update":
2158
- if (!isAssistantSdkMessage(event.message))
2159
- return undefined;
2160
- return mapAssistantMessageEvent("message_update", sessionId, event.message);
2130
+ return {
2131
+ type: "message_update",
2132
+ sessionId,
2133
+ text: extractAssistantText(event.message),
2134
+ };
2161
2135
  case "message_end":
2162
- if (!isAssistantSdkMessage(event.message))
2163
- return undefined;
2164
- return mapAssistantMessageEvent("message_end", sessionId, event.message);
2136
+ return {
2137
+ type: "message_end",
2138
+ sessionId,
2139
+ text: extractAssistantText(event.message),
2140
+ };
2165
2141
  case "turn_end":
2166
2142
  // Usage is emitted from message_end (see prompt subscribe). turn_end
2167
2143
  // carries the same final assistant message and would double-count.
@@ -12,7 +12,6 @@
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";
16
15
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
17
16
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
18
17
  import { cleanChatTitle } from "./session-store.js";
@@ -84,16 +83,12 @@ function turnEventToStorePartial(event) {
84
83
  case "agent_start":
85
84
  return { kind: "agent_start", data: {} };
86
85
  case "message_update":
87
- case "message_end": {
88
- const content = parseAssistantContent(event.content);
89
86
  return {
90
- kind: event.type,
91
- data: {
92
- text: String(event.text ?? ""),
93
- ...(content ? { content } : {}),
94
- },
87
+ kind: "message_update",
88
+ data: { text: String(event.text ?? "") },
95
89
  };
96
- }
90
+ case "message_end":
91
+ return { kind: "message_end", data: { text: String(event.text ?? "") } };
97
92
  case "usage":
98
93
  return { kind: "usage", data: (event.usage ?? {}) };
99
94
  case "tool_call":
@@ -1650,24 +1645,14 @@ async function handleChatFiles(res, deps, sessionId, query) {
1650
1645
  }
1651
1646
  export async function handleChatCommands(res, deps, sessionId) {
1652
1647
  try {
1653
- const listed = typeof deps.runtime.listSlashCommands === "function"
1648
+ const commands = typeof deps.runtime.listSlashCommands === "function"
1654
1649
  ? await deps.runtime.listSlashCommands(sessionId)
1655
- : { commands: [] };
1656
- // Partial single-source failures keep successful commands + compact warning (AC-3).
1657
- const commands = listed.commands ?? [];
1658
- const warning = typeof listed.warning === "string" && listed.warning.trim()
1659
- ? listed.warning.trim().slice(0, 160)
1660
- : undefined;
1650
+ : [];
1661
1651
  res.statusCode = 200;
1662
1652
  res.setHeader("content-type", "application/json; charset=utf-8");
1663
- res.end(JSON.stringify({
1664
- ok: true,
1665
- commands,
1666
- ...(warning ? { warning } : {}),
1667
- }));
1653
+ res.end(JSON.stringify({ ok: true, commands }));
1668
1654
  }
1669
1655
  catch (error) {
1670
- // Whole-list / readiness failures stay fail-soft with empty commands + warning.
1671
1656
  res.statusCode = 200;
1672
1657
  res.setHeader("content-type", "application/json; charset=utf-8");
1673
1658
  res.end(JSON.stringify({
@@ -2814,21 +2799,8 @@ export async function handleChatCompact(req, res, deps, sessionId) {
2814
2799
  return;
2815
2800
  }
2816
2801
  }
2817
- let body = {};
2818
- try {
2819
- body = await readJsonBody(req);
2820
- }
2821
- catch {
2822
- sendJson(res, 400, {
2823
- ok: false,
2824
- error: { code: "INVALID_INPUT", message: "invalid json body" },
2825
- });
2826
- return;
2827
- }
2828
- const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
2829
- const customInstructions = rawInstructions?.trim() || undefined;
2830
2802
  try {
2831
- const snapshot = await deps.runtime.compact(sessionId, customInstructions);
2803
+ const snapshot = await deps.runtime.compact(sessionId);
2832
2804
  const event = deps.events.append(sessionId, deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`, { kind: "compact", data: snapshot });
2833
2805
  sendJson(res, 200, { ok: true, snapshot, eventId: event.eventId });
2834
2806
  }
@@ -368,9 +368,6 @@ 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
- : {}),
374
371
  }));
375
372
  }
376
373
  break;
@@ -41,6 +41,13 @@ 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
+ },
44
51
  ];
45
52
  /** Pi Web commands with real browser-equivalent behavior (UI-11). */
46
53
  export const WEB_SLASH_COMMANDS = [
@@ -96,15 +103,6 @@ export function resolveShortcut(input) {
96
103
  return { command: local.command, action: local.action };
97
104
  return { command: token, action: "unknown" };
98
105
  }
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
- }
108
106
  /**
109
107
  * Merge Operator, Web, and dynamic Session commands with conflict dedupe.
110
108
  * Priority: operator/web > extension > skill > prompt. /reload is single-card.
@@ -1,17 +1,6 @@
1
1
  /**
2
2
  * Turn grouping for Operator Chat inline process UX (pi-web ProcessDetails mind).
3
3
  */
4
- import { getDisplayableAssistantBlocks, joinAssistantText, parseAssistantContent, splitFinalAssistantBlocks, } from "./assistant-content.js";
5
- import { semanticActivityForTool, } from "./semantic-activity.js";
6
- function emptyGroup(key) {
7
- return {
8
- key,
9
- assistants: [],
10
- tools: [],
11
- processItems: [],
12
- live: false,
13
- };
14
- }
15
4
  /**
16
5
  * Restore chat bubbles + tool cards from durable session messages.
17
6
  * Tool rows often appear before the final assistant `message_end` message;
@@ -47,13 +36,11 @@ export function hydrateSessionThread(raw) {
47
36
  if (item.role === "assistant") {
48
37
  if (!turnId)
49
38
  turnId = `restored-${item.id}`;
50
- const content = parseAssistantContent(item.content);
51
39
  messages.push({
52
40
  id: item.id,
53
41
  role: "assistant",
54
42
  text: item.text,
55
43
  createdAt,
56
- ...(content && content.length > 0 ? { content } : {}),
57
44
  });
58
45
  flushPendingToAssistant(item.id);
59
46
  continue;
@@ -77,60 +64,6 @@ export function hydrateSessionThread(raw) {
77
64
  }
78
65
  return { messages, toolCalls };
79
66
  }
80
- function pushToolProcessItem(processItems, tool) {
81
- const activity = semanticActivityForTool(tool);
82
- if (activity)
83
- processItems.push({ kind: "activity", activity });
84
- else
85
- processItems.push({ kind: "tool", tool });
86
- }
87
- function emitProcessBlocks(blocks, group, processItems, usedToolIds) {
88
- for (const block of blocks) {
89
- if (block.type === "thinking") {
90
- processItems.push({ kind: "thinking", thinking: block.thinking });
91
- continue;
92
- }
93
- if (block.type === "text") {
94
- if (block.text.trim())
95
- processItems.push({ kind: "text", text: block.text });
96
- continue;
97
- }
98
- const tool = group.tools.find((entry) => entry.id === block.toolCallId);
99
- if (tool) {
100
- pushToolProcessItem(processItems, tool);
101
- usedToolIds.add(tool.id);
102
- }
103
- }
104
- }
105
- function fillProcessDetails(group) {
106
- const usedToolIds = new Set();
107
- const processItems = [];
108
- const prior = group.assistants.slice(0, -1);
109
- const finalAssistant = group.assistants.at(-1);
110
- for (const assistant of prior) {
111
- emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
112
- }
113
- const blocks = finalAssistant?.content ?? [];
114
- const split = splitFinalAssistantBlocks(blocks, { isStreaming: group.live });
115
- emitProcessBlocks(split.processBlocks, group, processItems, usedToolIds);
116
- for (const tool of group.tools) {
117
- if (!usedToolIds.has(tool.id))
118
- pushToolProcessItem(processItems, tool);
119
- }
120
- group.processItems = processItems;
121
- if (!finalAssistant)
122
- return;
123
- if (blocks.length === 0) {
124
- group.answer = finalAssistant;
125
- return;
126
- }
127
- const answerText = joinAssistantText(split.answerBlocks);
128
- group.answer = {
129
- ...finalAssistant,
130
- text: answerText,
131
- content: split.answerBlocks,
132
- };
133
- }
134
67
  /**
135
68
  * Build user → process → answer groups for the message timeline.
136
69
  */
@@ -139,7 +72,12 @@ export function buildTurnProcessGroups(input) {
139
72
  const groups = [];
140
73
  let current = null;
141
74
  const startGroup = (key) => {
142
- const group = emptyGroup(key);
75
+ const group = {
76
+ key,
77
+ assistants: [],
78
+ tools: [],
79
+ live: false,
80
+ };
143
81
  groups.push(group);
144
82
  return group;
145
83
  };
@@ -154,7 +92,12 @@ export function buildTurnProcessGroups(input) {
154
92
  current.assistants.push(message);
155
93
  }
156
94
  if (groups.length === 0 && input.toolCalls.length > 0) {
157
- groups.push(emptyGroup("orphan-tools"));
95
+ groups.push({
96
+ key: "orphan-tools",
97
+ assistants: [],
98
+ tools: [],
99
+ live: false,
100
+ });
158
101
  }
159
102
  const assigned = new Set();
160
103
  const assistantToGroup = new Map();
@@ -169,6 +112,7 @@ export function buildTurnProcessGroups(input) {
169
112
  if (group) {
170
113
  group.tools.push(tool);
171
114
  assigned.add(tool.id);
115
+ continue;
172
116
  }
173
117
  }
174
118
  }
@@ -190,6 +134,7 @@ export function buildTurnProcessGroups(input) {
190
134
  if (restored) {
191
135
  restored.tools.push(tool);
192
136
  assigned.add(tool.id);
137
+ continue;
193
138
  }
194
139
  }
195
140
  }
@@ -206,47 +151,28 @@ export function buildTurnProcessGroups(input) {
206
151
  windowGroup.tools.push(tool);
207
152
  assigned.add(tool.id);
208
153
  }
209
- else {
210
- const lastGroup = groups.at(-1);
211
- if (lastGroup) {
212
- lastGroup.tools.push(tool);
213
- assigned.add(tool.id);
214
- }
154
+ else if (groups.length > 0) {
155
+ groups[groups.length - 1].tools.push(tool);
156
+ assigned.add(tool.id);
215
157
  }
216
158
  }
217
159
  for (const group of groups) {
218
- group.tools.sort((a, b) => {
219
- if (a.eventSeq !== undefined && b.eventSeq !== undefined) {
220
- return a.eventSeq - b.eventSeq;
221
- }
222
- return a.createdAt - b.createdAt;
223
- });
160
+ group.tools.sort((a, b) => a.createdAt - b.createdAt);
224
161
  group.live =
225
162
  input.streaming &&
226
163
  input.activeAssistantId !== null &&
227
164
  group.assistants.some((a) => a.id === input.activeAssistantId);
228
- fillProcessDetails(group);
229
165
  }
230
166
  return groups;
231
167
  }
232
- export function processGroupSummary(tools, options) {
168
+ export function processGroupSummary(tools) {
233
169
  const count = tools.length;
234
- const thinkingCount = options?.thinkingCount ?? 0;
235
- const textCount = options?.textCount ?? 0;
236
- const activityCount = options?.activityCount ?? 0;
237
- const stepCount = thinkingCount + textCount + activityCount + count;
238
- const errorCount = tools.filter((t) => t.status === "error").length +
239
- (options?.activityErrorCount ?? 0);
240
- const runningCount = tools.filter((t) => t.status === "running").length +
241
- (options?.activityRunningCount ?? 0);
242
- const parts = [`处理详情 · ${stepCount} 步`];
243
- if (count > 0)
244
- parts.push(`${count} 工具`);
245
- if (activityCount > 0)
246
- parts.push(`${activityCount} 进展`);
170
+ const errorCount = tools.filter((t) => t.status === "error").length;
171
+ const runningCount = tools.filter((t) => t.status === "running").length;
172
+ const parts = [`执行过程 · ${count} 工具`];
247
173
  if (errorCount > 0)
248
174
  parts.push(`${errorCount} 出错`);
249
175
  else if (runningCount > 0)
250
176
  parts.push("运行中");
251
- return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
177
+ return { count, errorCount, runningCount, label: parts.join(" · ") };
252
178
  }
@@ -27,7 +27,6 @@ function readWorkspaceParams(query) {
27
27
  ...(read("dagRunId") ? { dagRunId: read("dagRunId") } : {}),
28
28
  ...(read("fromNodeId") ? { fromNodeId: read("fromNodeId") } : {}),
29
29
  ...(read("featureId") ? { featureId: read("featureId") } : {}),
30
- ...(read("intent") ? { intent: read("intent") } : {}),
31
30
  };
32
31
  }
33
32
  /** Parse `#/tasks?taskId=…` hash routes (Observe-style virtual paths). */
@@ -79,7 +78,7 @@ export function resolveDefaultLanding(input) {
79
78
  return (workspaceFromLocation(input.location) ??
80
79
  configuredLanding(input.storage?.getItem(DEFAULT_LANDING_STORAGE_KEY)) ??
81
80
  configuredLanding(input.env) ??
82
- "chat");
81
+ "tasks");
83
82
  }
84
83
  /** Hash path segment for Console virtual routing (no leading `#`). */
85
84
  export function buildConsoleHashPath(workspace, params = {}) {