@namzu/cli 24.0.0 → 25.0.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 (192) hide show
  1. package/CHANGELOG.md +776 -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 +2 -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 +235 -15
  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 +159 -31
  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);
@@ -1106,7 +1110,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1106
1110
  // so the feedback store correctly refused every TUI rating as unverifiable.
1107
1111
  // Bind both trees to the exact conversation captured with the streamed
1108
1112
  // message; `/resume` may change the active scope before a delayed click.
1109
- const sessionDir = new DefaultPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
1113
+ const sessionDir = new CliPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
1110
1114
  const store = new DiskMessageFeedbackStore({
1111
1115
  rootDir: join(sessionDir, 'feedback'),
1112
1116
  runsDir: join(sessionDir, 'runs'),
@@ -1321,6 +1325,70 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1321
1325
  return;
1322
1326
  if (picker.options[index]?.disabledReason)
1323
1327
  return;
1328
+ if (picker.kind === 'plugin' || picker.kind === 'plugin-action') {
1329
+ if (picker.busy || picker.owner !== session)
1330
+ return;
1331
+ if (picker.kind === 'plugin') {
1332
+ const child = pluginActionPicker(picker.owner, value);
1333
+ setSelectedChoice(0);
1334
+ setChoicePicker(child ? { ...child, back: picker } : null);
1335
+ return;
1336
+ }
1337
+ const plugin = picker.owner.plugins?.list().find((item) => item.name === picker.pluginName);
1338
+ if (!plugin || !picker.owner.plugins)
1339
+ return;
1340
+ if (value === 'details') {
1341
+ setChoicePicker(null);
1342
+ pushMessage('system', pluginDetails(plugin));
1343
+ return;
1344
+ }
1345
+ if (state !== 'idle' || permission) {
1346
+ setChoicePicker({
1347
+ ...picker,
1348
+ notice: 'Wait for the active turn and permission prompts to finish.',
1349
+ });
1350
+ return;
1351
+ }
1352
+ const enabled = value === 'enable';
1353
+ const remembering = value === 'remember';
1354
+ const pending = {
1355
+ ...picker,
1356
+ busy: true,
1357
+ notice: remembering
1358
+ ? 'Saving plugin setting…'
1359
+ : enabled
1360
+ ? 'Enabling plugin…'
1361
+ : 'Disabling plugin…',
1362
+ };
1363
+ setChoicePicker(pending);
1364
+ const generation = conversationGenRef.current;
1365
+ const change = remembering
1366
+ ? picker.owner.plugins.rememberState(plugin.name)
1367
+ : picker.owner.plugins.setEnabled(plugin.name, enabled);
1368
+ void change.then(() => {
1369
+ if (appLifetime.signal.aborted ||
1370
+ conversationGenRef.current !== generation ||
1371
+ choicePickerRef.current !== pending)
1372
+ return;
1373
+ const updated = pluginActionPicker(picker.owner, plugin.name);
1374
+ setSelectedChoice(0);
1375
+ setChoicePicker(updated ? { ...updated, back: loadedPluginPicker(picker.owner) } : null);
1376
+ }, (error) => {
1377
+ if (appLifetime.signal.aborted ||
1378
+ conversationGenRef.current !== generation ||
1379
+ choicePickerRef.current !== pending)
1380
+ return;
1381
+ const updated = pluginActionPicker(picker.owner, plugin.name);
1382
+ setChoicePicker(updated
1383
+ ? {
1384
+ ...updated,
1385
+ back: loadedPluginPicker(picker.owner),
1386
+ notice: `Could not change plugin: ${error instanceof Error ? error.message : String(error)}`,
1387
+ }
1388
+ : null);
1389
+ });
1390
+ return;
1391
+ }
1324
1392
  if (picker.kind === 'review-preset') {
1325
1393
  if (reviewChoiceInFlightRef.current)
1326
1394
  return;
@@ -1580,6 +1648,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1580
1648
  setSelectedChoice,
1581
1649
  setTextPrompt,
1582
1650
  session,
1651
+ state,
1652
+ permission,
1583
1653
  ]);
1584
1654
  const sendCopyRequest = useCallback((index) => {
1585
1655
  const picker = copyPickerRef.current;
@@ -1785,6 +1855,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1785
1855
  ...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
1786
1856
  ...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
1787
1857
  ...(activeCtx.memory ? { memory: activeCtx.memory } : {}),
1858
+ ...(activeCtx.limits ? { limits: activeCtx.limits } : {}),
1788
1859
  ...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
1789
1860
  // Somebody is at this terminal, so the model may ask them one
1790
1861
  // question when a decision is genuinely theirs.
@@ -2696,6 +2767,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2696
2767
  pushMessage('system', 'The conversation changed while this editor was open. Reopen the command in the current conversation.');
2697
2768
  return;
2698
2769
  }
2770
+ if (prompt.kind === 'run-limit') {
2771
+ const field = RUN_LIMIT_FIELDS.find((field) => field.key === prompt.limitKey);
2772
+ if (field)
2773
+ commandPickerSubmitRef.current(`/config limits ${field.name} ${value.trim()}`);
2774
+ return;
2775
+ }
2699
2776
  if (prompt.kind === 'goal-create' || prompt.kind === 'goal-edit') {
2700
2777
  if (value.trim())
2701
2778
  commandPickerSubmitRef.current(`/goal ${prompt.kind === 'goal-create' ? 'set' : 'edit'} ${value.trim()}`);
@@ -3025,10 +3102,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3025
3102
  }
3026
3103
  switch (event.kind) {
3027
3104
  case 'delta': {
3028
- if (event.messageId && st.sourceMessageId !== event.messageId) {
3105
+ if (event.messageId &&
3106
+ (st.sourceMessageId !== event.messageId ||
3107
+ (event.textPart && st.sourceTextPartId !== event.textPart.id))) {
3029
3108
  if (st.sourceMessageId !== undefined)
3030
3109
  closeAssistant();
3031
3110
  st.sourceMessageId = event.messageId;
3111
+ st.sourceTextPartId = event.textPart?.id;
3032
3112
  }
3033
3113
  setState('thinking');
3034
3114
  if (event.messageId && event.runId && st.sessionId) {
@@ -3167,6 +3247,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3167
3247
  setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3168
3248
  break;
3169
3249
  }
3250
+ const evidence = !event.isError && event.output !== undefined
3251
+ ? conversationEvidenceView(event.toolName, event.output) : undefined;
3252
+ if (evidence !== undefined) {
3253
+ pushMessage('tool', evidence.content, false, '✓', evidence.detail, theme.status.ok, 'ctrl+o details', 'evidence');
3254
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3255
+ break;
3256
+ }
3170
3257
  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
3258
  ? formatElapsed(event.durationMs)
3172
3259
  : done
@@ -3275,6 +3362,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3275
3362
  pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '!');
3276
3363
  break;
3277
3364
  case 'done': {
3365
+ // Persistence and memory use the settled answer, not concatenated
3366
+ // commentary deltas. The transcript keeps each visible part above.
3367
+ if (event.text !== undefined)
3368
+ st.text = event.text;
3278
3369
  // `run_completed` is not synonymous with success: budgets,
3279
3370
  // cancellation and output guardrails arrive through this event too.
3280
3371
  // Missing remains a normal end for older producers, matching the
@@ -3287,7 +3378,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3287
3378
  outcome: st.completed ? 'completed' : 'stopped',
3288
3379
  };
3289
3380
  closeAssistant();
3290
- const stopNotice = describeRunStop(event.stopReason);
3381
+ const stopNotice = describeRunStop(event.stopReason, event.budget);
3291
3382
  if (stopNotice)
3292
3383
  pushMessage('system', stopNotice, false, '■');
3293
3384
  break;
@@ -3587,6 +3678,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3587
3678
  activeTurnInboxRef.current = inbox;
3588
3679
  const turnPermissionMode = permissionModeRef.current;
3589
3680
  const turnReasoningEffort = reasoningEffortRef.current;
3681
+ const turnRunLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
3590
3682
  // Always carry the guarded callback. `auto` and `strict` decide before
3591
3683
  // calling it in makeResumeHandler; retaining it is what lets a session
3592
3684
  // launched with --yolo later return to prompt mode truthfully.
@@ -3644,6 +3736,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3644
3736
  signal: ac.signal,
3645
3737
  runId,
3646
3738
  permissionMode: turnPermissionMode,
3739
+ limits: turnRunLimits,
3647
3740
  ...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
3648
3741
  ...(goalRound ? { goalRound } : {}),
3649
3742
  // The mode above decides whether this callback is consulted.
@@ -3679,6 +3772,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3679
3772
  }
3680
3773
  else if (event.kind === 'delta')
3681
3774
  st.text += event.text;
3775
+ else if (event.kind === 'done' && event.text !== undefined)
3776
+ st.text = event.text;
3682
3777
  }
3683
3778
  }
3684
3779
  else {
@@ -4135,6 +4230,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4135
4230
  }
4136
4231
  setProviderSetup(true);
4137
4232
  return;
4233
+ case 'run-limits-set':
4234
+ case 'run-limit-editor':
4235
+ case 'run-limits-picker': {
4236
+ const currentLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
4237
+ if (slash.kind === 'run-limit-editor') {
4238
+ const scope = scopeRef.current;
4239
+ if (!scope) {
4240
+ pushMessage('system', 'Run limits are available after the session starts.');
4241
+ return;
4242
+ }
4243
+ const field = RUN_LIMIT_FIELDS.find((field) => field.key === slash.key);
4244
+ textPromptTokenRef.current += 1;
4245
+ setTextPrompt({
4246
+ token: textPromptTokenRef.current,
4247
+ kind: 'run-limit',
4248
+ limitKey: slash.key,
4249
+ title: `${field.label} (${slash.key === 'timeoutMs' ? 'ms; ' : ''}0 = unlimited)`,
4250
+ placeholder: field.hint,
4251
+ emptyNotice: 'Enter a value, or Esc to keep the current limit.',
4252
+ initialValue: String(currentLimits[slash.key]),
4253
+ sessionId: scope.sessionId,
4254
+ });
4255
+ return;
4256
+ }
4257
+ if (slash.kind === 'run-limits-set') {
4258
+ runLimitsOverrideRef.current = { ...runLimitsOverrideRef.current, ...slash.limits };
4259
+ }
4260
+ const commands = runLimitCommands(resolveRunGuards(currentLimits, runLimitsOverrideRef.current));
4261
+ setSelectedChoice(0);
4262
+ setChoicePicker({
4263
+ kind: 'command',
4264
+ title: 'Run limits',
4265
+ notice: `${slash.kind === 'run-limits-set' ? 'Updated. ' : ''}This session · new turns and their agents. Existing runs keep their limits.`,
4266
+ values: commands,
4267
+ options: commands.map((command) => ({
4268
+ label: command.label,
4269
+ description: command.description,
4270
+ })),
4271
+ windowSize: 5,
4272
+ });
4273
+ return;
4274
+ }
4138
4275
  case 'settings-picker': {
4139
4276
  const commands = [
4140
4277
  {
@@ -4155,6 +4292,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4155
4292
  label: 'Permissions',
4156
4293
  description: `${permissionModeLabel(effectivePermissionMode(permissionModeRef.current, session?.approvalLatched() ?? false))} · this session.`,
4157
4294
  },
4295
+ {
4296
+ name: 'config limits',
4297
+ label: 'Run limits',
4298
+ description: runLimitCommands(resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current))
4299
+ .slice(0, 3)
4300
+ .map((field) => `${field.label}: ${field.description}`)
4301
+ .join(' · '),
4302
+ },
4158
4303
  {
4159
4304
  name: 'status',
4160
4305
  label: 'Web & session',
@@ -4171,7 +4316,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4171
4316
  setChoicePicker({
4172
4317
  kind: 'command',
4173
4318
  title: 'Configuration',
4174
- notice: 'Model, reasoning and permission changes apply to this session.',
4319
+ notice: 'Model, reasoning, permission and run-limit changes apply to this session.',
4175
4320
  values: commands,
4176
4321
  options: commands.map((command) => ({
4177
4322
  label: command.label,
@@ -4484,6 +4629,31 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4484
4629
  pushMessage('system', `Skills (● active):\n ${lines.join('\n ')}`);
4485
4630
  return;
4486
4631
  }
4632
+ case 'plugins': {
4633
+ const plugins = session?.plugins?.list() ?? [];
4634
+ if (slash.name) {
4635
+ const plugin = plugins.find((item) => item.name === slash.name);
4636
+ pushMessage('system', plugin
4637
+ ? pluginDetails(plugin)
4638
+ : `Plugin "${slash.name}" is not loaded in this session. Use /plugins to inspect availability.`);
4639
+ }
4640
+ else if (plugins.length === 0 || !session) {
4641
+ pushMessage('system', emptyPluginReport(ctx.plugins, ctx.cwd));
4642
+ }
4643
+ else if (slash.list) {
4644
+ pushMessage('system', `Plugins\n${plugins
4645
+ .map((plugin) => {
4646
+ const option = pluginOption(plugin);
4647
+ return `${option.label} · ${option.description}`;
4648
+ })
4649
+ .join('\n')}`);
4650
+ }
4651
+ else {
4652
+ setSelectedChoice(0);
4653
+ setChoicePicker(loadedPluginPicker(session));
4654
+ }
4655
+ return;
4656
+ }
4487
4657
  case 'skill-picker': {
4488
4658
  const skills = discoverSkills({ cwd: ctx.cwd });
4489
4659
  if (skills.length === 0) {
@@ -5554,6 +5724,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5554
5724
  // menu, so a provider/session change cannot silently reinterpret Enter.
5555
5725
  if (choicePickerRef.current) {
5556
5726
  const picker = choicePickerRef.current;
5727
+ if ((picker.kind === 'plugin' || picker.kind === 'plugin-action') && picker.busy)
5728
+ return;
5557
5729
  // Composer publishes a chooser while handling Return. App receives the
5558
5730
  // same input dispatch (and key repeat can arrive before the commit), so
5559
5731
  // the synchronous ref is an ownership fence but not yet an actionable
@@ -5805,7 +5977,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5805
5977
  setPickerSelectionKind('provider-and-model');
5806
5978
  setPhase('picker');
5807
5979
  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' &&
5980
+ } })) : 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
5981
  state !== 'awaiting-permission' &&
5810
5982
  !compacting &&
5811
5983
  externalEditorRequest === null &&
@@ -5932,7 +6104,55 @@ function labelOfOption(question, optionId) {
5932
6104
  return question.options.find((option) => option.id === optionId)?.label ?? optionId;
5933
6105
  }
5934
6106
  function choicePickerSearchable(picker) {
5935
- return ['command', 'skill', 'review-branch', 'review-commit'].includes(picker.kind);
6107
+ return ['command', 'skill', 'plugin', 'review-branch', 'review-commit'].includes(picker.kind);
6108
+ }
6109
+ function loadedPluginPicker(owner) {
6110
+ const plugins = owner.plugins?.list() ?? [];
6111
+ return {
6112
+ kind: 'plugin',
6113
+ owner,
6114
+ title: 'Plugins',
6115
+ notice: 'Loaded in this session · select to inspect or change state',
6116
+ values: plugins.map((plugin) => plugin.name),
6117
+ options: plugins.map(pluginOption),
6118
+ };
6119
+ }
6120
+ function pluginActionPicker(owner, name) {
6121
+ const plugin = owner.plugins?.list().find((item) => item.name === name);
6122
+ if (!plugin)
6123
+ return null;
6124
+ const enabled = plugin.status === 'enabled';
6125
+ return {
6126
+ kind: 'plugin-action',
6127
+ owner,
6128
+ pluginName: name,
6129
+ title: `${plugin.name} · ${plugin.version}`,
6130
+ notice: `${plugin.status} · after restart: ${pluginStartupState(plugin)} · ${plugin.tools.length} tool${plugin.tools.length === 1 ? '' : 's'}`,
6131
+ values: ['details', enabled ? 'disable' : 'enable', 'remember'],
6132
+ options: [
6133
+ {
6134
+ label: 'Details',
6135
+ description: 'Show directory and contributions.',
6136
+ selectedDescription: plugin.description,
6137
+ },
6138
+ {
6139
+ label: enabled ? 'Disable for this session' : 'Enable for this session',
6140
+ description: enabled
6141
+ ? 'Remove its tools, skills and hooks; disconnect its MCP servers.'
6142
+ : 'Load this trusted plugin’s tools, skills, hooks and MCP servers.',
6143
+ selectedDescription: 'Resets on restart or model switch. An idle session is required.',
6144
+ },
6145
+ {
6146
+ label: `Keep ${enabled ? 'enabled' : 'disabled'} after restart`,
6147
+ description: 'Remember this state for future sessions and model switches.',
6148
+ selectedDescription: plugin.startupError ??
6149
+ 'Applies only to this plugin directory. Other running sessions keep their state.',
6150
+ ...(plugin.startupEnabled === enabled
6151
+ ? { disabledReason: 'Already the startup setting.' }
6152
+ : {}),
6153
+ },
6154
+ ],
6155
+ };
5936
6156
  }
5937
6157
  function permissionPicker(mode, approvedAll, more = false) {
5938
6158
  const current = effectivePermissionMode(mode, approvedAll);