@namzu/cli 17.0.1 → 18.1.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 (79) hide show
  1. package/README.md +22 -12
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/acp.d.ts.map +1 -1
  6. package/dist/commands/acp.js +1 -0
  7. package/dist/commands/acp.js.map +1 -1
  8. package/dist/commands/run-stream.d.ts.map +1 -1
  9. package/dist/commands/run-stream.js +1 -0
  10. package/dist/commands/run-stream.js.map +1 -1
  11. package/dist/commands/run.d.ts.map +1 -1
  12. package/dist/commands/run.js +1 -0
  13. package/dist/commands/run.js.map +1 -1
  14. package/dist/config/load.d.ts.map +1 -1
  15. package/dist/config/load.js +15 -0
  16. package/dist/config/load.js.map +1 -1
  17. package/dist/config/schema.d.ts +17 -0
  18. package/dist/config/schema.d.ts.map +1 -1
  19. package/dist/config/schema.js.map +1 -1
  20. package/dist/context/doctrine.d.ts +44 -0
  21. package/dist/context/doctrine.d.ts.map +1 -0
  22. package/dist/context/doctrine.js +81 -0
  23. package/dist/context/doctrine.js.map +1 -0
  24. package/dist/context/turn-snapshot.d.ts +59 -0
  25. package/dist/context/turn-snapshot.d.ts.map +1 -0
  26. package/dist/context/turn-snapshot.js +122 -0
  27. package/dist/context/turn-snapshot.js.map +1 -0
  28. package/dist/integrations/subagents/activity.d.ts +8 -0
  29. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  30. package/dist/integrations/subagents/activity.js +38 -2
  31. package/dist/integrations/subagents/activity.js.map +1 -1
  32. package/dist/integrations/subagents/policy.d.ts +6 -0
  33. package/dist/integrations/subagents/policy.d.ts.map +1 -0
  34. package/dist/integrations/subagents/policy.js +6 -0
  35. package/dist/integrations/subagents/policy.js.map +1 -0
  36. package/dist/integrations/subagents/runtime.d.ts +13 -0
  37. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  38. package/dist/integrations/subagents/runtime.js +77 -13
  39. package/dist/integrations/subagents/runtime.js.map +1 -1
  40. package/dist/permissions/mode.d.ts +25 -0
  41. package/dist/permissions/mode.d.ts.map +1 -1
  42. package/dist/permissions/mode.js +11 -1
  43. package/dist/permissions/mode.js.map +1 -1
  44. package/dist/tui/AgentExplorer.d.ts +21 -0
  45. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  46. package/dist/tui/AgentExplorer.js +52 -1
  47. package/dist/tui/AgentExplorer.js.map +1 -1
  48. package/dist/tui/App.d.ts.map +1 -1
  49. package/dist/tui/App.js +298 -26
  50. package/dist/tui/App.js.map +1 -1
  51. package/dist/tui/Composer.d.ts +8 -1
  52. package/dist/tui/Composer.d.ts.map +1 -1
  53. package/dist/tui/Composer.js +23 -3
  54. package/dist/tui/Composer.js.map +1 -1
  55. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  56. package/dist/tui/PermissionOverlay.js +16 -1
  57. package/dist/tui/PermissionOverlay.js.map +1 -1
  58. package/dist/tui/TaskList.d.ts +30 -0
  59. package/dist/tui/TaskList.d.ts.map +1 -0
  60. package/dist/tui/TaskList.js +58 -0
  61. package/dist/tui/TaskList.js.map +1 -0
  62. package/dist/tui/agent.d.ts +49 -4
  63. package/dist/tui/agent.d.ts.map +1 -1
  64. package/dist/tui/agent.js +171 -20
  65. package/dist/tui/agent.js.map +1 -1
  66. package/dist/tui/permission-review.d.ts.map +1 -1
  67. package/dist/tui/permission-review.js +151 -1
  68. package/dist/tui/permission-review.js.map +1 -1
  69. package/dist/tui/slashCommands.d.ts +2 -0
  70. package/dist/tui/slashCommands.d.ts.map +1 -1
  71. package/dist/tui/slashCommands.js +6 -3
  72. package/dist/tui/slashCommands.js.map +1 -1
  73. package/dist/tui/stream-blocks.d.ts +24 -0
  74. package/dist/tui/stream-blocks.d.ts.map +1 -1
  75. package/dist/tui/stream-blocks.js +74 -0
  76. package/dist/tui/stream-blocks.js.map +1 -1
  77. package/dist/tui/types.d.ts +3 -1
  78. package/dist/tui/types.d.ts.map +1 -1
  79. package/package.json +5 -5
package/dist/tui/agent.js CHANGED
@@ -20,21 +20,25 @@
20
20
  * `emptySession()` whose `send()` yields a single error event so the UI
21
21
  * renders an actionable hint rather than crashing.
22
22
  */
23
- import { BOOT_EVENT_NAMES, DefaultPathBuilder, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createToolPresenter, generateRunId, getBuiltinTools, isTrustedReadOnly, query, resumeRun, withProviderFallback, } from '@namzu/sdk';
23
+ import { BOOT_EVENT_NAMES, DefaultPathBuilder, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, WebFetchTool, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildCoordinatorTools, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createToolPresenter, generateRunId, getBuiltinTools, isTrustedReadOnly, query, resumeRun, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
24
24
  import { SubprocessComputerUseHost } from '@namzu/computer-use';
25
25
  import { realpath, stat } from 'node:fs/promises';
26
26
  import { join, parse, resolve } from 'node:path';
27
27
  import { probeCapabilities } from '../context/capabilities.js';
28
+ import { NAMZU_DELEGATION_DOCTRINE, NAMZU_PLAN_MODE_DOCTRINE, NAMZU_WORKING_DOCTRINE, } from '../context/doctrine.js';
28
29
  import { composeEnvironmentPrompt, readEnvironmentFacts } from '../context/environment.js';
29
30
  import { ProjectInstructionTracker } from '../context/project-tracker.js';
30
31
  import { resolveSandbox, sandboxResolvedSeverity, } from '../context/sandbox.js';
32
+ import { composeTurnSnapshot, readTurnSnapshot } from '../context/turn-snapshot.js';
31
33
  import { connectMcpServers, } from '../integrations/mcp/servers.js';
32
34
  import { createCliPluginRuntime } from '../integrations/plugins/runtime.js';
33
35
  import { CredentialRefreshRejectedError, CredentialWithdrawnError, PROVIDER_REGISTRY, chainCapabilityDisagreements, chainPositionName, describeAcceptedMismatch, describeCapabilityRefusal, discoverProviders, ensureFreshAnthropicToken, ensureFreshStoredCodexCredential, ensureRegistered, findDetected, isAnthropicOAuthToken, isRegistered, missingCredentialMessage, primaryProvider, readCodexCredentialFile, readPreferences, readSubscriptionCredential, resolveChainCapabilities, sameOAuthCredential, unresolvedMembers, unsupportedProviderMessage, } from '../integrations/providers/index.js';
34
36
  import { ensurePrivateStateDirectory } from '../integrations/state/private-directory.js';
37
+ import { CLI_INTERACTIVE_RUN_TIMEOUT_MS } from '../integrations/subagents/policy.js';
35
38
  import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
36
39
  import { cliLogger } from '../logging.js';
37
40
  import { composeMemoryPrompt, readMemory } from '../memory/store.js';
41
+ import { ACCEPT_EDITS_TOOLS, PLAN_MODE_REFUSAL } from '../permissions/mode.js';
38
42
  import { projectRunConversation } from './conversation-history.js';
39
43
  /**
40
44
  * Let one caller stop waiting without cutting a shared queue in the middle.
@@ -788,6 +792,15 @@ export async function createAgentSession(prefs, detected, options = {}) {
788
792
  // This session passes a `taskStore` to query() below, which registers the
789
793
  // task tools deferred — so `search_tools` has something to find here.
790
794
  registry.register([SearchToolsTool]);
795
+ // Web reach, opted into in the config file and nowhere else. The parent's
796
+ // registry only: a child's config carries no provider, and a tool whose
797
+ // provider is missing is a tool that reports itself unwired — truthful,
798
+ // and noise. The guarded provider refuses private and loopback addresses
799
+ // and bounds redirects and body; every fetch is reviewed like a shell
800
+ // command (see `isPromptExempt`).
801
+ const webCapability = options.web?.fetch ? { fetch: new GuardedFetchProvider() } : undefined;
802
+ if (webCapability)
803
+ registry.register(WebFetchTool);
791
804
  // Native sub-agents: register the canonical `Agent` tool so the model can
792
805
  // delegate a self-contained task to a fresh sub-agent (own context window).
793
806
  // Best-effort — if the runtime can't stand up, the chat still works.
@@ -863,14 +876,62 @@ export async function createAgentSession(prefs, detected, options = {}) {
863
876
  // and got none had nothing on stderr to say why.
864
877
  cliLogger().warn('sub-agent runtime unavailable this session', exceptionAttributes(err));
865
878
  }
866
- // Task store → query registers task_create / task_update / task_list as
867
- // DEFERRED tools and emits task_created/task_updated, so the agent can track
868
- // a plan for the current request. Tasks are run-scoped.
879
+ // `ask_user_question`, where somebody can answer. The SDK tool parks the
880
+ // run through the handler it was BUILT with, so that handler reads the
881
+ // turn's answerer through a holder the prelude fills: the tool is per
882
+ // session, the person answering is per turn. Needs the delegation
883
+ // gateway the tool builder requires; a session without one has no
884
+ // question tool either, and says nothing — it also has no `Agent`.
885
+ let currentOnQuestion;
886
+ if (options.askUser && subagentGateway) {
887
+ const parkQuestion = async (request) => {
888
+ if (request.type !== 'user_question')
889
+ return { action: 'continue' };
890
+ const ask = currentOnQuestion;
891
+ if (!ask)
892
+ return { action: 'continue' };
893
+ const answer = await ask(request.question);
894
+ switch (answer.kind) {
895
+ case 'answer':
896
+ return {
897
+ action: 'answer_question',
898
+ selectedOptionIds: [...answer.selectedOptionIds],
899
+ ...(answer.freeText !== undefined ? { freeText: answer.freeText } : {}),
900
+ questionId: request.question.questionId,
901
+ };
902
+ case 'abort':
903
+ return { action: 'abort', reason: 'The user declined to answer.' };
904
+ default:
905
+ return { action: 'continue' };
906
+ }
907
+ };
908
+ const askTool = buildCoordinatorTools({
909
+ gateway: subagentGateway,
910
+ workingDirectory: cwd,
911
+ allowedAgentIds: [],
912
+ allowDelegation: false,
913
+ resumeHandler: parkQuestion,
914
+ // The builder stamps this on the park request. The handler above
915
+ // routes by the question, not by the run, and no durable park
916
+ // recorder is supplied, so a session-scoped id is what is true: the
917
+ // tool is built once per session and the turn is not known yet.
918
+ runId: asRunId('run_namzu-interactive-question'),
919
+ }).find((tool) => tool.name === 'ask_user_question');
920
+ if (askTool)
921
+ registry.register(askTool);
922
+ }
923
+ // Task store → query registers task_create / task_update / task_list and
924
+ // emits task_created/task_updated, so the agent can track a plan for the
925
+ // current request. Tasks are run-scoped. The kernel's default availability
926
+ // for them is `deferred`; this session overrides that to `active` at the
927
+ // query call, because the doctrine tells the model to plan with them and a
928
+ // tool it must search for first is a tool it skips.
869
929
  //
870
- // "Deferred" is why this session mounts `search_tools` above: these three are
871
- // the roster it searches. They are registered inside query(), after this
872
- // function returns, which is why the connect line reports no count of them —
873
- // counting here would mean restating query's registration order in the CLI.
930
+ // `search_tools` stays mounted above even so: a tool server or plugin can
931
+ // still register a deferred roster, and that is what the search is for. The
932
+ // task tools are registered inside query(), after this function returns,
933
+ // which is why the connect line reports no count of them — counting here
934
+ // would mean restating query's registration order in the CLI.
874
935
  //
875
936
  // It is also why `toolNames` below reads the registry rather than a list
876
937
  // captured on this line. The count at connect time is unchanged; what
@@ -1061,8 +1122,44 @@ export async function createAgentSession(prefs, detected, options = {}) {
1061
1122
  ? await currentPluginSkills(pluginRuntime.skills)
1062
1123
  : undefined;
1063
1124
  const memoryPrompt = composeMemoryPrompt(readMemory());
1064
- const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
1065
- const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt, opts?.extraSystem]
1125
+ currentOnQuestion = opts?.onQuestion;
1126
+ const [environmentFacts, turnSnapshot] = await Promise.all([
1127
+ readEnvironmentFacts(cwd),
1128
+ readTurnSnapshot(cwd),
1129
+ ]);
1130
+ const environmentPrompt = composeEnvironmentPrompt(environmentFacts);
1131
+ // The repository as it stood when THIS turn began, through the
1132
+ // SDK's `turn` placement — the ephemeral trailing message that is
1133
+ // never cached and never enters history. FIRST iteration only:
1134
+ // later iterations work from state the model itself changed, and
1135
+ // `git status` is the honest source for that. A registry per
1136
+ // turn, closed over this turn's snapshot, rather than one
1137
+ // session-scoped holder every send overwrites: two overlapping
1138
+ // sends would otherwise both render whichever ran second.
1139
+ const turnSnapshotPrompt = turnSnapshot ? composeTurnSnapshot(turnSnapshot) : null;
1140
+ const promptContributions = new PromptContributionRegistry();
1141
+ promptContributions.register({
1142
+ id: 'namzu.turn-snapshot',
1143
+ placement: 'turn',
1144
+ render: ({ iteration }) => (iteration === 1 ? turnSnapshotPrompt : null),
1145
+ });
1146
+ // The citation rules that come with the web tools, only when the
1147
+ // tools are there: guidance about a capability the turn does not
1148
+ // have reads as a capability it should be looking for.
1149
+ if (webCapability)
1150
+ promptContributions.register(webGuidanceContribution);
1151
+ const systemPrompt = [
1152
+ NAMZU_IDENTITY,
1153
+ NAMZU_WORKING_DOCTRINE,
1154
+ NAMZU_DELEGATION_DOCTRINE,
1155
+ // Present only while the turn runs under `plan`. A mode change
1156
+ // is rare, so the cached prefix it re-keys is a price paid once
1157
+ // per switch rather than once per turn.
1158
+ opts?.permissionMode === 'plan' ? NAMZU_PLAN_MODE_DOCTRINE : undefined,
1159
+ environmentPrompt,
1160
+ memoryPrompt,
1161
+ opts?.extraSystem,
1162
+ ]
1066
1163
  .filter((s) => Boolean(s))
1067
1164
  .join('\n\n') || undefined;
1068
1165
  let capturedAuthority;
@@ -1122,6 +1219,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
1122
1219
  opts: turnOpts,
1123
1220
  resumeHandler,
1124
1221
  taskGateway: subagentGateway,
1222
+ promptContributions,
1223
+ ...(webCapability ? { web: webCapability } : {}),
1224
+ // Active, not deferred: the doctrine tells the model to open a
1225
+ // task list for multi-step work, and a tool it has to search
1226
+ // for first is a tool it will skip.
1227
+ runtimeToolOverrides: {
1228
+ task_create: 'active',
1229
+ task_update: 'active',
1230
+ task_list: 'active',
1231
+ },
1125
1232
  onRunEvent: options.onRunEvent,
1126
1233
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
1127
1234
  ...(options.sandbox?.teardownTimeoutMs !== undefined
@@ -1156,7 +1263,13 @@ export async function createAgentSession(prefs, detected, options = {}) {
1156
1263
  : undefined;
1157
1264
  const memoryPrompt = composeMemoryPrompt(readMemory());
1158
1265
  const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
1159
- const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
1266
+ const systemPrompt = [
1267
+ NAMZU_IDENTITY,
1268
+ NAMZU_WORKING_DOCTRINE,
1269
+ NAMZU_DELEGATION_DOCTRINE,
1270
+ environmentPrompt,
1271
+ memoryPrompt,
1272
+ ]
1160
1273
  .filter((s) => Boolean(s))
1161
1274
  .join('\n\n') || undefined;
1162
1275
  const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
@@ -1173,6 +1286,15 @@ export async function createAgentSession(prefs, detected, options = {}) {
1173
1286
  skillRegistry: pluginRuntime?.skills,
1174
1287
  skills: pluginSkills,
1175
1288
  taskStore,
1289
+ // The same availability the original run registered under.
1290
+ // A resumed run re-registers the task tools; leaving them at
1291
+ // the kernel's `deferred` default would hand the model a plan
1292
+ // it started with active tools and can no longer update.
1293
+ runtimeToolOverrides: {
1294
+ task_create: 'active',
1295
+ task_update: 'active',
1296
+ task_list: 'active',
1297
+ },
1176
1298
  ...(subagentGateway ? { taskGateway: subagentGateway } : {}),
1177
1299
  authorizationGate: gateFor(options.rules),
1178
1300
  compactionConfig: COMPACTION_CONFIG,
@@ -1190,7 +1312,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
1190
1312
  runConfig: {
1191
1313
  model,
1192
1314
  ...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
1193
- timeoutMs: 600_000,
1315
+ timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
1194
1316
  tokenBudget: 1_000_000,
1195
1317
  maxIterations: 50,
1196
1318
  maxResponseTokens: 8192,
@@ -1587,7 +1709,7 @@ const COMPACTION_CONFIG = {
1587
1709
  maxCharsPerRequirement: 300,
1588
1710
  maxCharsPerTask: 400,
1589
1711
  };
1590
- async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, sandboxWorkspace, rules, reviewAnswer, maxAnswerReviews, promoteMemory, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
1712
+ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, sandboxWorkspace, rules, reviewAnswer, maxAnswerReviews, promoteMemory, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, promptContributions, runtimeToolOverrides, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
1591
1713
  const signal = opts?.signal;
1592
1714
  // One presenter for the whole stream, built from the registry this scope
1593
1715
  // already holds. Its absence HERE is what forced presentation to be name
@@ -1629,7 +1751,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
1629
1751
  model,
1630
1752
  ...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
1631
1753
  ...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
1632
- timeoutMs: 600_000,
1754
+ timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
1633
1755
  tokenBudget: 1_000_000,
1634
1756
  maxIterations: 50,
1635
1757
  maxResponseTokens: 8192,
@@ -1656,6 +1778,9 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
1656
1778
  // tools `query()` registers deferred below and any tool server that
1657
1779
  // connected after this session was built.
1658
1780
  resumeHandler,
1781
+ ...(promptContributions ? { promptContributions } : {}),
1782
+ ...(runtimeToolOverrides ? { runtimeToolOverrides } : {}),
1783
+ ...(web ? { web } : {}),
1659
1784
  signal,
1660
1785
  ...scope,
1661
1786
  });
@@ -1730,6 +1855,21 @@ exempt = () => false) {
1730
1855
  if (!batchNeedsPrompt(request.toolCalls, exempt)) {
1731
1856
  return { action: 'approve_tools' };
1732
1857
  }
1858
+ // A batch of nothing but non-destructive file edits is the case this
1859
+ // mode exists for. One bash call in the same batch and the whole batch
1860
+ // asks — the operator reviews the batch as a unit, and a prompt that
1861
+ // showed only the shell command while the edits went through beside it
1862
+ // would be approving something it did not show.
1863
+ if (mode === 'accept-edits' &&
1864
+ request.toolCalls.every((tc) => !tc.isDestructive && (ACCEPT_EDITS_TOOLS.has(tc.name) || exempt(tc.name, tc.input)))) {
1865
+ return { action: 'approve_tools' };
1866
+ }
1867
+ // Reads were already approved above (they are exempt). Anything that
1868
+ // reached here would change something, and plan mode's answer to that
1869
+ // is the same every time: not now, tell the user what you would do.
1870
+ if (mode === 'plan') {
1871
+ return { action: 'reject_tools', feedback: PLAN_MODE_REFUSAL };
1872
+ }
1733
1873
  if (mode === 'strict') {
1734
1874
  return {
1735
1875
  action: 'reject_tools',
@@ -1816,6 +1956,11 @@ export function isPromptExempt(registry, name, input) {
1816
1956
  if (PROMPT_EXEMPT_WRITES.has(name.toLowerCase()))
1817
1957
  return true;
1818
1958
  const tool = registry.get(name) ?? registry.get(name.toLowerCase());
1959
+ // A fetch changes nothing here and declares itself read-only, and it is
1960
+ // still a request leaving the machine to an address the model chose. The
1961
+ // operator sees the URL before it goes, the way they see a shell command.
1962
+ if (tool?.category === 'network')
1963
+ return false;
1819
1964
  // A connected server's own claim about its own tool cannot skip the
1820
1965
  // prompt. Same predicate the kernel gate and plan mode use -- three
1821
1966
  // doors, one rule, because fixing two would close the issue and leave
@@ -1854,6 +1999,7 @@ export function toAgentEvent(event, presenter) {
1854
1999
  case 'tool_executing':
1855
2000
  return {
1856
2001
  kind: 'tool-start',
2002
+ runId: event.runId,
1857
2003
  toolUseId: event.toolUseId,
1858
2004
  toolName: event.toolName,
1859
2005
  ...(() => {
@@ -1870,6 +2016,7 @@ export function toAgentEvent(event, presenter) {
1870
2016
  case 'tool_progress':
1871
2017
  return {
1872
2018
  kind: 'tool-progress',
2019
+ runId: event.runId,
1873
2020
  toolUseId: event.toolUseId,
1874
2021
  toolName: event.toolName,
1875
2022
  message: event.message,
@@ -1885,6 +2032,7 @@ export function toAgentEvent(event, presenter) {
1885
2032
  const withoutRepeatedSummary = view.kind === 'terminal' && detail?.[0] === summary ? detail.slice(1) : detail;
1886
2033
  return {
1887
2034
  kind: 'tool-end',
2035
+ runId: event.runId,
1888
2036
  toolUseId: event.toolUseId,
1889
2037
  toolName: event.toolName,
1890
2038
  isError: event.isError,
@@ -1959,13 +2107,16 @@ export function toAgentEvent(event, presenter) {
1959
2107
  };
1960
2108
  }
1961
2109
  case 'task_created':
1962
- return { kind: 'task', subject: event.subject, status: event.status };
1963
2110
  case 'task_updated':
1964
- // Only surface completions — skip pending/in-progress churn so the
1965
- // transcript shows "todo added" then "todo done", not every flip.
1966
- return event.status === 'completed'
1967
- ? { kind: 'task', subject: event.subject, status: event.status }
1968
- : null;
2111
+ // Every change, not only completions: the live task list needs the
2112
+ // in-progress flips to show which step is current. The transcript
2113
+ // decides for itself which of these it records.
2114
+ return {
2115
+ kind: 'task',
2116
+ taskId: String(event.taskId),
2117
+ subject: event.subject,
2118
+ status: event.status,
2119
+ };
1969
2120
  case 'run_paused':
1970
2121
  // A pause is not an error and not an invisible end. The checkpoint and
1971
2122
  // classification are the recovery surface; dropping this event made a