@namzu/cli 30.0.0 → 32.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 (256) hide show
  1. package/CHANGELOG.md +217 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +5 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/acp.d.ts +20 -2
  6. package/dist/commands/acp.d.ts.map +1 -1
  7. package/dist/commands/acp.js +42 -5
  8. package/dist/commands/acp.js.map +1 -1
  9. package/dist/commands/drain.d.ts.map +1 -1
  10. package/dist/commands/drain.js +2 -1
  11. package/dist/commands/drain.js.map +1 -1
  12. package/dist/commands/exec-json.d.ts.map +1 -1
  13. package/dist/commands/exec-json.js +24 -1
  14. package/dist/commands/exec-json.js.map +1 -1
  15. package/dist/commands/exec.d.ts.map +1 -1
  16. package/dist/commands/exec.js +2 -1
  17. package/dist/commands/exec.js.map +1 -1
  18. package/dist/commands/prior-messages.d.ts.map +1 -1
  19. package/dist/commands/prior-messages.js +42 -2
  20. package/dist/commands/prior-messages.js.map +1 -1
  21. package/dist/commands/schedule.d.ts.map +1 -1
  22. package/dist/commands/schedule.js +7 -5
  23. package/dist/commands/schedule.js.map +1 -1
  24. package/dist/config/composer-triggers.d.ts +41 -0
  25. package/dist/config/composer-triggers.d.ts.map +1 -0
  26. package/dist/config/composer-triggers.js +80 -0
  27. package/dist/config/composer-triggers.js.map +1 -0
  28. package/dist/config/load.d.ts.map +1 -1
  29. package/dist/config/load.js +58 -0
  30. package/dist/config/load.js.map +1 -1
  31. package/dist/config/schema.d.ts +34 -0
  32. package/dist/config/schema.d.ts.map +1 -1
  33. package/dist/config/schema.js.map +1 -1
  34. package/dist/context/doctrine.d.ts +3 -3
  35. package/dist/context/doctrine.d.ts.map +1 -1
  36. package/dist/context/doctrine.js +3 -3
  37. package/dist/context/doctrine.js.map +1 -1
  38. package/dist/context/runtime-message.d.ts.map +1 -1
  39. package/dist/context/runtime-message.js +4 -0
  40. package/dist/context/runtime-message.js.map +1 -1
  41. package/dist/integrations/mcp/servers.d.ts +72 -5
  42. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  43. package/dist/integrations/mcp/servers.js +144 -23
  44. package/dist/integrations/mcp/servers.js.map +1 -1
  45. package/dist/integrations/plugins/runtime.d.ts +11 -9
  46. package/dist/integrations/plugins/runtime.d.ts.map +1 -1
  47. package/dist/integrations/plugins/runtime.js +17 -36
  48. package/dist/integrations/plugins/runtime.js.map +1 -1
  49. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  50. package/dist/integrations/resident/session-step.js +2 -1
  51. package/dist/integrations/resident/session-step.js.map +1 -1
  52. package/dist/integrations/sessions/store.d.ts.map +1 -1
  53. package/dist/integrations/sessions/store.js +20 -2
  54. package/dist/integrations/sessions/store.js.map +1 -1
  55. package/dist/integrations/subagents/NamzuCliAgent.d.ts +8 -0
  56. package/dist/integrations/subagents/NamzuCliAgent.d.ts.map +1 -0
  57. package/dist/integrations/subagents/NamzuCliAgent.js +9 -0
  58. package/dist/integrations/subagents/NamzuCliAgent.js.map +1 -0
  59. package/dist/integrations/subagents/runtime.d.ts +5 -4
  60. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  61. package/dist/integrations/subagents/runtime.js +46 -11
  62. package/dist/integrations/subagents/runtime.js.map +1 -1
  63. package/dist/integrations/web/search.d.ts.map +1 -1
  64. package/dist/integrations/web/search.js +8 -6
  65. package/dist/integrations/web/search.js.map +1 -1
  66. package/dist/permissions/rules.d.ts +5 -1
  67. package/dist/permissions/rules.d.ts.map +1 -1
  68. package/dist/permissions/rules.js +85 -3
  69. package/dist/permissions/rules.js.map +1 -1
  70. package/dist/schedule/build.d.ts +19 -2
  71. package/dist/schedule/build.d.ts.map +1 -1
  72. package/dist/schedule/build.js +119 -21
  73. package/dist/schedule/build.js.map +1 -1
  74. package/dist/schedule/changes.d.ts.map +1 -1
  75. package/dist/schedule/changes.js +13 -0
  76. package/dist/schedule/changes.js.map +1 -1
  77. package/dist/schedule/commands/add.d.ts +1 -1
  78. package/dist/schedule/commands/add.d.ts.map +1 -1
  79. package/dist/schedule/commands/add.js +186 -40
  80. package/dist/schedule/commands/add.js.map +1 -1
  81. package/dist/schedule/commands/list.d.ts +2 -0
  82. package/dist/schedule/commands/list.d.ts.map +1 -1
  83. package/dist/schedule/commands/list.js +64 -19
  84. package/dist/schedule/commands/list.js.map +1 -1
  85. package/dist/schedule/daemon/daemon.d.ts +1 -1
  86. package/dist/schedule/daemon/daemon.d.ts.map +1 -1
  87. package/dist/schedule/daemon/daemon.js +17 -5
  88. package/dist/schedule/daemon/daemon.js.map +1 -1
  89. package/dist/schedule/daemon/notify.d.ts +21 -1
  90. package/dist/schedule/daemon/notify.d.ts.map +1 -1
  91. package/dist/schedule/daemon/notify.js +43 -0
  92. package/dist/schedule/daemon/notify.js.map +1 -1
  93. package/dist/schedule/fire/fire.d.ts +1 -1
  94. package/dist/schedule/fire/fire.d.ts.map +1 -1
  95. package/dist/schedule/fire/fire.js +200 -14
  96. package/dist/schedule/fire/fire.js.map +1 -1
  97. package/dist/schedule/fire/result.d.ts +1 -1
  98. package/dist/schedule/fire/result.d.ts.map +1 -1
  99. package/dist/schedule/fire/result.js +2 -1
  100. package/dist/schedule/fire/result.js.map +1 -1
  101. package/dist/schedule/fire/run-script.d.ts +46 -0
  102. package/dist/schedule/fire/run-script.d.ts.map +1 -0
  103. package/dist/schedule/fire/run-script.js +81 -0
  104. package/dist/schedule/fire/run-script.js.map +1 -0
  105. package/dist/schedule/fire/unattended-note.d.ts +8 -0
  106. package/dist/schedule/fire/unattended-note.d.ts.map +1 -1
  107. package/dist/schedule/fire/unattended-note.js +5 -0
  108. package/dist/schedule/fire/unattended-note.js.map +1 -1
  109. package/dist/schedule/fire/wake-gate.d.ts +39 -0
  110. package/dist/schedule/fire/wake-gate.d.ts.map +1 -0
  111. package/dist/schedule/fire/wake-gate.js +90 -0
  112. package/dist/schedule/fire/wake-gate.js.map +1 -0
  113. package/dist/schedule/floor.d.ts +1 -1
  114. package/dist/schedule/floor.d.ts.map +1 -1
  115. package/dist/schedule/floor.js +288 -16
  116. package/dist/schedule/floor.js.map +1 -1
  117. package/dist/schedule/policy.d.ts +40 -0
  118. package/dist/schedule/policy.d.ts.map +1 -1
  119. package/dist/schedule/policy.js +36 -0
  120. package/dist/schedule/policy.js.map +1 -1
  121. package/dist/schedule/script-check.d.ts +53 -0
  122. package/dist/schedule/script-check.d.ts.map +1 -0
  123. package/dist/schedule/script-check.js +145 -0
  124. package/dist/schedule/script-check.js.map +1 -0
  125. package/dist/schedule/script-shell.d.ts +3 -0
  126. package/dist/schedule/script-shell.d.ts.map +1 -0
  127. package/dist/schedule/script-shell.js +5 -0
  128. package/dist/schedule/script-shell.js.map +1 -0
  129. package/dist/schedule/service/manifest.js +1 -1
  130. package/dist/schedule/service/manifest.js.map +1 -1
  131. package/dist/schedule/store/atomic.d.ts +5 -2
  132. package/dist/schedule/store/atomic.d.ts.map +1 -1
  133. package/dist/schedule/store/atomic.js +7 -4
  134. package/dist/schedule/store/atomic.js.map +1 -1
  135. package/dist/schedule/store/claims.js +1 -1
  136. package/dist/schedule/store/claims.js.map +1 -1
  137. package/dist/schedule/store/history.d.ts +1 -1
  138. package/dist/schedule/store/history.d.ts.map +1 -1
  139. package/dist/schedule/store/history.js +4 -1
  140. package/dist/schedule/store/history.js.map +1 -1
  141. package/dist/schedule/store/jobs.d.ts +19 -3
  142. package/dist/schedule/store/jobs.d.ts.map +1 -1
  143. package/dist/schedule/store/jobs.js +83 -16
  144. package/dist/schedule/store/jobs.js.map +1 -1
  145. package/dist/schedule/store/state.js +1 -1
  146. package/dist/schedule/store/state.js.map +1 -1
  147. package/dist/schedule/types.d.ts +71 -6
  148. package/dist/schedule/types.d.ts.map +1 -1
  149. package/dist/schedule/types.js +25 -1
  150. package/dist/schedule/types.js.map +1 -1
  151. package/dist/schedule/visible-source.d.ts +18 -0
  152. package/dist/schedule/visible-source.d.ts.map +1 -0
  153. package/dist/schedule/visible-source.js +59 -0
  154. package/dist/schedule/visible-source.js.map +1 -0
  155. package/dist/test-support/toolset.d.ts +23 -0
  156. package/dist/test-support/toolset.d.ts.map +1 -0
  157. package/dist/test-support/toolset.js +27 -0
  158. package/dist/test-support/toolset.js.map +1 -0
  159. package/dist/tui/App.d.ts.map +1 -1
  160. package/dist/tui/App.js +323 -60
  161. package/dist/tui/App.js.map +1 -1
  162. package/dist/tui/ChoicePicker.d.ts +2 -0
  163. package/dist/tui/ChoicePicker.d.ts.map +1 -1
  164. package/dist/tui/ChoicePicker.js +10 -2
  165. package/dist/tui/ChoicePicker.js.map +1 -1
  166. package/dist/tui/Composer.d.ts +62 -3
  167. package/dist/tui/Composer.d.ts.map +1 -1
  168. package/dist/tui/Composer.js +162 -25
  169. package/dist/tui/Composer.js.map +1 -1
  170. package/dist/tui/ComposerFrame.d.ts +1 -1
  171. package/dist/tui/ComposerFrame.js +3 -3
  172. package/dist/tui/ComposerFrame.js.map +1 -1
  173. package/dist/tui/EffortSlider.d.ts +8 -9
  174. package/dist/tui/EffortSlider.d.ts.map +1 -1
  175. package/dist/tui/EffortSlider.js +15 -12
  176. package/dist/tui/EffortSlider.js.map +1 -1
  177. package/dist/tui/StatusBar.d.ts +12 -12
  178. package/dist/tui/StatusBar.d.ts.map +1 -1
  179. package/dist/tui/StatusBar.js +15 -15
  180. package/dist/tui/StatusBar.js.map +1 -1
  181. package/dist/tui/agent.d.ts +46 -7
  182. package/dist/tui/agent.d.ts.map +1 -1
  183. package/dist/tui/agent.js +386 -178
  184. package/dist/tui/agent.js.map +1 -1
  185. package/dist/tui/hypermode.d.ts +37 -0
  186. package/dist/tui/hypermode.d.ts.map +1 -0
  187. package/dist/tui/hypermode.js +58 -0
  188. package/dist/tui/hypermode.js.map +1 -0
  189. package/dist/tui/permission-review.d.ts +1 -1
  190. package/dist/tui/permission-review.js +1 -1
  191. package/dist/tui/schedule/host-commands.d.ts.map +1 -1
  192. package/dist/tui/schedule/host-commands.js +94 -33
  193. package/dist/tui/schedule/host-commands.js.map +1 -1
  194. package/dist/tui/schedule/integration.d.ts +6 -2
  195. package/dist/tui/schedule/integration.d.ts.map +1 -1
  196. package/dist/tui/schedule/integration.js +3 -1
  197. package/dist/tui/schedule/integration.js.map +1 -1
  198. package/dist/tui/schedule/resume.d.ts.map +1 -1
  199. package/dist/tui/schedule/resume.js +10 -1
  200. package/dist/tui/schedule/resume.js.map +1 -1
  201. package/dist/tui/schedule/tool-host.d.ts +4 -1
  202. package/dist/tui/schedule/tool-host.d.ts.map +1 -1
  203. package/dist/tui/schedule/tool-host.js +155 -40
  204. package/dist/tui/schedule/tool-host.js.map +1 -1
  205. package/dist/tui/slashCommands.d.ts +23 -5
  206. package/dist/tui/slashCommands.d.ts.map +1 -1
  207. package/dist/tui/slashCommands.js +80 -31
  208. package/dist/tui/slashCommands.js.map +1 -1
  209. package/dist/tui/theme.d.ts +10 -4
  210. package/dist/tui/theme.d.ts.map +1 -1
  211. package/dist/tui/theme.js +4 -3
  212. package/dist/tui/theme.js.map +1 -1
  213. package/dist/tui/triggers/analyze.d.ts +57 -0
  214. package/dist/tui/triggers/analyze.d.ts.map +1 -0
  215. package/dist/tui/triggers/analyze.js +221 -0
  216. package/dist/tui/triggers/analyze.js.map +1 -0
  217. package/dist/tui/triggers/context-text.d.ts +21 -0
  218. package/dist/tui/triggers/context-text.d.ts.map +1 -0
  219. package/dist/tui/triggers/context-text.js +44 -0
  220. package/dist/tui/triggers/context-text.js.map +1 -0
  221. package/dist/tui/triggers/copy.d.ts +38 -0
  222. package/dist/tui/triggers/copy.d.ts.map +1 -0
  223. package/dist/tui/triggers/copy.js +163 -0
  224. package/dist/tui/triggers/copy.js.map +1 -0
  225. package/dist/tui/triggers/detect.d.ts +83 -0
  226. package/dist/tui/triggers/detect.d.ts.map +1 -0
  227. package/dist/tui/triggers/detect.js +295 -0
  228. package/dist/tui/triggers/detect.js.map +1 -0
  229. package/dist/tui/triggers/fold.d.ts +34 -0
  230. package/dist/tui/triggers/fold.d.ts.map +1 -0
  231. package/dist/tui/triggers/fold.js +44 -0
  232. package/dist/tui/triggers/fold.js.map +1 -0
  233. package/dist/tui/triggers/pattern.d.ts +66 -0
  234. package/dist/tui/triggers/pattern.d.ts.map +1 -0
  235. package/dist/tui/triggers/pattern.js +243 -0
  236. package/dist/tui/triggers/pattern.js.map +1 -0
  237. package/dist/tui/triggers/provenance.d.ts +58 -0
  238. package/dist/tui/triggers/provenance.d.ts.map +1 -0
  239. package/dist/tui/triggers/provenance.js +98 -0
  240. package/dist/tui/triggers/provenance.js.map +1 -0
  241. package/dist/tui/triggers/registry.d.ts +113 -0
  242. package/dist/tui/triggers/registry.d.ts.map +1 -0
  243. package/dist/tui/triggers/registry.js +164 -0
  244. package/dist/tui/triggers/registry.js.map +1 -0
  245. package/dist/tui/triggers/setting.d.ts +18 -0
  246. package/dist/tui/triggers/setting.d.ts.map +1 -0
  247. package/dist/tui/triggers/setting.js +61 -0
  248. package/dist/tui/triggers/setting.js.map +1 -0
  249. package/dist/tui/triggers/verbs.d.ts +29 -0
  250. package/dist/tui/triggers/verbs.d.ts.map +1 -0
  251. package/dist/tui/triggers/verbs.js +140 -0
  252. package/dist/tui/triggers/verbs.js.map +1 -0
  253. package/dist/tui/types.d.ts +3 -1
  254. package/dist/tui/types.d.ts.map +1 -1
  255. package/package.json +12 -12
  256. package/skills/schedule-task/SKILL.md +115 -3
package/dist/tui/App.js CHANGED
@@ -19,7 +19,7 @@ import { ToolOutputViewer } from './ToolOutputViewer.js';
19
19
  import { join, relative } from 'node:path';
20
20
  import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, SKILL_TOOL_NAME, } from '@namzu/sdk';
21
21
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
22
- import { useCallback, useEffect, useRef, useState } from 'react';
22
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
23
23
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
24
24
  import { visibleProjectInstructionPath } from '../context/project-path.js';
25
25
  import { runtimeContextLabel } from '../context/runtime-message.js';
@@ -43,8 +43,15 @@ import { BrandHeader } from './BrandHeader.js';
43
43
  import { ChoicePicker } from './ChoicePicker.js';
44
44
  import { emptyPluginReport, pluginDetails, pluginOption, pluginStartupState } from './plugin-view.js';
45
45
  import { Composer, suggestionWindowSize, } from './Composer.js';
46
+ import { resolveComposerTriggers } from '../config/composer-triggers.js';
47
+ import { triggerContextTexts, triggerEffort } from './triggers/context-text.js';
48
+ import { transcriptTagLine } from './triggers/copy.js';
49
+ import { unavailable as triggerUnavailable } from './triggers/detect.js';
50
+ import { BUILTIN_TRIGGERS, compileRegistry, triggerDefinition, } from './triggers/registry.js';
51
+ import { describeComposerTriggers, setComposerTriggers } from './triggers/setting.js';
46
52
  import { ComposerFrame } from './ComposerFrame.js';
47
53
  import { EffortSlider, effortSliderLayout } from './EffortSlider.js';
54
+ import { HYPERMODE, HYPERMODE_SUMMARY, ORCHESTRATE_ALIAS_NOTICE, hypermodeEffort, hypermodeStopLabel, } from './hypermode.js';
48
55
  import { CopyPicker } from './CopyPicker.js';
49
56
  import { EditPromptPicker } from './EditPromptPicker.js';
50
57
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
@@ -415,11 +422,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
415
422
  // A session setting layered above effort, not a level a provider publishes
416
423
  // — see docs/cli/slash-commands.md's /effort section. In-memory and
417
424
  // per-session like reasoningEffort above; no preferences file involved.
418
- const [orchestrateMode, setOrchestrateModeState] = useState(false);
419
- const orchestrateModeRef = useRef(false);
420
- const setOrchestrateMode = useCallback((next) => {
421
- orchestrateModeRef.current = next;
422
- setOrchestrateModeState(next);
425
+ const [hypermode, setHypermodeState] = useState(false);
426
+ const hypermodeRef = useRef(false);
427
+ const setHypermode = useCallback((next) => {
428
+ hypermodeRef.current = next;
429
+ setHypermodeState(next);
423
430
  }, []);
424
431
  const [activeSkills, setActiveSkills] = useState([]);
425
432
  // Read when the session comes up rather than on every keystroke: the
@@ -983,7 +990,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
983
990
  ask: (question, signal) => scheduleLiveRef.current?.ask(question, signal) ?? Promise.resolve({ kind: 'skip' }),
984
991
  askPermission: (request) => scheduleLiveRef.current?.askPermission(request) ??
985
992
  Promise.resolve({ kind: 'reject', feedback: 'Nobody can answer yet.' }),
986
- submit: (text) => scheduleLiveRef.current?.submit(text),
993
+ submit: (text, createdBy) => scheduleLiveRef.current?.submit(text, createdBy),
987
994
  });
988
995
  }
989
996
  /**
@@ -1297,6 +1304,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1297
1304
  const learningRef = useRef({ generation: -1, proposed: false, skillUsed: false });
1298
1305
  /** `/skills save off|on` in this session, over whatever the config files say. */
1299
1306
  const skillSuggestOverrideRef = useRef(undefined);
1307
+ /** `/config triggers on|off` in this session, over whatever the config files say. */
1308
+ const [composerTriggersOverride, setComposerTriggersOverride] = useState();
1309
+ const composerTriggerSettings = useMemo(() => resolveComposerTriggers(ctx.composerTriggers), [ctx.composerTriggers]);
1310
+ const composerTriggerRegistry = useMemo(() => compileRegistry(BUILTIN_TRIGGERS, {
1311
+ arming: composerTriggerSettings.arming,
1312
+ locales: composerTriggerSettings.languages,
1313
+ }), [composerTriggerSettings]);
1314
+ const composerTriggersOn = composerTriggersOverride ?? composerTriggerSettings.enabled;
1315
+ /**
1316
+ * Whether the model can run `skill-creator` here, which save-as-skill
1317
+ * needs. Read once for the tag row; read again when a turn starts and when
1318
+ * its follow-up would run, in case the skills changed meanwhile.
1319
+ */
1320
+ const readSkillCreator = useCallback(() => {
1321
+ try {
1322
+ const creator = discoverSkills({
1323
+ cwd: ctx.cwd,
1324
+ ...(ctx.skills ? { config: ctx.skills } : {}),
1325
+ }).find((skill) => skill.name === SKILL_CREATOR_SKILL);
1326
+ return Boolean(creator && !creator.problem && creator.invocation !== 'operator');
1327
+ }
1328
+ catch {
1329
+ return false;
1330
+ }
1331
+ }, [ctx.cwd, ctx.skills]);
1332
+ const skillCreatorAvailable = useMemo(() => readSkillCreator(), [readSkillCreator]);
1333
+ /** The session as the triggers see it right now, for a check at a turn's start or end. */
1334
+ const triggerContextNow = useCallback(() => ({
1335
+ permissionMode: permissionModeRef.current,
1336
+ sessionHypermode: hypermodeRef.current,
1337
+ effortMenu: (session?.reasoningEffortLevels?.length ?? 0) > 0,
1338
+ // The interactive session always mounts `Agent` and the `schedule`
1339
+ // tool; only scheduled runs withhold them, and those never read
1340
+ // composer triggers.
1341
+ agentTool: true,
1342
+ skillCreator: readSkillCreator(),
1343
+ scheduleTool: true,
1344
+ }), [readSkillCreator, session]);
1345
+ /** A turn's hand-off to its after-turn triggers; assigned below, where the queue lives. */
1346
+ const triggerFollowUpsRef = useRef(null);
1300
1347
  const conversationLearning = useCallback(() => {
1301
1348
  if (learningRef.current.generation !== conversationGenRef.current) {
1302
1349
  learningRef.current = {
@@ -1613,22 +1660,28 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1613
1660
  pushMessage('system', `Reasoning: ${effort} (this session).`);
1614
1661
  }, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
1615
1662
  /**
1616
- * Turn the session's orchestrate mode on or off.
1663
+ * Turn the session's hypermode on or off.
1617
1664
  *
1618
1665
  * On: pins effort to the model's highest published level (last entry of
1619
1666
  * `reasoningEffortLevels`, which every provider publishes low-to-high) and
1620
- * strengthens delegation guidance for future turns via `SendOptions.orchestrate`
1667
+ * strengthens delegation guidance for future turns via `SendOptions.hypermode`
1621
1668
  * — see `applyReasoningEffort` above for the mirrored guard shape. When the
1622
1669
  * model publishes no exact menu, effort is left alone and the notice says
1623
1670
  * so explicitly rather than silently doing nothing.
1624
1671
  */
1625
- const applyOrchestrateMode = useCallback((enabled, selectedSession) => {
1672
+ /**
1673
+ * The effort in force before hypermode pinned the highest level, and the
1674
+ * level it pinned. Turning the mode off puts the effort back — unless the
1675
+ * operator chose another level meanwhile, which then stays.
1676
+ */
1677
+ const hypermodePinRef = useRef(null);
1678
+ const applyHypermode = useCallback((enabled, selectedSession) => {
1626
1679
  if (!session?.hasProvider) {
1627
- pushMessage('system', 'No active session — pick a provider before turning orchestrate mode on.');
1680
+ pushMessage('system', 'No active session — pick a provider before turning hypermode on.');
1628
1681
  return;
1629
1682
  }
1630
1683
  if (selectedSession !== undefined && selectedSession !== session) {
1631
- pushMessage('system', 'Orchestrate mode was not changed: the selected model is no longer active.');
1684
+ pushMessage('system', 'Hypermode was not changed: the selected model is no longer active.');
1632
1685
  return;
1633
1686
  }
1634
1687
  if (state !== 'idle' ||
@@ -1637,22 +1690,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1637
1690
  (queuedRef.current.length > 0 && selectedSession !== session) ||
1638
1691
  permissionResolveRef.current !== null ||
1639
1692
  compactingRef.current) {
1640
- pushMessage('system', 'Orchestrate mode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
1693
+ pushMessage('system', 'Hypermode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
1694
+ return;
1695
+ }
1696
+ if (!enabled && !hypermodeRef.current) {
1697
+ pushMessage('system', 'Hypermode is off.');
1641
1698
  return;
1642
1699
  }
1643
- setOrchestrateMode(enabled);
1700
+ setHypermode(enabled);
1644
1701
  if (!enabled) {
1645
- pushMessage('system', 'Orchestrate mode is off.');
1702
+ const pin = hypermodePinRef.current;
1703
+ hypermodePinRef.current = null;
1704
+ if (pin && reasoningEffortRef.current === pin.pinned) {
1705
+ setReasoningEffort(pin.before);
1706
+ pushMessage('system', `Hypermode is off — effort back to ${pin.before ?? 'the provider default'}.`);
1707
+ return;
1708
+ }
1709
+ pushMessage('system', 'Hypermode is off.');
1646
1710
  return;
1647
1711
  }
1648
- const highest = highestReasoningEffort(session.reasoningEffortLevels);
1649
- if (highest !== undefined) {
1650
- setReasoningEffort(highest);
1651
- pushMessage('system', `Orchestrate mode is on — effort pinned to ${highest} for ${session.modelSummary ?? 'this model'}, and delegation guidance is strengthened for this session.`);
1712
+ const pinned = hypermodeEffort(session.reasoningEffortLevels);
1713
+ if (pinned !== undefined) {
1714
+ // Turning it on twice keeps the effort from before the first time.
1715
+ if (!hypermodePinRef.current)
1716
+ hypermodePinRef.current = { before: reasoningEffortRef.current, pinned };
1717
+ else
1718
+ hypermodePinRef.current = { ...hypermodePinRef.current, pinned };
1719
+ setReasoningEffort(pinned);
1720
+ pushMessage('system', `Hypermode is on — effort pinned to ${pinned} for ${session.modelSummary ?? 'this model'}, and delegation guidance is strengthened for this session.`);
1652
1721
  return;
1653
1722
  }
1654
- pushMessage('system', `Orchestrate mode is on — ${session.modelSummary ?? 'this model'} does not publish an exact effort menu, so effort was left as is. Delegation guidance is still strengthened for this session.`);
1655
- }, [hasUnsettledTurn, pushMessage, session, setOrchestrateMode, setReasoningEffort, state]);
1723
+ pushMessage('system', `Hypermode is on — ${session.modelSummary ?? 'this model'} does not publish an exact effort menu, so effort was left as is. Delegation guidance is still strengthened for this session.`);
1724
+ }, [hasUnsettledTurn, pushMessage, session, setHypermode, setReasoningEffort, state]);
1656
1725
  const stepReasoningEffort = useCallback((direction) => {
1657
1726
  const levels = session?.reasoningEffortLevels;
1658
1727
  if (!session?.hasProvider || levels === undefined || levels.length === 0) {
@@ -2214,8 +2283,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2214
2283
  removeStoredCredential(value);
2215
2284
  return;
2216
2285
  }
2217
- if (picker.kind === 'reasoning-effort' && value === ORCHESTRATE_MODE_VALUE) {
2218
- applyOrchestrateMode(!orchestrateModeRef.current, picker.selectedSession);
2286
+ if (picker.kind === 'reasoning-effort' && value === HYPERMODE_VALUE) {
2287
+ applyHypermode(!hypermodeRef.current, picker.selectedSession);
2219
2288
  return;
2220
2289
  }
2221
2290
  applyReasoningEffort(value, picker.selectedSession);
@@ -2223,7 +2292,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2223
2292
  activateSkill,
2224
2293
  advanceQueueContinuation,
2225
2294
  appLifetime.signal,
2226
- applyOrchestrateMode,
2295
+ applyHypermode,
2227
2296
  applyPermissionMode,
2228
2297
  applyReasoningEffort,
2229
2298
  archiveCurrentConversation,
@@ -2546,11 +2615,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2546
2615
  // A picker-owned provider/model change is one state transition. Clear the
2547
2616
  // old model's effort selection before publishing the replacement session
2548
2617
  // or releasing any paused queue. Failed and superseded candidates returned
2549
- // above, so they leave the current session selection untouched. Orchestrate
2618
+ // above, so they leave the current session selection untouched. Hypermode
2550
2619
  // mode survives the switch: re-pin to the new model's highest published
2551
2620
  // level instead of clearing, exactly as it pinned when first turned on.
2552
2621
  if (signal !== undefined) {
2553
- setReasoningEffort(orchestrateModeRef.current ? highestReasoningEffort(s.reasoningEffortLevels) : undefined);
2622
+ const repinned = hypermodeRef.current ? hypermodeEffort(s.reasoningEffortLevels) : undefined;
2623
+ // A new model starts at its own default; that is what turning
2624
+ // hypermode off returns to now.
2625
+ hypermodePinRef.current = repinned ? { before: undefined, pinned: repinned } : null;
2626
+ setReasoningEffort(repinned);
2554
2627
  }
2555
2628
  // Re-hydration (a provider switch via /model) builds a second session;
2556
2629
  // without this the first one's tool-server child processes stay alive
@@ -2559,9 +2632,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2559
2632
  void previousSessionRef.current?.close();
2560
2633
  previousSessionRef.current = s;
2561
2634
  setSession(s);
2562
- // Orchestrate mode already re-pinned effort above; reopening this picker
2635
+ // Hypermode already re-pinned effort above; reopening this picker
2563
2636
  // would ask the operator to redo a choice the mode just made for them.
2564
- if (!orchestrateModeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
2637
+ if (!hypermodeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
2565
2638
  // Own input before publishing ready or releasing a paused queue. The
2566
2639
  // menu closes only after choosing an effort or keeping the new default.
2567
2640
  setSelectedChoice(0);
@@ -3961,6 +4034,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3961
4034
  ...question.options.map((option) => ({
3962
4035
  label: option.label,
3963
4036
  description: option.description ?? '',
4037
+ // A badge, not label text: the SDK already took any
4038
+ // "(Recommended)" the model wrote out of the label.
4039
+ ...(option.recommended === true ? { recommended: true } : {}),
3964
4040
  })),
3965
4041
  ...(question.allowFreeText
3966
4042
  ? [{ label: 'Something else…', description: 'Answer in your own words' }]
@@ -4433,7 +4509,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4433
4509
  if (closing)
4434
4510
  pushMessage('system', closing, false, '✻', undefined, theme.text.muted);
4435
4511
  }
4436
- offerSkillSuggestion(st);
4512
+ // A save the operator asked for in words takes the place of
4513
+ // the proposal to save it.
4514
+ if (!triggerFollowUpsRef.current?.(st, st.completed ? 'completed' : 'stopped'))
4515
+ offerSkillSuggestion(st);
4437
4516
  break;
4438
4517
  }
4439
4518
  case 'paused':
@@ -4445,6 +4524,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4445
4524
  st.outcome = 'stopped';
4446
4525
  st.queuePauseOutcome = 'paused';
4447
4526
  st.notification = { kind: 'turn-settled', outcome: 'stopped' };
4527
+ triggerFollowUpsRef.current?.(st, 'paused');
4448
4528
  if (event.handoff) {
4449
4529
  // Nothing failed and nothing needs approving: a tool needs the
4450
4530
  // operator to do something first. Continuing is one key.
@@ -4465,6 +4545,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4465
4545
  case 'error':
4466
4546
  closeAssistant();
4467
4547
  st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
4548
+ triggerFollowUpsRef.current?.(st, st.outcome);
4468
4549
  if (event.message !== 'aborted') {
4469
4550
  st.notification = { kind: 'turn-settled', outcome: 'failed' };
4470
4551
  pushMessage('system', describeTurnInterruption(event), false, '!');
@@ -4581,11 +4662,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4581
4662
  // Reserved before the turn begins, so everything that refers to this
4582
4663
  // turn — its goal-round authority above all — names it from the start.
4583
4664
  const turnId = generateTurnId();
4665
+ // The composer triggers the operator armed for this message, checked
4666
+ // again now: the mode or the session may have changed while it waited
4667
+ // in the queue. None of them changes the operator's words.
4668
+ const requestedTriggers = prompt.kind === 'human' ? (prompt.triggers ?? []) : [];
4669
+ const triggerContext = triggerContextNow();
4670
+ const turnTriggers = requestedTriggers.filter((id) => triggerUnavailable(id, triggerContext) === undefined);
4671
+ const pinnedEffort = triggerEffort(turnTriggers, session.reasoningEffortLevels);
4584
4672
  if (goalRound) {
4585
4673
  pushMessage('system', `Goal round ${goalRound.round} / ${goalRound.maxGoalRounds}`, false, '◎', [`Objective: ${goalRound.objective}`]);
4586
4674
  }
4587
4675
  else {
4588
4676
  pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
4677
+ const tagLine = transcriptTagLine(turnTriggers, {
4678
+ effort: pinnedEffort,
4679
+ steered: false,
4680
+ });
4681
+ if (tagLine)
4682
+ pushMessage('system', tagLine, false, '✦', undefined, theme.accent.trigger);
4683
+ for (const id of requestedTriggers) {
4684
+ const reason = triggerUnavailable(id, triggerContext);
4685
+ if (reason)
4686
+ pushMessage('system', `${triggerDefinition(id).label} · not applied: ${reason}`, false, '✧', undefined, theme.text.muted);
4687
+ }
4589
4688
  }
4590
4689
  // A plan is scoped to the request it was made for. Cleared here, at
4591
4690
  // the start of the next one, rather than when the previous turn
@@ -4617,6 +4716,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4617
4716
  startedInPlanMode: permissionModeRef.current === 'plan',
4618
4717
  }
4619
4718
  : {}),
4719
+ followUps: turnTriggers.filter((id) => triggerDefinition(id).scope === 'after-turn'),
4720
+ contextTexts: triggerContextTexts(turnTriggers),
4620
4721
  };
4621
4722
  liveStreamRef.current = st;
4622
4723
  // A skill activated with /skills <name> stays in the prompt of every
@@ -4792,7 +4893,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4792
4893
  activeTurnInboxRef.current = inbox;
4793
4894
  const turnPermissionMode = permissionModeRef.current;
4794
4895
  const turnReasoningEffort = reasoningEffortRef.current;
4795
- const turnOrchestrateMode = orchestrateModeRef.current;
4896
+ const turnHypermode = hypermodeRef.current;
4796
4897
  const turnLimits = resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current);
4797
4898
  // Always carry the guarded callback. `auto` and `strict` decide before
4798
4899
  // calling it in makeResumeHandler; retaining it is what lets a session
@@ -4840,8 +4941,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4840
4941
  // while this turn runs, and the change governs what follows.
4841
4942
  currentPermissionMode: () => permissionModeRef.current,
4842
4943
  limits: turnLimits,
4843
- ...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
4844
- ...(turnOrchestrateMode ? { orchestrate: true } : {}),
4944
+ // A trigger that pins the highest effort does so for this
4945
+ // turn only; the session's own setting is untouched.
4946
+ ...((pinnedEffort ?? turnReasoningEffort) !== undefined
4947
+ ? { effort: pinnedEffort ?? turnReasoningEffort }
4948
+ : {}),
4949
+ ...(turnHypermode ? { hypermode: true } : {}),
4950
+ // Read at every iteration, so an after-turn trigger a steer
4951
+ // binds to this turn is said from the next one on.
4952
+ hostContext: () => st.contextTexts ?? [],
4845
4953
  ...(goalRound ? { goalRound } : {}),
4846
4954
  // The mode above decides whether this callback is consulted.
4847
4955
  onPermission: askPermission,
@@ -5055,6 +5163,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5055
5163
  sendTerminalNotification,
5056
5164
  session,
5057
5165
  setQueuePause,
5166
+ triggerContextNow,
5058
5167
  wakeGoalDriver,
5059
5168
  ]);
5060
5169
  const selectModelIntent = useCallback(async (query) => {
@@ -5176,7 +5285,31 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5176
5285
  pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
5177
5286
  }
5178
5287
  }, [ctx.cwd, pushMessage, session]);
5179
- const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
5288
+ /**
5289
+ * Enter steered a message into the running turn. Its after-turn triggers
5290
+ * (save as skill) bind to that turn and are told to the model from its next
5291
+ * iteration; its turn triggers do not apply to a turn already running —
5292
+ * the tag row said so before Enter, and Tab would have queued it instead.
5293
+ */
5294
+ const bindSteeredTriggers = useCallback((ids) => {
5295
+ if (ids.length === 0)
5296
+ return;
5297
+ const live = liveStreamRef.current;
5298
+ const afterTurn = ids.filter((id) => triggerDefinition(id).scope === 'after-turn');
5299
+ const turnOnly = ids.filter((id) => triggerDefinition(id).scope === 'turn');
5300
+ if (live && afterTurn.length > 0) {
5301
+ live.followUps = [...new Set([...(live.followUps ?? []), ...afterTurn])];
5302
+ live.contextTexts = [
5303
+ ...new Set([...(live.contextTexts ?? []), ...triggerContextTexts(afterTurn)]),
5304
+ ];
5305
+ const line = transcriptTagLine(afterTurn, { effort: undefined, steered: true });
5306
+ if (line)
5307
+ pushMessage('system', line, false, '✦', undefined, theme.accent.trigger);
5308
+ }
5309
+ if (turnOnly.length > 0)
5310
+ pushMessage('system', `${turnOnly.map((id) => triggerDefinition(id).label).join(', ')} · not applied: Enter steered this message into the running turn. Tab queues a message as a new turn with it.`, false, '✧', undefined, theme.text.muted);
5311
+ }, [pushMessage]);
5312
+ const handleSubmit = useCallback((value, attachments, mode, meta) => {
5180
5313
  if (goalCommandInFlightRef.current) {
5181
5314
  pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
5182
5315
  return;
@@ -5200,8 +5333,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5200
5333
  }
5201
5334
  // The operator moved on; a parked turn is still there for /resume.
5202
5335
  setHandoffPark(null);
5203
- setHistory((prev) => [...prev, value]);
5204
- const selectionIntent = !attachments?.length ? parseModelSelectionIntent(value) : undefined;
5336
+ // A loop is not something the operator typed here, so it is not a line
5337
+ // Up should bring back.
5338
+ const fromLoop = meta.source === 'operator-loop' || meta.source === 'model-loop';
5339
+ if (!fromLoop)
5340
+ setHistory((prev) => [...prev, value]);
5341
+ // What the model wrote is never the operator acting. A loop the model
5342
+ // made (`session_loop`) fires this path on a timer, for up to a week,
5343
+ // after one review of its creation — so its text must not reach the
5344
+ // host-side meanings of `/`, `!`, `#` or a model switch: `!` runs on
5345
+ // the host outside the sandbox with no review, and `#` writes memory
5346
+ // every later turn reads. It is sent as the prompt it looks like.
5347
+ const operatorText = meta.source !== 'model-loop';
5348
+ const selectionIntent = operatorText && !attachments?.length ? parseModelSelectionIntent(value) : undefined;
5205
5349
  if (selectionIntent) {
5206
5350
  void selectModelIntent(selectionIntent.query);
5207
5351
  return;
@@ -5209,12 +5353,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5209
5353
  // `#` remembers, `!` runs — neither is a prompt. Both are the
5210
5354
  // operator acting directly, the way other coding agents spell it,
5211
5355
  // and both leave a row the model reads on its next turn.
5212
- if (value.startsWith('#') && value.slice(1).trim().length > 0) {
5356
+ if (operatorText && value.startsWith('#') && value.slice(1).trim().length > 0) {
5213
5357
  const note = value.slice(1).trim();
5214
5358
  void rememberProjectNote(note);
5215
5359
  return;
5216
5360
  }
5217
- const escaped = shellEscapeCommand(value);
5361
+ const escaped = operatorText ? shellEscapeCommand(value) : null;
5218
5362
  if (escaped !== null) {
5219
5363
  pushMessage('user', value);
5220
5364
  const rowId = pushMessage('tool', `! ${escaped}`, true, '…');
@@ -5235,13 +5379,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5235
5379
  });
5236
5380
  return;
5237
5381
  }
5382
+ // Composer triggers (`./triggers/`). Only the composer's own submit
5383
+ // carries them, so nothing a loop, a picker or the model sends can
5384
+ // arm one. A message that is nothing but a trigger with a command of
5385
+ // its own (`bunu skill olarak kaydet`) runs that command here, in this
5386
+ // same call, so history holds the operator's words once; a trigger
5387
+ // that needs a task and got none is not applied, and says so.
5388
+ let armedTriggers = meta.source === 'composer' ? meta.triggers.map((trigger) => trigger.id) : [];
5389
+ let dispatch = value;
5390
+ if (meta.source === 'composer' && meta.onlyTriggers && armedTriggers.length > 0) {
5391
+ const command = armedTriggers
5392
+ .map((id) => triggerDefinition(id).standalone)
5393
+ .find((standalone) => standalone !== undefined);
5394
+ const needTask = armedTriggers.filter((id) => triggerDefinition(id).standalone === undefined);
5395
+ if (needTask.length > 0)
5396
+ pushMessage('system', `${needTask.map((id) => triggerDefinition(id).label).join(', ')} · not applied: the message has no task. Put the task in the same message${needTask.includes('hypermode') ? ', e.g. “hypermode fix the flaky test”' : ''}.`, false, '✧', undefined, theme.text.muted);
5397
+ armedTriggers = [];
5398
+ if (!command)
5399
+ return;
5400
+ dispatch = command;
5401
+ }
5238
5402
  // What actually gets sent. A `prompt` action replaces it with text the
5239
5403
  // command composed, and then takes the ordinary send path below —
5240
5404
  // including the queue — so a command-driven turn is not a second way
5241
5405
  // to run one.
5242
- let outgoing = value;
5406
+ let outgoing = dispatch;
5243
5407
  let skillFlow = false;
5244
- const slash = runSlash(value, slashCtx, hostCommands);
5408
+ const slash = operatorText ? runSlash(dispatch, slashCtx, hostCommands) : null;
5245
5409
  if (slash) {
5246
5410
  switch (slash.kind) {
5247
5411
  case 'message':
@@ -5380,6 +5544,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5380
5544
  description: `${session?.webSearchSummary ?? 'Not resolved'} · view tools, workspace and usage.`,
5381
5545
  },
5382
5546
  { name: 'setup', label: 'Provider setup', description: 'Check installed CLIs and access; connect or install without leaving Namzu.' },
5547
+ {
5548
+ name: `config triggers ${composerTriggersOn ? 'off' : 'on'}`,
5549
+ label: 'Composer triggers',
5550
+ description: composerTriggersOn
5551
+ ? 'On · hypermode and save-as-skill words act for their message. Select to turn off.'
5552
+ : 'Off · words in a message are only words. Select to turn on.',
5553
+ },
5383
5554
  {
5384
5555
  name: 'config sources',
5385
5556
  label: 'Setting sources',
@@ -5397,7 +5568,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5397
5568
  description: command.description,
5398
5569
  disabledReason: command.problem,
5399
5570
  })),
5400
- windowSize: 7,
5571
+ windowSize: 8,
5401
5572
  });
5402
5573
  return;
5403
5574
  }
@@ -5631,7 +5802,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5631
5802
  const values = [undefined, ...levels];
5632
5803
  const current = reasoningEffortRef.current;
5633
5804
  setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
5634
- setChoicePicker(reasoningEffortPicker(session, current, false, orchestrateModeRef.current) ?? null);
5805
+ setChoicePicker(reasoningEffortPicker(session, current, false, hypermodeRef.current) ?? null);
5635
5806
  return;
5636
5807
  }
5637
5808
  case 'login':
@@ -6153,9 +6324,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6153
6324
  : 'Raw output mode off — rich transcript rendering restored.');
6154
6325
  return;
6155
6326
  }
6156
- case 'orchestrate-mode': {
6157
- const enabled = slash.enabled === 'toggle' ? !orchestrateModeRef.current : slash.enabled;
6158
- applyOrchestrateMode(enabled);
6327
+ case 'composer-triggers': {
6328
+ const profile = ctx.configDebug?.selectedProfile?.name;
6329
+ const cascade = { cwd: ctx.cwd, ...(profile ? { profile } : {}) };
6330
+ if (slash.setting === 'list') {
6331
+ pushMessage('system', describeComposerTriggers(composerTriggersOn, cascade));
6332
+ return;
6333
+ }
6334
+ const on = slash.setting === 'on';
6335
+ // The session follows the choice whatever the files say; the
6336
+ // message says whether a later start will too.
6337
+ setComposerTriggersOverride(on);
6338
+ pushMessage('system', setComposerTriggers(on, cascade));
6339
+ return;
6340
+ }
6341
+ case 'hypermode': {
6342
+ if (slash.via === 'orchestrate')
6343
+ pushMessage('system', ORCHESTRATE_ALIAS_NOTICE);
6344
+ const enabled = slash.enabled === 'toggle' ? !hypermodeRef.current : slash.enabled;
6345
+ applyHypermode(enabled);
6159
6346
  return;
6160
6347
  }
6161
6348
  case 'export-picker': {
@@ -6225,6 +6412,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6225
6412
  text: outgoing,
6226
6413
  ...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
6227
6414
  ...(skillFlow ? { skillFlow: true } : {}),
6415
+ ...(armedTriggers.length > 0 && !skillFlow ? { triggers: armedTriggers } : {}),
6228
6416
  };
6229
6417
  // `/skills save` and `/skills new` are turns of their own: steered into
6230
6418
  // a running turn, they would lose the flag that keeps the skill-making
@@ -6242,8 +6430,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6242
6430
  message: createUserMessage(expanded.sendText, prompt.attachments),
6243
6431
  attachedFiles: expanded.attached.length,
6244
6432
  queueBoundary: queuedRef.current.length,
6245
- }))
6433
+ })) {
6434
+ bindSteeredTriggers(armedTriggers);
6246
6435
  return;
6436
+ }
6247
6437
  }
6248
6438
  }
6249
6439
  enqueueQueued(prompt);
@@ -6251,6 +6441,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6251
6441
  activeSkills,
6252
6442
  advanceQueueContinuation,
6253
6443
  applyPermissionMode,
6444
+ bindSteeredTriggers,
6445
+ composerTriggersOn,
6254
6446
  conversationLearning,
6255
6447
  applyReasoningEffort,
6256
6448
  appLifetime,
@@ -6287,7 +6479,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6287
6479
  // picker is also used by review/export flows. Keep only this dispatch hop in
6288
6480
  // a ref so selecting a help row re-enters the one ordinary slash-command
6289
6481
  // path instead of growing a second command executor.
6290
- commandPickerSubmitRef.current = handleSubmit;
6482
+ commandPickerSubmitRef.current = (command) => handleSubmit(command, undefined, 'submit', { source: 'command-picker' });
6483
+ // A turn's after-turn triggers, when it ends. Save as skill runs exactly the
6484
+ // prompt `/skills save` sends, at the front of the queue, only when the
6485
+ // turn completed, did tool work, did not already save a skill, and saving
6486
+ // is still possible now. Otherwise one row says why, and `/skills save`
6487
+ // still works. Returns whether the turn had one, so the proposal to save
6488
+ // it is not also printed.
6489
+ triggerFollowUpsRef.current = (st, ending) => {
6490
+ const followUps = st.followUps ?? [];
6491
+ st.followUps = [];
6492
+ if (!followUps.includes('save-skill'))
6493
+ return false;
6494
+ const reason = ending === 'cancelled'
6495
+ ? 'the turn was cancelled'
6496
+ : ending === 'failed'
6497
+ ? 'the turn failed'
6498
+ : ending === 'paused'
6499
+ ? 'the turn paused'
6500
+ : ending === 'stopped'
6501
+ ? 'the turn stopped before it finished'
6502
+ : !st.learning || st.learning.steps < 1
6503
+ ? 'the turn did no tool work'
6504
+ : st.learning.savedSkill
6505
+ ? 'the turn already saved a skill'
6506
+ : triggerUnavailable('save-skill', triggerContextNow());
6507
+ if (reason) {
6508
+ pushMessage('system', `save as skill · not run: ${reason} · /skills save runs it anyway`, false, '✧', undefined, theme.text.muted);
6509
+ return true;
6510
+ }
6511
+ // The proposal was taken up, as `/skills save` records it.
6512
+ conversationLearning().proposed = true;
6513
+ const home = resolveNamzuHome();
6514
+ const ledger = readSuggestionLedger(home);
6515
+ if (ledger.unanswered > 0)
6516
+ writeSuggestionLedger(home, { ...ledger, unanswered: 0 });
6517
+ replaceQueued([
6518
+ { kind: 'human', text: learnSkillPrompt(), skillFlow: true },
6519
+ ...queuedRef.current,
6520
+ ]);
6521
+ pushMessage('system', 'save as skill · running /skills save for this turn', false, '✦', undefined, theme.accent.trigger);
6522
+ return true;
6523
+ };
6291
6524
  saveSkillNotifyRef.current = () => sendTerminalNotification({ kind: 'approval-required' });
6292
6525
  scheduleLiveRef.current = {
6293
6526
  ...(scheduleLiveRef.current?.model ? { model: scheduleLiveRef.current.model } : {}),
@@ -6297,7 +6530,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6297
6530
  },
6298
6531
  ask: askQuestion,
6299
6532
  askPermission: onPermission,
6300
- submit: (text) => handleSubmit(text),
6533
+ submit: (text, createdBy) => handleSubmit(text, undefined, 'submit', {
6534
+ source: createdBy === 'operator' ? 'operator-loop' : 'model-loop',
6535
+ }),
6301
6536
  };
6302
6537
  // Scheduled work since the TUI last looked: one line, after the first
6303
6538
  // paint, never blocking it. Loops fire when a turn ends, and on their timer.
@@ -7292,6 +7527,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7292
7527
  : `Session: ${permissionSource}`
7293
7528
  : undefined;
7294
7529
  const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
7530
+ const highestEffort = highestReasoningEffort(session?.reasoningEffortLevels);
7531
+ const hypermodeLevel = hypermodeEffort(session?.reasoningEffortLevels);
7532
+ const composerTriggers = composerTriggersOn
7533
+ ? {
7534
+ registry: composerTriggerRegistry,
7535
+ suggest: composerTriggerSettings.suggest,
7536
+ context: {
7537
+ permissionMode: displayedPermissionMode,
7538
+ sessionHypermode: hypermode,
7539
+ effortMenu: highestEffort !== undefined,
7540
+ agentTool: true,
7541
+ skillCreator: skillCreatorAvailable,
7542
+ scheduleTool: true,
7543
+ },
7544
+ ...(highestEffort ? { highestEffort } : {}),
7545
+ ...(hypermodeLevel ? { hypermodeEffort: hypermodeLevel } : {}),
7546
+ }
7547
+ : undefined;
7295
7548
  // True exactly when the phase ternary below renders its final branch — the
7296
7549
  // normal composer view, as opposed to provider setup or a lifecycle
7297
7550
  // picker/prompt. The agent rail is a feature of that view: computing this
@@ -7332,7 +7585,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7332
7585
  agentSurface === null &&
7333
7586
  outputViewer === null &&
7334
7587
  choicePicker?.kind === 'reasoning-effort' &&
7335
- effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2), highest: choicePicker.options.at(-2)?.label === 'default' ? undefined : choicePicker.options.at(-2)?.label })) : 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, ...(orchestrateMode ? { mode: 'orchestrate' } : {}), focus: phase === 'ready' &&
7588
+ effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2) })) : 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, ...(hypermode ? { mode: HYPERMODE } : {}), focus: phase === 'ready' &&
7336
7589
  state !== 'awaiting-permission' &&
7337
7590
  !compacting &&
7338
7591
  externalEditorRequest === null &&
@@ -7360,7 +7613,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7360
7613
  ? queuePause.outcome === 'paused'
7361
7614
  ? 'held after a resumable turn paused; wait for recovery, change model, or send a message to release it'
7362
7615
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
7363
- : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
7616
+ : 'sending when ready', queuedTriggerLabels(queued)] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
7364
7617
  phase !== 'ready' ||
7365
7618
  state === 'awaiting-permission' ||
7366
7619
  compacting ||
@@ -7381,7 +7634,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7381
7634
  (handoffPark !== null && state === 'idle'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
7382
7635
  ...command,
7383
7636
  description: command.unavailable?.(slashCtx) ?? command.description,
7384
- })), mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, orchestrate: orchestrateMode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
7637
+ })), mentionCandidates: mentionCandidates, history: history, ...(composerTriggers ? { triggers: composerTriggers } : {}), turnActive: !compacting && (state === 'thinking' || state === 'tool') })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, hypermode: hypermode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
7385
7638
  permission === null &&
7386
7639
  agentSurface === null &&
7387
7640
  outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
@@ -7596,25 +7849,36 @@ function permissionPicker(mode, approvedAll, more = false) {
7596
7849
  };
7597
7850
  }
7598
7851
  /**
7599
- * The picker's sentinel for the orchestrate-mode row, below the rule.
7852
+ * The picker's sentinel for the hypermode row, below the rule.
7600
7853
  *
7601
7854
  * A plain string outside the `ReasoningEffort` union by construction — never
7602
7855
  * added to any model's published menu — so it can share the same picker
7603
7856
  * without ever being mistaken for a level `/effort <token>` could select.
7604
7857
  */
7605
- const ORCHESTRATE_MODE_VALUE = 'orchestrate';
7858
+ const HYPERMODE_VALUE = 'hypermode';
7859
+ /** ` · ✦ save as skill` when queued messages carry composer triggers, so the queue line says so. */
7860
+ function queuedTriggerLabels(queued) {
7861
+ const labels = new Set();
7862
+ for (const prompt of queued) {
7863
+ if (prompt.kind !== 'human')
7864
+ continue;
7865
+ for (const id of prompt.triggers ?? [])
7866
+ labels.add(triggerDefinition(id).label);
7867
+ }
7868
+ return labels.size > 0 ? ` · ✦ ${[...labels].join(', ')}` : '';
7869
+ }
7606
7870
  /** Last entry of a low-to-high menu every provider publishes in that order; `undefined` when none is known. */
7607
7871
  function highestReasoningEffort(levels) {
7608
7872
  return levels && levels.length > 0 ? levels[levels.length - 1] : undefined;
7609
7873
  }
7610
- function reasoningEffortPicker(session, current, afterModelSelection = false, orchestrateOn = false) {
7874
+ function reasoningEffortPicker(session, current, afterModelSelection = false, hypermodeOn = false) {
7611
7875
  const levels = session.reasoningEffortLevels;
7612
7876
  if (!session.hasProvider || levels === undefined)
7613
7877
  return undefined;
7614
7878
  const effortValues = [undefined, ...levels];
7615
7879
  const values = [
7616
7880
  ...effortValues,
7617
- ORCHESTRATE_MODE_VALUE,
7881
+ HYPERMODE_VALUE,
7618
7882
  ];
7619
7883
  return {
7620
7884
  kind: 'reasoning-effort',
@@ -7635,12 +7899,11 @@ function reasoningEffortPicker(session, current, afterModelSelection = false, or
7635
7899
  })),
7636
7900
  {
7637
7901
  // Below a rule, visually apart from the levels above: a session
7638
- // setting, not a sixth level the provider published.
7639
- label: 'orchestrate',
7640
- description: orchestrateOn
7641
- ? 'On · highest level and delegate by default'
7642
- : 'Off · highest level and delegate by default',
7643
- current: orchestrateOn,
7902
+ // setting, not a sixth level the provider published. The label
7903
+ // names the level it pins, then the mode.
7904
+ label: hypermodeStopLabel(hypermodeEffort(levels)),
7905
+ description: `${hypermodeOn ? 'On' : 'Off'} · ${HYPERMODE_SUMMARY}`,
7906
+ current: hypermodeOn,
7644
7907
  ruleBefore: true,
7645
7908
  },
7646
7909
  ],