@namzu/cli 24.0.0 → 25.0.1

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 (192) hide show
  1. package/CHANGELOG.md +841 -0
  2. package/README.md +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +1 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/drain.d.ts +0 -23
  7. package/dist/commands/drain.d.ts.map +1 -1
  8. package/dist/commands/drain.js +23 -9
  9. package/dist/commands/drain.js.map +1 -1
  10. package/dist/commands/resident-flags.d.ts +6 -1
  11. package/dist/commands/resident-flags.d.ts.map +1 -1
  12. package/dist/commands/resident-flags.js +39 -1
  13. package/dist/commands/resident-flags.js.map +1 -1
  14. package/dist/commands/resident-learning.d.ts +6 -0
  15. package/dist/commands/resident-learning.d.ts.map +1 -0
  16. package/dist/commands/resident-learning.js +293 -0
  17. package/dist/commands/resident-learning.js.map +1 -0
  18. package/dist/commands/resident.d.ts.map +1 -1
  19. package/dist/commands/resident.js +42 -4
  20. package/dist/commands/resident.js.map +1 -1
  21. package/dist/commands/run-flags.d.ts +2 -2
  22. package/dist/commands/run-flags.d.ts.map +1 -1
  23. package/dist/commands/run-flags.js +6 -6
  24. package/dist/commands/run-flags.js.map +1 -1
  25. package/dist/commands/run-stream.d.ts.map +1 -1
  26. package/dist/commands/run-stream.js +9 -10
  27. package/dist/commands/run-stream.js.map +1 -1
  28. package/dist/commands/run.d.ts.map +1 -1
  29. package/dist/commands/run.js +3 -2
  30. package/dist/commands/run.js.map +1 -1
  31. package/dist/config/load.d.ts.map +1 -1
  32. package/dist/config/load.js +27 -4
  33. package/dist/config/load.js.map +1 -1
  34. package/dist/config/run-limits.d.ts +11 -0
  35. package/dist/config/run-limits.d.ts.map +1 -0
  36. package/dist/config/run-limits.js +64 -0
  37. package/dist/config/run-limits.js.map +1 -0
  38. package/dist/config/schema.d.ts +12 -4
  39. package/dist/config/schema.d.ts.map +1 -1
  40. package/dist/config/schema.js.map +1 -1
  41. package/dist/context/runtime-message.d.ts.map +1 -1
  42. package/dist/context/runtime-message.js +6 -0
  43. package/dist/context/runtime-message.js.map +1 -1
  44. package/dist/integrations/plugins/runtime.d.ts +19 -1
  45. package/dist/integrations/plugins/runtime.d.ts.map +1 -1
  46. package/dist/integrations/plugins/runtime.js +89 -1
  47. package/dist/integrations/plugins/runtime.js.map +1 -1
  48. package/dist/integrations/plugins/settings.d.ts +15 -0
  49. package/dist/integrations/plugins/settings.d.ts.map +1 -0
  50. package/dist/integrations/plugins/settings.js +106 -0
  51. package/dist/integrations/plugins/settings.js.map +1 -0
  52. package/dist/integrations/resident/inspection.d.ts +10 -0
  53. package/dist/integrations/resident/inspection.d.ts.map +1 -0
  54. package/dist/integrations/resident/inspection.js +263 -0
  55. package/dist/integrations/resident/inspection.js.map +1 -0
  56. package/dist/integrations/resident/learning-sources.d.ts +4 -0
  57. package/dist/integrations/resident/learning-sources.d.ts.map +1 -0
  58. package/dist/integrations/resident/learning-sources.js +79 -0
  59. package/dist/integrations/resident/learning-sources.js.map +1 -0
  60. package/dist/integrations/resident/learning-storage.d.ts +5 -0
  61. package/dist/integrations/resident/learning-storage.d.ts.map +1 -0
  62. package/dist/integrations/resident/learning-storage.js +51 -0
  63. package/dist/integrations/resident/learning-storage.js.map +1 -0
  64. package/dist/integrations/resident/runner-launch.d.ts +5 -0
  65. package/dist/integrations/resident/runner-launch.d.ts.map +1 -1
  66. package/dist/integrations/resident/runner-launch.js +2 -0
  67. package/dist/integrations/resident/runner-launch.js.map +1 -1
  68. package/dist/integrations/resident/runner-store.d.ts.map +1 -1
  69. package/dist/integrations/resident/runner-store.js +2 -4
  70. package/dist/integrations/resident/runner-store.js.map +1 -1
  71. package/dist/integrations/resident/runner-worker.js +3 -0
  72. package/dist/integrations/resident/runner-worker.js.map +1 -1
  73. package/dist/integrations/resident/session-step.d.ts +6 -1
  74. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  75. package/dist/integrations/resident/session-step.js +110 -14
  76. package/dist/integrations/resident/session-step.js.map +1 -1
  77. package/dist/integrations/resident/storage.d.ts.map +1 -1
  78. package/dist/integrations/resident/storage.js +7 -6
  79. package/dist/integrations/resident/storage.js.map +1 -1
  80. package/dist/integrations/resident/tool-evidence.d.ts +6 -0
  81. package/dist/integrations/resident/tool-evidence.d.ts.map +1 -0
  82. package/dist/integrations/resident/tool-evidence.js +117 -0
  83. package/dist/integrations/resident/tool-evidence.js.map +1 -0
  84. package/dist/integrations/resident/verification.d.ts +11 -0
  85. package/dist/integrations/resident/verification.d.ts.map +1 -0
  86. package/dist/integrations/resident/verification.js +109 -0
  87. package/dist/integrations/resident/verification.js.map +1 -0
  88. package/dist/integrations/sessions/compaction-evidence.d.ts +5 -0
  89. package/dist/integrations/sessions/compaction-evidence.d.ts.map +1 -0
  90. package/dist/integrations/sessions/compaction-evidence.js +59 -0
  91. package/dist/integrations/sessions/compaction-evidence.js.map +1 -0
  92. package/dist/integrations/sessions/context-inventory.js +1 -1
  93. package/dist/integrations/sessions/context-inventory.js.map +1 -1
  94. package/dist/integrations/sessions/conversation-search.d.ts +56 -8
  95. package/dist/integrations/sessions/conversation-search.d.ts.map +1 -1
  96. package/dist/integrations/sessions/conversation-search.js +600 -63
  97. package/dist/integrations/sessions/conversation-search.js.map +1 -1
  98. package/dist/integrations/sessions/database.d.ts +5 -0
  99. package/dist/integrations/sessions/database.d.ts.map +1 -0
  100. package/dist/integrations/sessions/database.js +34 -0
  101. package/dist/integrations/sessions/database.js.map +1 -0
  102. package/dist/integrations/sessions/evidence-page-validation.d.ts +8 -0
  103. package/dist/integrations/sessions/evidence-page-validation.d.ts.map +1 -0
  104. package/dist/integrations/sessions/evidence-page-validation.js +74 -0
  105. package/dist/integrations/sessions/evidence-page-validation.js.map +1 -0
  106. package/dist/integrations/sessions/evidence-recall.d.ts +5 -0
  107. package/dist/integrations/sessions/evidence-recall.d.ts.map +1 -0
  108. package/dist/integrations/sessions/evidence-recall.js +173 -0
  109. package/dist/integrations/sessions/evidence-recall.js.map +1 -0
  110. package/dist/integrations/sessions/paths.d.ts +7 -0
  111. package/dist/integrations/sessions/paths.d.ts.map +1 -0
  112. package/dist/integrations/sessions/paths.js +14 -0
  113. package/dist/integrations/sessions/paths.js.map +1 -0
  114. package/dist/integrations/sessions/run-discovery.d.ts +24 -0
  115. package/dist/integrations/sessions/run-discovery.d.ts.map +1 -0
  116. package/dist/integrations/sessions/run-discovery.js +142 -0
  117. package/dist/integrations/sessions/run-discovery.js.map +1 -0
  118. package/dist/integrations/sessions/store.d.ts +8 -18
  119. package/dist/integrations/sessions/store.d.ts.map +1 -1
  120. package/dist/integrations/sessions/store.js +14 -39
  121. package/dist/integrations/sessions/store.js.map +1 -1
  122. package/dist/integrations/sessions/transcript-export.d.ts +0 -1
  123. package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
  124. package/dist/integrations/sessions/transcript-export.js +3 -2
  125. package/dist/integrations/sessions/transcript-export.js.map +1 -1
  126. package/dist/integrations/sessions/turn-evidence.d.ts +0 -9
  127. package/dist/integrations/sessions/turn-evidence.d.ts.map +1 -1
  128. package/dist/integrations/sessions/turn-evidence.js +5 -2
  129. package/dist/integrations/sessions/turn-evidence.js.map +1 -1
  130. package/dist/integrations/state/report.d.ts.map +1 -1
  131. package/dist/integrations/state/report.js +78 -21
  132. package/dist/integrations/state/report.js.map +1 -1
  133. package/dist/integrations/subagents/parent.d.ts +3 -2
  134. package/dist/integrations/subagents/parent.d.ts.map +1 -1
  135. package/dist/integrations/subagents/parent.js +11 -6
  136. package/dist/integrations/subagents/parent.js.map +1 -1
  137. package/dist/integrations/subagents/policy.d.ts +2 -5
  138. package/dist/integrations/subagents/policy.d.ts.map +1 -1
  139. package/dist/integrations/subagents/policy.js +2 -5
  140. package/dist/integrations/subagents/policy.js.map +1 -1
  141. package/dist/integrations/subagents/runtime.d.ts +7 -0
  142. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  143. package/dist/integrations/subagents/runtime.js +9 -7
  144. package/dist/integrations/subagents/runtime.js.map +1 -1
  145. package/dist/permissions/rules.d.ts +2 -8
  146. package/dist/permissions/rules.d.ts.map +1 -1
  147. package/dist/permissions/rules.js +19 -10
  148. package/dist/permissions/rules.js.map +1 -1
  149. package/dist/tui/App.d.ts +0 -12
  150. package/dist/tui/App.d.ts.map +1 -1
  151. package/dist/tui/App.js +245 -19
  152. package/dist/tui/App.js.map +1 -1
  153. package/dist/tui/ChoicePicker.d.ts +2 -1
  154. package/dist/tui/ChoicePicker.d.ts.map +1 -1
  155. package/dist/tui/ChoicePicker.js +10 -8
  156. package/dist/tui/ChoicePicker.js.map +1 -1
  157. package/dist/tui/ToolOutputViewer.js +1 -1
  158. package/dist/tui/ToolOutputViewer.js.map +1 -1
  159. package/dist/tui/agent.d.ts +20 -3
  160. package/dist/tui/agent.d.ts.map +1 -1
  161. package/dist/tui/agent.js +211 -39
  162. package/dist/tui/agent.js.map +1 -1
  163. package/dist/tui/conversation-evidence-view.d.ts +6 -0
  164. package/dist/tui/conversation-evidence-view.d.ts.map +1 -0
  165. package/dist/tui/conversation-evidence-view.js +96 -0
  166. package/dist/tui/conversation-evidence-view.js.map +1 -0
  167. package/dist/tui/conversation-history.d.ts +3 -1
  168. package/dist/tui/conversation-history.d.ts.map +1 -1
  169. package/dist/tui/conversation-history.js +16 -1
  170. package/dist/tui/conversation-history.js.map +1 -1
  171. package/dist/tui/markdownParser.d.ts.map +1 -1
  172. package/dist/tui/markdownParser.js +2 -1
  173. package/dist/tui/markdownParser.js.map +1 -1
  174. package/dist/tui/plugin-view.d.ts +9 -0
  175. package/dist/tui/plugin-view.d.ts.map +1 -0
  176. package/dist/tui/plugin-view.js +59 -0
  177. package/dist/tui/plugin-view.js.map +1 -0
  178. package/dist/tui/run-interruption.d.ts +2 -2
  179. package/dist/tui/run-interruption.d.ts.map +1 -1
  180. package/dist/tui/run-interruption.js +5 -1
  181. package/dist/tui/run-interruption.js.map +1 -1
  182. package/dist/tui/run-limits-settings.d.ts +39 -0
  183. package/dist/tui/run-limits-settings.d.ts.map +1 -0
  184. package/dist/tui/run-limits-settings.js +88 -0
  185. package/dist/tui/run-limits-settings.js.map +1 -0
  186. package/dist/tui/slashCommands.d.ts +6 -1
  187. package/dist/tui/slashCommands.d.ts.map +1 -1
  188. package/dist/tui/slashCommands.js +40 -9
  189. package/dist/tui/slashCommands.js.map +1 -1
  190. package/dist/tui/types.d.ts +4 -2
  191. package/dist/tui/types.d.ts.map +1 -1
  192. package/package.json +7 -7
package/dist/tui/App.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { resolveRunGuards } from '../config/run-limits.js';
3
+ import { RUN_LIMIT_FIELDS, runLimitCommands } from './run-limits-settings.js';
4
+ import { CliPathBuilder } from '../integrations/sessions/paths.js';
2
5
  /**
3
6
  * TUI root. Composes the banner, transcript, composer, status bar, and
4
7
  * the first-run provider picker overlay.
@@ -15,7 +18,7 @@ import { discoverProviders } from '../integrations/providers/discover.js';
15
18
  import { ProviderSetup } from './ProviderSetup.js';
16
19
  import { ToolOutputViewer } from './ToolOutputViewer.js';
17
20
  import { join, relative } from 'node:path';
18
- import { DEFAULT_MAX_GOAL_ROUNDS, DefaultPathBuilder, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
21
+ import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
19
22
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
20
23
  import { useCallback, useEffect, useRef, useState } from 'react';
21
24
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
@@ -36,6 +39,7 @@ import { discoverUserCommands } from '../user-commands/store.js';
36
39
  import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
37
40
  import { BrandHeader } from './BrandHeader.js';
38
41
  import { ChoicePicker } from './ChoicePicker.js';
42
+ import { emptyPluginReport, pluginDetails, pluginOption, pluginStartupState } from './plugin-view.js';
39
43
  import { Composer, suggestionWindowSize, } from './Composer.js';
40
44
  import { ComposerFrame } from './ComposerFrame.js';
41
45
  import { CopyPicker } from './CopyPicker.js';
@@ -48,13 +52,14 @@ import { StatusBar } from './StatusBar.js';
48
52
  import { TaskList } from './TaskList.js';
49
53
  import { TextPrompt } from './TextPrompt.js';
50
54
  import { modelCatalogueView } from './model-catalogue-view.js';
55
+ import { conversationEvidenceView } from './conversation-evidence-view.js';
51
56
  import { Transcript, willCollapse } from './Transcript.js';
52
57
  import { TrustPrompt } from './TrustPrompt.js';
53
58
  import { createAgentSession, describeProviderModels, probeAgentSession, } from './agent.js';
54
59
  import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
55
60
  import { keepRecentRows } from './compact-transcript.js';
56
61
  import { approvalIsDeliberate } from './consent-timing.js';
57
- import { planTurnPublication } from './conversation-history.js';
62
+ import { assistantTranscriptTexts, planTurnPublication } from './conversation-history.js';
58
63
  import { copyTargetsForResponse } from './copy-targets.js';
59
64
  import { editablePrompts } from './edit-prompts.js';
60
65
  import { editDraftInExternalEditor } from './external-editor.js';
@@ -171,13 +176,11 @@ export function projectConversation(messages, nextId, readableUserTexts = []) {
171
176
  ];
172
177
  }
173
178
  if (message.role === 'assistant') {
174
- return [
175
- {
176
- id: nextId(),
177
- role: 'assistant',
178
- content: typeof message.content === 'string' ? message.content : '',
179
- },
180
- ];
179
+ return assistantTranscriptTexts(message).map((content) => ({
180
+ id: nextId(),
181
+ role: 'assistant',
182
+ content,
183
+ }));
181
184
  }
182
185
  if (message.role === 'system' && isCompactionMessage(message.content)) {
183
186
  return [
@@ -379,6 +382,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
379
382
  const permissionModeSourceRef = useRef(ctx.skipPermissions === true ? 'launch-bypass' : 'default');
380
383
  const [reasoningEffort, setReasoningEffortState] = useState();
381
384
  const reasoningEffortRef = useRef(undefined);
385
+ const runLimitsOverrideRef = useRef({});
382
386
  const setReasoningEffort = useCallback((next) => {
383
387
  reasoningEffortRef.current = next;
384
388
  setReasoningEffortState(next);
@@ -401,10 +405,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
401
405
  summaries: 0,
402
406
  reclaimedTokens: 0,
403
407
  });
404
- // Jobs that ended while no turn was running. The kernel's notice needs a
405
- // tool result to ride on, so between turns the exit is held here and
406
- // handed to the next send as part of the system text; the transcript
407
- // row is written at once.
408
+ // Jobs that ended while no turn was running.
409
+ //
410
+ // Only those: an exit that lands mid-turn belongs to the kernel, which
411
+ // rides it out on the next tool result or, for a job the model awaited,
412
+ // delivers it as the message that releases its own suspend. Queueing one
413
+ // here as well would announce the same exit twice, so the `abortRef`
414
+ // check below is the boundary between the two owners rather than an
415
+ // optimisation. Between turns the kernel is not listening, so the exit is
416
+ // held here and handed to the next send as part of the system text; the
417
+ // transcript row is written at once.
408
418
  const idleJobNoticesRef = useRef([]);
409
419
  const drainIdleJobNotices = () => {
410
420
  if (idleJobNoticesRef.current.length === 0)
@@ -1106,7 +1116,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1106
1116
  // so the feedback store correctly refused every TUI rating as unverifiable.
1107
1117
  // Bind both trees to the exact conversation captured with the streamed
1108
1118
  // message; `/resume` may change the active scope before a delayed click.
1109
- const sessionDir = new DefaultPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
1119
+ const sessionDir = new CliPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
1110
1120
  const store = new DiskMessageFeedbackStore({
1111
1121
  rootDir: join(sessionDir, 'feedback'),
1112
1122
  runsDir: join(sessionDir, 'runs'),
@@ -1321,6 +1331,70 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1321
1331
  return;
1322
1332
  if (picker.options[index]?.disabledReason)
1323
1333
  return;
1334
+ if (picker.kind === 'plugin' || picker.kind === 'plugin-action') {
1335
+ if (picker.busy || picker.owner !== session)
1336
+ return;
1337
+ if (picker.kind === 'plugin') {
1338
+ const child = pluginActionPicker(picker.owner, value);
1339
+ setSelectedChoice(0);
1340
+ setChoicePicker(child ? { ...child, back: picker } : null);
1341
+ return;
1342
+ }
1343
+ const plugin = picker.owner.plugins?.list().find((item) => item.name === picker.pluginName);
1344
+ if (!plugin || !picker.owner.plugins)
1345
+ return;
1346
+ if (value === 'details') {
1347
+ setChoicePicker(null);
1348
+ pushMessage('system', pluginDetails(plugin));
1349
+ return;
1350
+ }
1351
+ if (state !== 'idle' || permission) {
1352
+ setChoicePicker({
1353
+ ...picker,
1354
+ notice: 'Wait for the active turn and permission prompts to finish.',
1355
+ });
1356
+ return;
1357
+ }
1358
+ const enabled = value === 'enable';
1359
+ const remembering = value === 'remember';
1360
+ const pending = {
1361
+ ...picker,
1362
+ busy: true,
1363
+ notice: remembering
1364
+ ? 'Saving plugin setting…'
1365
+ : enabled
1366
+ ? 'Enabling plugin…'
1367
+ : 'Disabling plugin…',
1368
+ };
1369
+ setChoicePicker(pending);
1370
+ const generation = conversationGenRef.current;
1371
+ const change = remembering
1372
+ ? picker.owner.plugins.rememberState(plugin.name)
1373
+ : picker.owner.plugins.setEnabled(plugin.name, enabled);
1374
+ void change.then(() => {
1375
+ if (appLifetime.signal.aborted ||
1376
+ conversationGenRef.current !== generation ||
1377
+ choicePickerRef.current !== pending)
1378
+ return;
1379
+ const updated = pluginActionPicker(picker.owner, plugin.name);
1380
+ setSelectedChoice(0);
1381
+ setChoicePicker(updated ? { ...updated, back: loadedPluginPicker(picker.owner) } : null);
1382
+ }, (error) => {
1383
+ if (appLifetime.signal.aborted ||
1384
+ conversationGenRef.current !== generation ||
1385
+ choicePickerRef.current !== pending)
1386
+ return;
1387
+ const updated = pluginActionPicker(picker.owner, plugin.name);
1388
+ setChoicePicker(updated
1389
+ ? {
1390
+ ...updated,
1391
+ back: loadedPluginPicker(picker.owner),
1392
+ notice: `Could not change plugin: ${error instanceof Error ? error.message : String(error)}`,
1393
+ }
1394
+ : null);
1395
+ });
1396
+ return;
1397
+ }
1324
1398
  if (picker.kind === 'review-preset') {
1325
1399
  if (reviewChoiceInFlightRef.current)
1326
1400
  return;
@@ -1580,6 +1654,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1580
1654
  setSelectedChoice,
1581
1655
  setTextPrompt,
1582
1656
  session,
1657
+ state,
1658
+ permission,
1583
1659
  ]);
1584
1660
  const sendCopyRequest = useCallback((index) => {
1585
1661
  const picker = copyPickerRef.current;
@@ -1785,6 +1861,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1785
1861
  ...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
1786
1862
  ...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
1787
1863
  ...(activeCtx.memory ? { memory: activeCtx.memory } : {}),
1864
+ ...(activeCtx.limits ? { limits: activeCtx.limits } : {}),
1788
1865
  ...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
1789
1866
  // Somebody is at this terminal, so the model may ask them one
1790
1867
  // question when a decision is genuinely theirs.
@@ -2696,6 +2773,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2696
2773
  pushMessage('system', 'The conversation changed while this editor was open. Reopen the command in the current conversation.');
2697
2774
  return;
2698
2775
  }
2776
+ if (prompt.kind === 'run-limit') {
2777
+ const field = RUN_LIMIT_FIELDS.find((field) => field.key === prompt.limitKey);
2778
+ if (field)
2779
+ commandPickerSubmitRef.current(`/config limits ${field.name} ${value.trim()}`);
2780
+ return;
2781
+ }
2699
2782
  if (prompt.kind === 'goal-create' || prompt.kind === 'goal-edit') {
2700
2783
  if (value.trim())
2701
2784
  commandPickerSubmitRef.current(`/goal ${prompt.kind === 'goal-create' ? 'set' : 'edit'} ${value.trim()}`);
@@ -3025,10 +3108,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3025
3108
  }
3026
3109
  switch (event.kind) {
3027
3110
  case 'delta': {
3028
- if (event.messageId && st.sourceMessageId !== event.messageId) {
3111
+ if (event.messageId &&
3112
+ (st.sourceMessageId !== event.messageId ||
3113
+ (event.textPart && st.sourceTextPartId !== event.textPart.id))) {
3029
3114
  if (st.sourceMessageId !== undefined)
3030
3115
  closeAssistant();
3031
3116
  st.sourceMessageId = event.messageId;
3117
+ st.sourceTextPartId = event.textPart?.id;
3032
3118
  }
3033
3119
  setState('thinking');
3034
3120
  if (event.messageId && event.runId && st.sessionId) {
@@ -3167,6 +3253,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3167
3253
  setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3168
3254
  break;
3169
3255
  }
3256
+ const evidence = !event.isError && event.output !== undefined
3257
+ ? conversationEvidenceView(event.toolName, event.output) : undefined;
3258
+ if (evidence !== undefined) {
3259
+ pushMessage('tool', evidence.content, false, '✓', evidence.detail, theme.status.ok, 'ctrl+o details', 'evidence');
3260
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3261
+ break;
3262
+ }
3170
3263
  pushMessage('tool', done?.label ?? formatToolCall(event.toolName, event.summary), false, event.isError ? '✗' : '✓', done?.detail, event.isError ? theme.status.error : theme.status.ok, event.durationMs !== undefined
3171
3264
  ? formatElapsed(event.durationMs)
3172
3265
  : done
@@ -3275,6 +3368,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3275
3368
  pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '!');
3276
3369
  break;
3277
3370
  case 'done': {
3371
+ // Persistence and memory use the settled answer, not concatenated
3372
+ // commentary deltas. The transcript keeps each visible part above.
3373
+ if (event.text !== undefined)
3374
+ st.text = event.text;
3278
3375
  // `run_completed` is not synonymous with success: budgets,
3279
3376
  // cancellation and output guardrails arrive through this event too.
3280
3377
  // Missing remains a normal end for older producers, matching the
@@ -3287,7 +3384,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3287
3384
  outcome: st.completed ? 'completed' : 'stopped',
3288
3385
  };
3289
3386
  closeAssistant();
3290
- const stopNotice = describeRunStop(event.stopReason);
3387
+ const stopNotice = describeRunStop(event.stopReason, event.budget);
3291
3388
  if (stopNotice)
3292
3389
  pushMessage('system', stopNotice, false, '■');
3293
3390
  break;
@@ -3587,6 +3684,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3587
3684
  activeTurnInboxRef.current = inbox;
3588
3685
  const turnPermissionMode = permissionModeRef.current;
3589
3686
  const turnReasoningEffort = reasoningEffortRef.current;
3687
+ const turnRunLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
3590
3688
  // Always carry the guarded callback. `auto` and `strict` decide before
3591
3689
  // calling it in makeResumeHandler; retaining it is what lets a session
3592
3690
  // launched with --yolo later return to prompt mode truthfully.
@@ -3644,6 +3742,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3644
3742
  signal: ac.signal,
3645
3743
  runId,
3646
3744
  permissionMode: turnPermissionMode,
3745
+ limits: turnRunLimits,
3647
3746
  ...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
3648
3747
  ...(goalRound ? { goalRound } : {}),
3649
3748
  // The mode above decides whether this callback is consulted.
@@ -3679,6 +3778,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3679
3778
  }
3680
3779
  else if (event.kind === 'delta')
3681
3780
  st.text += event.text;
3781
+ else if (event.kind === 'done' && event.text !== undefined)
3782
+ st.text = event.text;
3682
3783
  }
3683
3784
  }
3684
3785
  else {
@@ -4135,6 +4236,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4135
4236
  }
4136
4237
  setProviderSetup(true);
4137
4238
  return;
4239
+ case 'run-limits-set':
4240
+ case 'run-limit-editor':
4241
+ case 'run-limits-picker': {
4242
+ const currentLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
4243
+ if (slash.kind === 'run-limit-editor') {
4244
+ const scope = scopeRef.current;
4245
+ if (!scope) {
4246
+ pushMessage('system', 'Run limits are available after the session starts.');
4247
+ return;
4248
+ }
4249
+ const field = RUN_LIMIT_FIELDS.find((field) => field.key === slash.key);
4250
+ textPromptTokenRef.current += 1;
4251
+ setTextPrompt({
4252
+ token: textPromptTokenRef.current,
4253
+ kind: 'run-limit',
4254
+ limitKey: slash.key,
4255
+ title: `${field.label} (${slash.key === 'timeoutMs' ? 'ms; ' : ''}0 = unlimited)`,
4256
+ placeholder: field.hint,
4257
+ emptyNotice: 'Enter a value, or Esc to keep the current limit.',
4258
+ initialValue: String(currentLimits[slash.key]),
4259
+ sessionId: scope.sessionId,
4260
+ });
4261
+ return;
4262
+ }
4263
+ if (slash.kind === 'run-limits-set') {
4264
+ runLimitsOverrideRef.current = { ...runLimitsOverrideRef.current, ...slash.limits };
4265
+ }
4266
+ const commands = runLimitCommands(resolveRunGuards(currentLimits, runLimitsOverrideRef.current));
4267
+ setSelectedChoice(0);
4268
+ setChoicePicker({
4269
+ kind: 'command',
4270
+ title: 'Run limits',
4271
+ notice: `${slash.kind === 'run-limits-set' ? 'Updated. ' : ''}This session · new turns and their agents. Existing runs keep their limits.`,
4272
+ values: commands,
4273
+ options: commands.map((command) => ({
4274
+ label: command.label,
4275
+ description: command.description,
4276
+ })),
4277
+ windowSize: 5,
4278
+ });
4279
+ return;
4280
+ }
4138
4281
  case 'settings-picker': {
4139
4282
  const commands = [
4140
4283
  {
@@ -4155,6 +4298,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4155
4298
  label: 'Permissions',
4156
4299
  description: `${permissionModeLabel(effectivePermissionMode(permissionModeRef.current, session?.approvalLatched() ?? false))} · this session.`,
4157
4300
  },
4301
+ {
4302
+ name: 'config limits',
4303
+ label: 'Run limits',
4304
+ description: runLimitCommands(resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current))
4305
+ .slice(0, 3)
4306
+ .map((field) => `${field.label}: ${field.description}`)
4307
+ .join(' · '),
4308
+ },
4158
4309
  {
4159
4310
  name: 'status',
4160
4311
  label: 'Web & session',
@@ -4171,7 +4322,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4171
4322
  setChoicePicker({
4172
4323
  kind: 'command',
4173
4324
  title: 'Configuration',
4174
- notice: 'Model, reasoning and permission changes apply to this session.',
4325
+ notice: 'Model, reasoning, permission and run-limit changes apply to this session.',
4175
4326
  values: commands,
4176
4327
  options: commands.map((command) => ({
4177
4328
  label: command.label,
@@ -4484,6 +4635,31 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4484
4635
  pushMessage('system', `Skills (● active):\n ${lines.join('\n ')}`);
4485
4636
  return;
4486
4637
  }
4638
+ case 'plugins': {
4639
+ const plugins = session?.plugins?.list() ?? [];
4640
+ if (slash.name) {
4641
+ const plugin = plugins.find((item) => item.name === slash.name);
4642
+ pushMessage('system', plugin
4643
+ ? pluginDetails(plugin)
4644
+ : `Plugin "${slash.name}" is not loaded in this session. Use /plugins to inspect availability.`);
4645
+ }
4646
+ else if (plugins.length === 0 || !session) {
4647
+ pushMessage('system', emptyPluginReport(ctx.plugins, ctx.cwd));
4648
+ }
4649
+ else if (slash.list) {
4650
+ pushMessage('system', `Plugins\n${plugins
4651
+ .map((plugin) => {
4652
+ const option = pluginOption(plugin);
4653
+ return `${option.label} · ${option.description}`;
4654
+ })
4655
+ .join('\n')}`);
4656
+ }
4657
+ else {
4658
+ setSelectedChoice(0);
4659
+ setChoicePicker(loadedPluginPicker(session));
4660
+ }
4661
+ return;
4662
+ }
4487
4663
  case 'skill-picker': {
4488
4664
  const skills = discoverSkills({ cwd: ctx.cwd });
4489
4665
  if (skills.length === 0) {
@@ -5554,6 +5730,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5554
5730
  // menu, so a provider/session change cannot silently reinterpret Enter.
5555
5731
  if (choicePickerRef.current) {
5556
5732
  const picker = choicePickerRef.current;
5733
+ if ((picker.kind === 'plugin' || picker.kind === 'plugin-action') && picker.busy)
5734
+ return;
5557
5735
  // Composer publishes a chooser while handling Return. App receives the
5558
5736
  // same input dispatch (and key repeat can arrive before the commit), so
5559
5737
  // the synchronous ref is an ownership fence but not yet an actionable
@@ -5805,7 +5983,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5805
5983
  setPickerSelectionKind('provider-and-model');
5806
5984
  setPhase('picker');
5807
5985
  void discoverProviders({ skipProbes: true }).then(setPickerDetected).catch((error) => setPickerNotice(String(error)));
5808
- } })) : phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true && permission === null && textPrompt === null, thinking: thinking })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && outputViewer === null && choicePicker ? (_jsx(ChoicePicker, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
5986
+ } })) : phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true && permission === null && textPrompt === null, thinking: thinking })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && outputViewer === null && choicePicker ? (_jsx(ChoicePicker, { busy: 'busy' in choicePicker && choicePicker.busy === true, columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
5809
5987
  state !== 'awaiting-permission' &&
5810
5988
  !compacting &&
5811
5989
  externalEditorRequest === null &&
@@ -5932,7 +6110,55 @@ function labelOfOption(question, optionId) {
5932
6110
  return question.options.find((option) => option.id === optionId)?.label ?? optionId;
5933
6111
  }
5934
6112
  function choicePickerSearchable(picker) {
5935
- return ['command', 'skill', 'review-branch', 'review-commit'].includes(picker.kind);
6113
+ return ['command', 'skill', 'plugin', 'review-branch', 'review-commit'].includes(picker.kind);
6114
+ }
6115
+ function loadedPluginPicker(owner) {
6116
+ const plugins = owner.plugins?.list() ?? [];
6117
+ return {
6118
+ kind: 'plugin',
6119
+ owner,
6120
+ title: 'Plugins',
6121
+ notice: 'Loaded in this session · select to inspect or change state',
6122
+ values: plugins.map((plugin) => plugin.name),
6123
+ options: plugins.map(pluginOption),
6124
+ };
6125
+ }
6126
+ function pluginActionPicker(owner, name) {
6127
+ const plugin = owner.plugins?.list().find((item) => item.name === name);
6128
+ if (!plugin)
6129
+ return null;
6130
+ const enabled = plugin.status === 'enabled';
6131
+ return {
6132
+ kind: 'plugin-action',
6133
+ owner,
6134
+ pluginName: name,
6135
+ title: `${plugin.name} · ${plugin.version}`,
6136
+ notice: `${plugin.status} · after restart: ${pluginStartupState(plugin)} · ${plugin.tools.length} tool${plugin.tools.length === 1 ? '' : 's'}`,
6137
+ values: ['details', enabled ? 'disable' : 'enable', 'remember'],
6138
+ options: [
6139
+ {
6140
+ label: 'Details',
6141
+ description: 'Show directory and contributions.',
6142
+ selectedDescription: plugin.description,
6143
+ },
6144
+ {
6145
+ label: enabled ? 'Disable for this session' : 'Enable for this session',
6146
+ description: enabled
6147
+ ? 'Remove its tools, skills and hooks; disconnect its MCP servers.'
6148
+ : 'Load this trusted plugin’s tools, skills, hooks and MCP servers.',
6149
+ selectedDescription: 'Resets on restart or model switch. An idle session is required.',
6150
+ },
6151
+ {
6152
+ label: `Keep ${enabled ? 'enabled' : 'disabled'} after restart`,
6153
+ description: 'Remember this state for future sessions and model switches.',
6154
+ selectedDescription: plugin.startupError ??
6155
+ 'Applies only to this plugin directory. Other running sessions keep their state.',
6156
+ ...(plugin.startupEnabled === enabled
6157
+ ? { disabledReason: 'Already the startup setting.' }
6158
+ : {}),
6159
+ },
6160
+ ],
6161
+ };
5936
6162
  }
5937
6163
  function permissionPicker(mode, approvedAll, more = false) {
5938
6164
  const current = effectivePermissionMode(mode, approvedAll);