longer-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/README.zh-CN.md +227 -0
  4. package/agent_templates/executor/agent.yaml +22 -0
  5. package/agent_templates/executor/system_prompt.md +17 -0
  6. package/agent_templates/explorer/agent.yaml +13 -0
  7. package/agent_templates/explorer/system_prompt.md +19 -0
  8. package/agent_templates/main/agent.yaml +7 -0
  9. package/agent_templates/main/system_prompt.md +45 -0
  10. package/configExample.yaml +83 -0
  11. package/dist/agents/agent.d.ts +79 -0
  12. package/dist/agents/agent.d.ts.map +1 -0
  13. package/dist/agents/agent.js +156 -0
  14. package/dist/agents/agent.js.map +1 -0
  15. package/dist/agents/tool-loop.d.ts +140 -0
  16. package/dist/agents/tool-loop.d.ts.map +1 -0
  17. package/dist/agents/tool-loop.js +465 -0
  18. package/dist/agents/tool-loop.js.map +1 -0
  19. package/dist/ask.d.ts +81 -0
  20. package/dist/ask.d.ts.map +1 -0
  21. package/dist/ask.js +34 -0
  22. package/dist/ask.js.map +1 -0
  23. package/dist/auth/openai-oauth.d.ts +66 -0
  24. package/dist/auth/openai-oauth.d.ts.map +1 -0
  25. package/dist/auth/openai-oauth.js +640 -0
  26. package/dist/auth/openai-oauth.js.map +1 -0
  27. package/dist/cli.d.ts +14 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +254 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/commands.d.ts +118 -0
  32. package/dist/commands.d.ts.map +1 -0
  33. package/dist/commands.js +862 -0
  34. package/dist/commands.js.map +1 -0
  35. package/dist/config.d.ts +130 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +648 -0
  38. package/dist/config.js.map +1 -0
  39. package/dist/context-rendering.d.ts +69 -0
  40. package/dist/context-rendering.d.ts.map +1 -0
  41. package/dist/context-rendering.js +250 -0
  42. package/dist/context-rendering.js.map +1 -0
  43. package/dist/document-projection.d.ts +12 -0
  44. package/dist/document-projection.d.ts.map +1 -0
  45. package/dist/document-projection.js +75 -0
  46. package/dist/document-projection.js.map +1 -0
  47. package/dist/ephemeral-log.d.ts +15 -0
  48. package/dist/ephemeral-log.d.ts.map +1 -0
  49. package/dist/ephemeral-log.js +173 -0
  50. package/dist/ephemeral-log.js.map +1 -0
  51. package/dist/file-attach.d.ts +89 -0
  52. package/dist/file-attach.d.ts.map +1 -0
  53. package/dist/file-attach.js +571 -0
  54. package/dist/file-attach.js.map +1 -0
  55. package/dist/index.d.ts +29 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +43 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/init-wizard.d.ts +13 -0
  60. package/dist/init-wizard.d.ts.map +1 -0
  61. package/dist/init-wizard.js +328 -0
  62. package/dist/init-wizard.js.map +1 -0
  63. package/dist/log-entry.d.ts +104 -0
  64. package/dist/log-entry.d.ts.map +1 -0
  65. package/dist/log-entry.js +292 -0
  66. package/dist/log-entry.js.map +1 -0
  67. package/dist/log-projection.d.ts +73 -0
  68. package/dist/log-projection.d.ts.map +1 -0
  69. package/dist/log-projection.js +651 -0
  70. package/dist/log-projection.js.map +1 -0
  71. package/dist/mcp-client.d.ts +55 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +402 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-selection.d.ts +16 -0
  76. package/dist/model-selection.d.ts.map +1 -0
  77. package/dist/model-selection.js +181 -0
  78. package/dist/model-selection.js.map +1 -0
  79. package/dist/network-retry.d.ts +38 -0
  80. package/dist/network-retry.d.ts.map +1 -0
  81. package/dist/network-retry.js +140 -0
  82. package/dist/network-retry.js.map +1 -0
  83. package/dist/persistence.d.ts +104 -0
  84. package/dist/persistence.d.ts.map +1 -0
  85. package/dist/persistence.js +644 -0
  86. package/dist/persistence.js.map +1 -0
  87. package/dist/primitives/context.d.ts +29 -0
  88. package/dist/primitives/context.d.ts.map +1 -0
  89. package/dist/primitives/context.js +85 -0
  90. package/dist/primitives/context.js.map +1 -0
  91. package/dist/progress.d.ts +51 -0
  92. package/dist/progress.d.ts.map +1 -0
  93. package/dist/progress.js +229 -0
  94. package/dist/progress.js.map +1 -0
  95. package/dist/provider-presets.d.ts +34 -0
  96. package/dist/provider-presets.d.ts.map +1 -0
  97. package/dist/provider-presets.js +181 -0
  98. package/dist/provider-presets.js.map +1 -0
  99. package/dist/providers/anthropic.d.ts +32 -0
  100. package/dist/providers/anthropic.d.ts.map +1 -0
  101. package/dist/providers/anthropic.js +450 -0
  102. package/dist/providers/anthropic.js.map +1 -0
  103. package/dist/providers/base.d.ts +135 -0
  104. package/dist/providers/base.d.ts.map +1 -0
  105. package/dist/providers/base.js +104 -0
  106. package/dist/providers/base.js.map +1 -0
  107. package/dist/providers/glm.d.ts +18 -0
  108. package/dist/providers/glm.d.ts.map +1 -0
  109. package/dist/providers/glm.js +59 -0
  110. package/dist/providers/glm.js.map +1 -0
  111. package/dist/providers/kimi.d.ts +23 -0
  112. package/dist/providers/kimi.d.ts.map +1 -0
  113. package/dist/providers/kimi.js +89 -0
  114. package/dist/providers/kimi.js.map +1 -0
  115. package/dist/providers/minimax.d.ts +20 -0
  116. package/dist/providers/minimax.d.ts.map +1 -0
  117. package/dist/providers/minimax.js +192 -0
  118. package/dist/providers/minimax.js.map +1 -0
  119. package/dist/providers/openai-chat.d.ts +33 -0
  120. package/dist/providers/openai-chat.d.ts.map +1 -0
  121. package/dist/providers/openai-chat.js +543 -0
  122. package/dist/providers/openai-chat.js.map +1 -0
  123. package/dist/providers/openai-responses.d.ts +26 -0
  124. package/dist/providers/openai-responses.d.ts.map +1 -0
  125. package/dist/providers/openai-responses.js +443 -0
  126. package/dist/providers/openai-responses.js.map +1 -0
  127. package/dist/providers/openrouter.d.ts +24 -0
  128. package/dist/providers/openrouter.d.ts.map +1 -0
  129. package/dist/providers/openrouter.js +177 -0
  130. package/dist/providers/openrouter.js.map +1 -0
  131. package/dist/providers/registry.d.ts +7 -0
  132. package/dist/providers/registry.d.ts.map +1 -0
  133. package/dist/providers/registry.js +38 -0
  134. package/dist/providers/registry.js.map +1 -0
  135. package/dist/security/path.d.ts +51 -0
  136. package/dist/security/path.d.ts.map +1 -0
  137. package/dist/security/path.js +187 -0
  138. package/dist/security/path.js.map +1 -0
  139. package/dist/security/sensitive-files.d.ts +3 -0
  140. package/dist/security/sensitive-files.d.ts.map +1 -0
  141. package/dist/security/sensitive-files.js +41 -0
  142. package/dist/security/sensitive-files.js.map +1 -0
  143. package/dist/session.d.ts +446 -0
  144. package/dist/session.d.ts.map +1 -0
  145. package/dist/session.js +4595 -0
  146. package/dist/session.js.map +1 -0
  147. package/dist/settings.d.ts +46 -0
  148. package/dist/settings.d.ts.map +1 -0
  149. package/dist/settings.js +134 -0
  150. package/dist/settings.js.map +1 -0
  151. package/dist/show-context.d.ts +35 -0
  152. package/dist/show-context.d.ts.map +1 -0
  153. package/dist/show-context.js +320 -0
  154. package/dist/show-context.js.map +1 -0
  155. package/dist/skills/loader.d.ts +49 -0
  156. package/dist/skills/loader.d.ts.map +1 -0
  157. package/dist/skills/loader.js +166 -0
  158. package/dist/skills/loader.js.map +1 -0
  159. package/dist/summarize-context.d.ts +29 -0
  160. package/dist/summarize-context.d.ts.map +1 -0
  161. package/dist/summarize-context.js +247 -0
  162. package/dist/summarize-context.js.map +1 -0
  163. package/dist/templates/loader.d.ts +104 -0
  164. package/dist/templates/loader.d.ts.map +1 -0
  165. package/dist/templates/loader.js +514 -0
  166. package/dist/templates/loader.js.map +1 -0
  167. package/dist/tools/basic.d.ts +29 -0
  168. package/dist/tools/basic.d.ts.map +1 -0
  169. package/dist/tools/basic.js +2079 -0
  170. package/dist/tools/basic.js.map +1 -0
  171. package/dist/tools/comm.d.ts +17 -0
  172. package/dist/tools/comm.d.ts.map +1 -0
  173. package/dist/tools/comm.js +192 -0
  174. package/dist/tools/comm.js.map +1 -0
  175. package/dist/tools/web-fetch.d.ts +11 -0
  176. package/dist/tools/web-fetch.d.ts.map +1 -0
  177. package/dist/tools/web-fetch.js +237 -0
  178. package/dist/tools/web-fetch.js.map +1 -0
  179. package/dist/tools/web-search.d.ts +24 -0
  180. package/dist/tools/web-search.d.ts.map +1 -0
  181. package/dist/tools/web-search.js +51 -0
  182. package/dist/tools/web-search.js.map +1 -0
  183. package/dist/tui/app.d.ts +35 -0
  184. package/dist/tui/app.d.ts.map +1 -0
  185. package/dist/tui/app.js +1042 -0
  186. package/dist/tui/app.js.map +1 -0
  187. package/dist/tui/checkbox-picker.d.ts +35 -0
  188. package/dist/tui/checkbox-picker.d.ts.map +1 -0
  189. package/dist/tui/checkbox-picker.js +85 -0
  190. package/dist/tui/checkbox-picker.js.map +1 -0
  191. package/dist/tui/command-picker.d.ts +31 -0
  192. package/dist/tui/command-picker.d.ts.map +1 -0
  193. package/dist/tui/command-picker.js +113 -0
  194. package/dist/tui/command-picker.js.map +1 -0
  195. package/dist/tui/components/ask-panel.d.ts +21 -0
  196. package/dist/tui/components/ask-panel.d.ts.map +1 -0
  197. package/dist/tui/components/ask-panel.js +81 -0
  198. package/dist/tui/components/ask-panel.js.map +1 -0
  199. package/dist/tui/components/conversation-panel.d.ts +68 -0
  200. package/dist/tui/components/conversation-panel.d.ts.map +1 -0
  201. package/dist/tui/components/conversation-panel.js +611 -0
  202. package/dist/tui/components/conversation-panel.js.map +1 -0
  203. package/dist/tui/components/input-panel.d.ts +27 -0
  204. package/dist/tui/components/input-panel.d.ts.map +1 -0
  205. package/dist/tui/components/input-panel.js +725 -0
  206. package/dist/tui/components/input-panel.js.map +1 -0
  207. package/dist/tui/components/logo-panel.d.ts +14 -0
  208. package/dist/tui/components/logo-panel.d.ts.map +1 -0
  209. package/dist/tui/components/logo-panel.js +37 -0
  210. package/dist/tui/components/logo-panel.js.map +1 -0
  211. package/dist/tui/components/plan-panel.d.ts +10 -0
  212. package/dist/tui/components/plan-panel.d.ts.map +1 -0
  213. package/dist/tui/components/plan-panel.js +8 -0
  214. package/dist/tui/components/plan-panel.js.map +1 -0
  215. package/dist/tui/components/status-bar.d.ts +24 -0
  216. package/dist/tui/components/status-bar.d.ts.map +1 -0
  217. package/dist/tui/components/status-bar.js +80 -0
  218. package/dist/tui/components/status-bar.js.map +1 -0
  219. package/dist/tui/input/editor-state.d.ts +22 -0
  220. package/dist/tui/input/editor-state.d.ts.map +1 -0
  221. package/dist/tui/input/editor-state.js +157 -0
  222. package/dist/tui/input/editor-state.js.map +1 -0
  223. package/dist/tui/input/keymap.d.ts +3 -0
  224. package/dist/tui/input/keymap.d.ts.map +1 -0
  225. package/dist/tui/input/keymap.js +72 -0
  226. package/dist/tui/input/keymap.js.map +1 -0
  227. package/dist/tui/input/paste-slots.d.ts +17 -0
  228. package/dist/tui/input/paste-slots.d.ts.map +1 -0
  229. package/dist/tui/input/paste-slots.js +46 -0
  230. package/dist/tui/input/paste-slots.js.map +1 -0
  231. package/dist/tui/input/paste.d.ts +15 -0
  232. package/dist/tui/input/paste.d.ts.map +1 -0
  233. package/dist/tui/input/paste.js +35 -0
  234. package/dist/tui/input/paste.js.map +1 -0
  235. package/dist/tui/input/protocol.d.ts +9 -0
  236. package/dist/tui/input/protocol.d.ts.map +1 -0
  237. package/dist/tui/input/protocol.js +387 -0
  238. package/dist/tui/input/protocol.js.map +1 -0
  239. package/dist/tui/input/sanitize.d.ts +6 -0
  240. package/dist/tui/input/sanitize.d.ts.map +1 -0
  241. package/dist/tui/input/sanitize.js +20 -0
  242. package/dist/tui/input/sanitize.js.map +1 -0
  243. package/dist/tui/input/types.d.ts +18 -0
  244. package/dist/tui/input/types.d.ts.map +1 -0
  245. package/dist/tui/input/types.js +2 -0
  246. package/dist/tui/input/types.js.map +1 -0
  247. package/dist/tui/launch.d.ts +23 -0
  248. package/dist/tui/launch.d.ts.map +1 -0
  249. package/dist/tui/launch.js +104 -0
  250. package/dist/tui/launch.js.map +1 -0
  251. package/dist/tui/theme.d.ts +20 -0
  252. package/dist/tui/theme.d.ts.map +1 -0
  253. package/dist/tui/theme.js +29 -0
  254. package/dist/tui/theme.js.map +1 -0
  255. package/dist/tui/types.d.ts +136 -0
  256. package/dist/tui/types.d.ts.map +1 -0
  257. package/dist/tui/types.js +9 -0
  258. package/dist/tui/types.js.map +1 -0
  259. package/package.json +76 -0
  260. package/prompts/sections/agents_md.md +23 -0
  261. package/prompts/sections/important_log.md +16 -0
  262. package/prompts/sections/system_mechanisms.md +18 -0
  263. package/prompts/tools/apply_patch.md +31 -0
  264. package/prompts/tools/ask.md +18 -0
  265. package/prompts/tools/bash.md +13 -0
  266. package/prompts/tools/bash_background.md +9 -0
  267. package/prompts/tools/bash_output.md +9 -0
  268. package/prompts/tools/check_status.md +3 -0
  269. package/prompts/tools/diff.md +5 -0
  270. package/prompts/tools/edit_file.md +11 -0
  271. package/prompts/tools/glob.md +7 -0
  272. package/prompts/tools/grep.md +20 -0
  273. package/prompts/tools/kill_agent.md +3 -0
  274. package/prompts/tools/kill_shell.md +5 -0
  275. package/prompts/tools/list_dir.md +5 -0
  276. package/prompts/tools/plan.md +252 -0
  277. package/prompts/tools/read_file.md +9 -0
  278. package/prompts/tools/show_context.md +12 -0
  279. package/prompts/tools/skill.md +7 -0
  280. package/prompts/tools/spawn_agent.md +195 -0
  281. package/prompts/tools/summarize_context.md +122 -0
  282. package/prompts/tools/test.md +5 -0
  283. package/prompts/tools/wait.md +17 -0
  284. package/prompts/tools/web_fetch.md +9 -0
  285. package/prompts/tools/web_search.md +5 -0
  286. package/prompts/tools/write_file.md +11 -0
  287. package/skills/.staging/.gitkeep +0 -0
  288. package/skills/explain-code/SKILL.md +15 -0
  289. package/skills/skill-manager/SKILL.md +83 -0
@@ -0,0 +1,1042 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Root TUI application component.
4
+ *
5
+ * Manages conversation state, session turn execution with streaming,
6
+ * Ctrl+C handling, and delegates rendering to child components.
7
+ *
8
+ * Layout: pure vertical — LogoPanel → ConversationPanel → InputPanel → StatusBar.
9
+ * Activity state (thinking / tool calling / waiting) is shown in the
10
+ * StatusBar alongside model name and context token count.
11
+ *
12
+ * Within a single turn, reasoning and text segments are tracked
13
+ * independently so they interleave in chronological order (not pinned
14
+ * to the top). Sub-agent tool activity is folded into compact rollup
15
+ * blocks in the conversation flow.
16
+ *
17
+ * Key bindings:
18
+ * Enter Send message
19
+ * Ctrl+N Insert newline
20
+ * Ctrl+G Toggle markdown rendered/raw mode
21
+ * Ctrl+C Cancel current turn (first press) / exit (second press)
22
+ * Ctrl+L Clear progress lines
23
+ * Ctrl+Y Copy last assistant reply to clipboard
24
+ */
25
+ import { useState, useCallback, useRef, useEffect } from "react";
26
+ import { Box, Text, useApp, useStdin } from "ink";
27
+ import { execSync } from "node:child_process";
28
+ import { StringDecoder } from "node:string_decoder";
29
+ import { LogoPanel } from "./components/logo-panel.js";
30
+ import { StatusBar } from "./components/status-bar.js";
31
+ import { ConversationPanel } from "./components/conversation-panel.js";
32
+ import { AskPanel } from "./components/ask-panel.js";
33
+ import { PlanPanel } from "./components/plan-panel.js";
34
+ import { InputPanel } from "./components/input-panel.js";
35
+ import { InputProtocolParser } from "./input/protocol.js";
36
+ import { mapInputEventToCommand } from "./input/keymap.js";
37
+ import { withValueAndCursor, insertText, moveLeft, moveRight, moveWordLeft, moveWordRight, moveHome, moveEnd, deleteBackward, deleteForward, deleteWordBackward, deleteWordForward, deleteToLineStart, deleteToLineEnd, } from "./input/editor-state.js";
38
+ import { saveLog } from "../persistence.js";
39
+ import { isCommandExitSignal } from "../commands.js";
40
+ import { formatDisplayModelName } from "../config.js";
41
+ import { projectToTuiEntries } from "../log-projection.js";
42
+ // ------------------------------------------------------------------
43
+ // Goodbye messages
44
+ // ------------------------------------------------------------------
45
+ const GOODBYE_MESSAGES = [
46
+ "Bye!", "Goodbye!", "See you later!", "Until next time!",
47
+ "Take care!", "Happy coding!", "Catch you later!",
48
+ "Peace out!", "So long!", "Off I go!", "Later, gator!",
49
+ ];
50
+ const CUSTOM_EMPTY_HINT = 'Custom answer is empty. Please enter an answer first, or choose "Discuss further" instead.';
51
+ // ------------------------------------------------------------------
52
+ // Clipboard helper
53
+ // ------------------------------------------------------------------
54
+ function copyToClipboard(text) {
55
+ try {
56
+ execSync("pbcopy", { input: text, timeout: 2000 });
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ // ------------------------------------------------------------------
64
+ // Inline editor helper (reuses pure functions from editor-state.ts)
65
+ // ------------------------------------------------------------------
66
+ function applyInlineEdit(value, cursor, event) {
67
+ if (event.type === "insert" && typeof event.text === "string") {
68
+ return insertText(withValueAndCursor(value, cursor, null), event.text);
69
+ }
70
+ if (event.type !== "key")
71
+ return null;
72
+ const cmd = mapInputEventToCommand(event);
73
+ if (!cmd)
74
+ return null;
75
+ const state = withValueAndCursor(value, cursor, null);
76
+ switch (cmd) {
77
+ case "move_left": return moveLeft(state);
78
+ case "move_right": return moveRight(state);
79
+ case "move_word_left": return moveWordLeft(state);
80
+ case "move_word_right": return moveWordRight(state);
81
+ case "move_home": return moveHome(state);
82
+ case "move_end": return moveEnd(state);
83
+ case "delete_backward": return deleteBackward(state);
84
+ case "delete_forward": return deleteForward(state);
85
+ case "delete_word_backward": return deleteWordBackward(state);
86
+ case "delete_word_forward": return deleteWordForward(state);
87
+ case "delete_to_line_start": return deleteToLineStart(state);
88
+ case "delete_to_line_end": return deleteToLineEnd(state);
89
+ default: return null;
90
+ }
91
+ }
92
+ export function App({ session, commandRegistry, store, onProgressCallback, }) {
93
+ const { exit } = useApp();
94
+ const { stdin, setRawMode, isRawModeSupported } = useStdin();
95
+ const [entries, setEntries] = useState(projectToTuiEntries([...(session.log ?? [])]));
96
+ const [processing, setProcessing] = useState(false);
97
+ const [inputHint, setInputHint] = useState(null);
98
+ const [markdownMode, setMarkdownMode] = useState("rendered");
99
+ const [hideProgress, setHideProgress] = useState(false);
100
+ // ---- Status bar state ----
101
+ const [activityPhase, setActivityPhase] = useState("idle");
102
+ const [activityToolName, setActivityToolName] = useState();
103
+ const [statusError, setStatusError] = useState(false);
104
+ const [contextTokens, setContextTokens] = useState(0);
105
+ const [cacheReadTokens, setCacheReadTokens] = useState(0);
106
+ const [pendingAsk, setPendingAsk] = useState(typeof session.getPendingAsk === "function" ? session.getPendingAsk() : null);
107
+ const [askError, setAskError] = useState(null);
108
+ const [askSelectionIndex, setAskSelectionIndex] = useState(0);
109
+ // Agent question state
110
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
111
+ const [questionAnswers, setQuestionAnswers] = useState(new Map());
112
+ const [customInputMode, setCustomInputMode] = useState(false);
113
+ const [noteInputMode, setNoteInputMode] = useState(false);
114
+ // Shared inline editor for custom input and note input (mutually exclusive)
115
+ const [inlineEditor, setInlineEditor] = useState({ value: "", cursor: 0 });
116
+ // Per-option note drafts, keyed by "questionIndex-optionIndex"
117
+ const [optionNotes, setOptionNotes] = useState(new Map());
118
+ // Review mode: show summary of all answers before submitting (multi-question only)
119
+ const [reviewMode, setReviewMode] = useState(false);
120
+ // Plan panel state
121
+ const [planCheckpoints, setPlanCheckpoints] = useState(null);
122
+ const cancelledRef = useRef(false);
123
+ const lastCtrlCRef = useRef(0);
124
+ const abortControllerRef = useRef(null);
125
+ const inputPanelRef = useRef(null);
126
+ const shortcutParserRef = useRef(new InputProtocolParser());
127
+ const shortcutDecoderRef = useRef(new StringDecoder("utf8"));
128
+ const inputHintTimerRef = useRef(null);
129
+ const markdownModeInitializedRef = useRef(false);
130
+ const runTurnRef = useRef(null);
131
+ const runManualSummarizeRef = useRef(null);
132
+ const runManualCompactRef = useRef(null);
133
+ // Raw mode
134
+ useEffect(() => {
135
+ if (!isRawModeSupported)
136
+ return;
137
+ setRawMode(true);
138
+ return () => {
139
+ setRawMode(false);
140
+ };
141
+ }, [isRawModeSupported, setRawMode]);
142
+ useEffect(() => {
143
+ const syncFromLog = () => {
144
+ const projected = projectToTuiEntries([...(session.log ?? [])]);
145
+ setEntries(hideProgress
146
+ ? projected.filter((e) => e.kind !== "progress" &&
147
+ e.kind !== "sub_agent_rollup" &&
148
+ e.kind !== "sub_agent_done")
149
+ : projected);
150
+ setPendingAsk(session.getPendingAsk?.() ?? null);
151
+ setContextTokens(session.lastInputTokens);
152
+ setCacheReadTokens(session.lastCacheReadTokens ?? 0);
153
+ };
154
+ syncFromLog();
155
+ if (typeof session.subscribeLog !== "function")
156
+ return;
157
+ // Throttle log listener to limit TUI refresh rate (min 200ms between renders)
158
+ let lastCallTime = 0;
159
+ let pendingTimer = null;
160
+ const throttledSync = () => {
161
+ const now = Date.now();
162
+ const elapsed = now - lastCallTime;
163
+ if (elapsed >= 200) {
164
+ lastCallTime = now;
165
+ syncFromLog();
166
+ }
167
+ else if (!pendingTimer) {
168
+ pendingTimer = setTimeout(() => {
169
+ pendingTimer = null;
170
+ lastCallTime = Date.now();
171
+ syncFromLog();
172
+ }, 200 - elapsed);
173
+ }
174
+ };
175
+ const unsub = session.subscribeLog(throttledSync);
176
+ return () => {
177
+ unsub();
178
+ if (pendingTimer)
179
+ clearTimeout(pendingTimer);
180
+ };
181
+ }, [session, hideProgress]);
182
+ // ------------------------------------------------------------------
183
+ // Input hint management
184
+ // ------------------------------------------------------------------
185
+ const clearInputHint = useCallback(() => {
186
+ if (inputHintTimerRef.current) {
187
+ clearTimeout(inputHintTimerRef.current);
188
+ inputHintTimerRef.current = null;
189
+ }
190
+ setInputHint(null);
191
+ }, []);
192
+ const showInputHint = useCallback((message, durationMs = 2000) => {
193
+ if (inputHintTimerRef.current) {
194
+ clearTimeout(inputHintTimerRef.current);
195
+ inputHintTimerRef.current = null;
196
+ }
197
+ setInputHint(message);
198
+ inputHintTimerRef.current = setTimeout(() => {
199
+ inputHintTimerRef.current = null;
200
+ setInputHint(null);
201
+ }, durationMs);
202
+ }, []);
203
+ useEffect(() => {
204
+ return () => {
205
+ if (inputHintTimerRef.current) {
206
+ clearTimeout(inputHintTimerRef.current);
207
+ inputHintTimerRef.current = null;
208
+ }
209
+ };
210
+ }, []);
211
+ useEffect(() => {
212
+ if (!markdownModeInitializedRef.current) {
213
+ markdownModeInitializedRef.current = true;
214
+ return;
215
+ }
216
+ showInputHint(markdownMode === "raw" ? "Markdown raw: ON" : "Markdown raw: OFF");
217
+ }, [markdownMode, showInputHint]);
218
+ useEffect(() => {
219
+ setAskSelectionIndex(0);
220
+ setCurrentQuestionIndex(0);
221
+ setQuestionAnswers(new Map());
222
+ setCustomInputMode(false);
223
+ setNoteInputMode(false);
224
+ setInlineEditor({ value: "", cursor: 0 });
225
+ setOptionNotes(new Map());
226
+ setReviewMode(false);
227
+ }, [pendingAsk?.id]);
228
+ // ------------------------------------------------------------------
229
+ // Auto-save
230
+ // ------------------------------------------------------------------
231
+ const autoSave = useCallback(() => {
232
+ if (!store)
233
+ return;
234
+ try {
235
+ if (typeof session.getLogForPersistence === "function" && store.sessionDir) {
236
+ const { meta, entries } = session.getLogForPersistence();
237
+ if (meta.turnCount === 0)
238
+ return; // Don't save empty sessions
239
+ saveLog(store.sessionDir, meta, entries);
240
+ }
241
+ }
242
+ catch {
243
+ // Auto-save failed silently
244
+ }
245
+ }, [session, store]);
246
+ const runPendingTurn = useCallback(async () => {
247
+ if (typeof session.resumePendingTurn !== "function") {
248
+ setAskError("Current session does not support resuming pending asks.");
249
+ return;
250
+ }
251
+ cancelledRef.current = false;
252
+ const controller = new AbortController();
253
+ abortControllerRef.current = controller;
254
+ setProcessing(true);
255
+ setActivityPhase("working");
256
+ setActivityToolName(undefined);
257
+ setStatusError(false);
258
+ try {
259
+ await session.resumePendingTurn({ signal: controller.signal });
260
+ if (cancelledRef.current || controller.signal.aborted) {
261
+ autoSave();
262
+ return;
263
+ }
264
+ setActivityPhase("idle");
265
+ setActivityToolName(undefined);
266
+ setContextTokens(session.lastInputTokens);
267
+ setCacheReadTokens(session.lastCacheReadTokens ?? 0);
268
+ setPendingAsk(session.getPendingAsk?.() ?? null);
269
+ autoSave();
270
+ }
271
+ catch (err) {
272
+ if (cancelledRef.current || controller.signal.aborted) {
273
+ autoSave();
274
+ return;
275
+ }
276
+ const msg = err instanceof Error ? err.message : String(err);
277
+ session.appendErrorMessage?.(msg, "resume_pending_turn");
278
+ setStatusError(true);
279
+ }
280
+ finally {
281
+ abortControllerRef.current = null;
282
+ inputPanelRef.current?.resetTurnPasteCounter();
283
+ if (!cancelledRef.current) {
284
+ setProcessing(false);
285
+ }
286
+ }
287
+ }, [session, autoSave]);
288
+ // Wire incremental save callback into Session
289
+ useEffect(() => {
290
+ session.onSaveRequest = autoSave;
291
+ return () => { session.onSaveRequest = undefined; };
292
+ }, [session, autoSave]);
293
+ const performExit = useCallback(async () => {
294
+ clearInputHint();
295
+ autoSave();
296
+ try {
297
+ await session.close();
298
+ }
299
+ catch {
300
+ // ignore close failures during shutdown
301
+ }
302
+ const msg = GOODBYE_MESSAGES[Math.floor(Math.random() * GOODBYE_MESSAGES.length)];
303
+ if (isRawModeSupported) {
304
+ setRawMode(false);
305
+ }
306
+ try {
307
+ process.stdout.write(`\n${msg}\n`);
308
+ }
309
+ catch {
310
+ console.log(msg);
311
+ }
312
+ exit();
313
+ }, [clearInputHint, autoSave, session, isRawModeSupported, setRawMode, exit]);
314
+ // ------------------------------------------------------------------
315
+ // Command context builder
316
+ // ------------------------------------------------------------------
317
+ const buildCommandContext = useCallback(() => {
318
+ return {
319
+ session,
320
+ store: store ?? undefined,
321
+ commandRegistry,
322
+ showMessage: (msg) => {
323
+ if (typeof session.appendStatusMessage === "function") {
324
+ session.appendStatusMessage(msg);
325
+ }
326
+ else {
327
+ showInputHint(msg, 2500);
328
+ }
329
+ },
330
+ autoSave,
331
+ resetUiState: () => {
332
+ cancelledRef.current = false;
333
+ setProcessing(false);
334
+ // Reset token count on /new
335
+ setContextTokens(0);
336
+ setCacheReadTokens(0);
337
+ setActivityPhase("idle");
338
+ setActivityToolName(undefined);
339
+ setStatusError(false);
340
+ setPendingAsk(null);
341
+ setAskError(null);
342
+ setHideProgress(false);
343
+ },
344
+ exit: performExit,
345
+ onTurnRequested: (content) => {
346
+ runTurnRef.current?.(content);
347
+ },
348
+ onManualSummarizeRequested: (instruction) => {
349
+ runManualSummarizeRef.current?.(instruction);
350
+ },
351
+ onManualCompactRequested: (instruction) => {
352
+ runManualCompactRef.current?.(instruction);
353
+ },
354
+ };
355
+ }, [session, store, commandRegistry, autoSave, performExit, showInputHint]);
356
+ // ------------------------------------------------------------------
357
+ // Progress callback (streaming)
358
+ // ------------------------------------------------------------------
359
+ const handleProgress = useCallback((event) => {
360
+ if (cancelledRef.current)
361
+ return;
362
+ const hasSubAgentId = event.extra?.["sub_agent_id"] !== undefined;
363
+ // ---- Status bar activity updates (primary agent only) ----
364
+ if (!hasSubAgentId) {
365
+ switch (event.action) {
366
+ case "reasoning_chunk":
367
+ setActivityPhase("thinking");
368
+ setActivityToolName(undefined);
369
+ break;
370
+ case "text_chunk":
371
+ setActivityPhase("generating");
372
+ setActivityToolName(undefined);
373
+ break;
374
+ case "tool_call":
375
+ setActivityPhase("tool_calling");
376
+ setActivityToolName(event.extra?.["tool"] ?? undefined);
377
+ break;
378
+ case "agent_no_reply":
379
+ setActivityPhase("waiting");
380
+ setActivityToolName(undefined);
381
+ break;
382
+ case "agent_end":
383
+ setContextTokens(session.lastInputTokens);
384
+ setCacheReadTokens(session.lastCacheReadTokens ?? 0);
385
+ break;
386
+ case "token_update":
387
+ // Real-time token count update after each provider call
388
+ setContextTokens(event.extra?.["input_tokens"] ?? session.lastInputTokens);
389
+ setCacheReadTokens(event.extra?.["cache_read_tokens"] ?? 0);
390
+ break;
391
+ }
392
+ }
393
+ // ---- Conversation entry routing ----
394
+ if (event.action === "ask_requested") {
395
+ const ask = event.extra?.["ask"] ?? session.getPendingAsk?.() ?? null;
396
+ setPendingAsk(ask);
397
+ setAskError(null);
398
+ setActivityPhase("waiting");
399
+ setActivityToolName(undefined);
400
+ return;
401
+ }
402
+ if (event.action === "ask_resolved") {
403
+ setPendingAsk(session.getPendingAsk?.() ?? null);
404
+ setAskError(null);
405
+ return;
406
+ }
407
+ // ---- Plan panel events ----
408
+ if (event.action === "plan_submit" || event.action === "plan_update") {
409
+ const cps = event.extra?.["checkpoints"];
410
+ if (cps)
411
+ setPlanCheckpoints(cps);
412
+ return;
413
+ }
414
+ if (event.action === "plan_finish") {
415
+ setPlanCheckpoints(null);
416
+ return;
417
+ }
418
+ }, [session]);
419
+ // Register progress callback on mount
420
+ useEffect(() => {
421
+ onProgressCallback(handleProgress);
422
+ }, [handleProgress, onProgressCallback]);
423
+ // ------------------------------------------------------------------
424
+ // Turn execution
425
+ // ------------------------------------------------------------------
426
+ const runTurn = useCallback(async (userInput) => {
427
+ cancelledRef.current = false;
428
+ const controller = new AbortController();
429
+ abortControllerRef.current = controller;
430
+ setProcessing(true);
431
+ setActivityPhase("working");
432
+ setActivityToolName(undefined);
433
+ setStatusError(false);
434
+ try {
435
+ await session.turn(userInput, { signal: controller.signal });
436
+ if (cancelledRef.current || controller.signal.aborted) {
437
+ autoSave();
438
+ return;
439
+ }
440
+ setActivityPhase("idle");
441
+ setActivityToolName(undefined);
442
+ setContextTokens(session.lastInputTokens);
443
+ setCacheReadTokens(session.lastCacheReadTokens ?? 0);
444
+ setPendingAsk(session.getPendingAsk?.() ?? null);
445
+ autoSave();
446
+ }
447
+ catch (err) {
448
+ if (cancelledRef.current || controller.signal.aborted) {
449
+ autoSave();
450
+ return;
451
+ }
452
+ const msg = err instanceof Error ? err.message : String(err);
453
+ session.appendErrorMessage?.(msg, "turn");
454
+ setStatusError(true);
455
+ setPendingAsk(session.getPendingAsk?.() ?? null);
456
+ }
457
+ finally {
458
+ abortControllerRef.current = null;
459
+ inputPanelRef.current?.resetTurnPasteCounter();
460
+ if (!cancelledRef.current) {
461
+ setProcessing(false);
462
+ }
463
+ }
464
+ }, [session, autoSave]);
465
+ runTurnRef.current = runTurn;
466
+ const runManualSummarize = useCallback(async (instruction) => {
467
+ if (typeof session.runManualSummarize !== "function") {
468
+ session.appendErrorMessage?.("Current session does not support /summarize.", "command");
469
+ return;
470
+ }
471
+ cancelledRef.current = false;
472
+ const controller = new AbortController();
473
+ abortControllerRef.current = controller;
474
+ setProcessing(true);
475
+ setActivityPhase("working");
476
+ setActivityToolName(undefined);
477
+ setStatusError(false);
478
+ try {
479
+ await session.runManualSummarize(instruction, { signal: controller.signal });
480
+ if (cancelledRef.current || controller.signal.aborted) {
481
+ autoSave();
482
+ return;
483
+ }
484
+ setActivityPhase("idle");
485
+ setActivityToolName(undefined);
486
+ setContextTokens(session.lastInputTokens);
487
+ setCacheReadTokens(session.lastCacheReadTokens ?? 0);
488
+ setPendingAsk(session.getPendingAsk?.() ?? null);
489
+ autoSave();
490
+ }
491
+ catch (err) {
492
+ if (cancelledRef.current || controller.signal.aborted) {
493
+ autoSave();
494
+ return;
495
+ }
496
+ const msg = err instanceof Error ? err.message : String(err);
497
+ session.appendErrorMessage?.(msg, "manual_summarize");
498
+ setStatusError(true);
499
+ }
500
+ finally {
501
+ abortControllerRef.current = null;
502
+ inputPanelRef.current?.resetTurnPasteCounter();
503
+ if (!cancelledRef.current) {
504
+ setProcessing(false);
505
+ }
506
+ }
507
+ }, [session, autoSave]);
508
+ runManualSummarizeRef.current = runManualSummarize;
509
+ const runManualCompact = useCallback(async (instruction) => {
510
+ if (typeof session.runManualCompact !== "function") {
511
+ session.appendErrorMessage?.("Current session does not support /compact.", "command");
512
+ return;
513
+ }
514
+ cancelledRef.current = false;
515
+ const controller = new AbortController();
516
+ abortControllerRef.current = controller;
517
+ setProcessing(true);
518
+ setActivityPhase("working");
519
+ setActivityToolName(undefined);
520
+ setStatusError(false);
521
+ try {
522
+ await session.runManualCompact(instruction, { signal: controller.signal });
523
+ if (cancelledRef.current || controller.signal.aborted) {
524
+ autoSave();
525
+ return;
526
+ }
527
+ setActivityPhase("idle");
528
+ setActivityToolName(undefined);
529
+ setContextTokens(session.lastInputTokens);
530
+ setCacheReadTokens(session.lastCacheReadTokens ?? 0);
531
+ setPendingAsk(session.getPendingAsk?.() ?? null);
532
+ autoSave();
533
+ }
534
+ catch (err) {
535
+ if (cancelledRef.current || controller.signal.aborted) {
536
+ autoSave();
537
+ return;
538
+ }
539
+ const msg = err instanceof Error ? err.message : String(err);
540
+ session.appendErrorMessage?.(msg, "manual_compact");
541
+ setStatusError(true);
542
+ }
543
+ finally {
544
+ abortControllerRef.current = null;
545
+ inputPanelRef.current?.resetTurnPasteCounter();
546
+ if (!cancelledRef.current) {
547
+ setProcessing(false);
548
+ }
549
+ }
550
+ }, [session, autoSave]);
551
+ runManualCompactRef.current = runManualCompact;
552
+ const resolveAgentQuestion = useCallback((answersOverride, notesOverride) => {
553
+ if (!pendingAsk || pendingAsk.kind !== "agent_question")
554
+ return;
555
+ const questions = pendingAsk.payload["questions"] ?? [];
556
+ const effectiveAnswers = answersOverride ?? questionAnswers;
557
+ const effectiveNotes = notesOverride ?? optionNotes;
558
+ for (let i = 0; i < questions.length; i++) {
559
+ if (!effectiveAnswers.has(i)) {
560
+ setReviewMode(false);
561
+ setCurrentQuestionIndex(i);
562
+ setAskSelectionIndex(0);
563
+ setAskError("Please answer all questions before continuing.");
564
+ return;
565
+ }
566
+ }
567
+ const answers = [];
568
+ for (let i = 0; i < questions.length; i++) {
569
+ const qa = effectiveAnswers.get(i);
570
+ const agentOptions = questions[i].options;
571
+ const selectedOption = agentOptions[qa.optionIndex];
572
+ if (!selectedOption) {
573
+ setReviewMode(false);
574
+ setCurrentQuestionIndex(i);
575
+ setAskSelectionIndex(0);
576
+ setAskError("Selected answer is out of range.");
577
+ return;
578
+ }
579
+ // Look up note for this question's selected option
580
+ const note = effectiveNotes.get(`${i}-${qa.optionIndex}`) || undefined;
581
+ answers.push({
582
+ questionIndex: i,
583
+ selectedOptionIndex: qa.optionIndex,
584
+ answerText: selectedOption.kind === "custom_input"
585
+ ? (qa.customText ?? "")
586
+ : selectedOption.label,
587
+ note,
588
+ });
589
+ }
590
+ const decision = { answers };
591
+ try {
592
+ if (typeof session.resolveAgentQuestionAsk === "function") {
593
+ session.resolveAgentQuestionAsk(pendingAsk.id, decision);
594
+ }
595
+ setPendingAsk(session.getPendingAsk?.() ?? null);
596
+ setAskError(null);
597
+ autoSave();
598
+ if (session.hasPendingTurnToResume?.()) {
599
+ void runPendingTurn();
600
+ }
601
+ }
602
+ catch (err) {
603
+ setAskError(err instanceof Error ? err.message : String(err));
604
+ }
605
+ }, [pendingAsk, questionAnswers, optionNotes, session, autoSave, runPendingTurn]);
606
+ // Helper: confirm current question's highlighted option into questionAnswers.
607
+ // Returns the updated map (avoids stale-closure from async setState).
608
+ const confirmCurrentQuestion = useCallback((sel, extra) => {
609
+ const next = new Map(questionAnswers);
610
+ next.set(currentQuestionIndex, { optionIndex: sel, ...extra });
611
+ setQuestionAnswers(next);
612
+ return next;
613
+ }, [questionAnswers, currentQuestionIndex]);
614
+ // Helper: submit or enter review mode
615
+ const submitOrReview = useCallback((updated) => {
616
+ if (!pendingAsk || pendingAsk.kind !== "agent_question")
617
+ return;
618
+ const questions = pendingAsk.payload["questions"] ?? [];
619
+ const firstMissing = questions.findIndex((_, idx) => !updated.has(idx));
620
+ if (firstMissing !== -1) {
621
+ setReviewMode(false);
622
+ setCurrentQuestionIndex(firstMissing);
623
+ setAskSelectionIndex(0);
624
+ setAskError("Please answer all questions before reviewing.");
625
+ return;
626
+ }
627
+ if (questions.length > 1) {
628
+ // Multi-question: enter review mode
629
+ setAskError(null);
630
+ setReviewMode(true);
631
+ }
632
+ else {
633
+ // Single question: submit directly
634
+ resolveAgentQuestion(updated, optionNotes);
635
+ }
636
+ }, [pendingAsk, optionNotes, resolveAgentQuestion]);
637
+ const resolveSelectedPendingAsk = useCallback(() => {
638
+ if (!pendingAsk)
639
+ return;
640
+ // Handle agent_question: confirm current question option
641
+ if (pendingAsk.kind === "agent_question") {
642
+ const questions = pendingAsk.payload["questions"] ?? [];
643
+ const q = questions[currentQuestionIndex];
644
+ if (!q)
645
+ return;
646
+ const selectedOption = q.options[askSelectionIndex];
647
+ if (!selectedOption)
648
+ return;
649
+ if (selectedOption.kind === "custom_input") {
650
+ if (customInputMode) {
651
+ const customText = inlineEditor.value.trim();
652
+ if (!customText) {
653
+ showInputHint(CUSTOM_EMPTY_HINT, 5000);
654
+ return;
655
+ }
656
+ // Confirm custom input
657
+ const updated = confirmCurrentQuestion(askSelectionIndex, { customText });
658
+ setCustomInputMode(false);
659
+ setInlineEditor({ value: "", cursor: 0 });
660
+ if (currentQuestionIndex < questions.length - 1) {
661
+ setCurrentQuestionIndex((prev) => prev + 1);
662
+ setAskSelectionIndex(0);
663
+ }
664
+ else {
665
+ submitOrReview(updated);
666
+ }
667
+ }
668
+ else {
669
+ const existing = questionAnswers.get(currentQuestionIndex);
670
+ setCustomInputMode(true);
671
+ setInlineEditor({
672
+ value: existing?.optionIndex === askSelectionIndex ? (existing.customText ?? "") : "",
673
+ cursor: existing?.optionIndex === askSelectionIndex ? (existing.customText ?? "").length : 0,
674
+ });
675
+ }
676
+ return;
677
+ }
678
+ // Normal option selected (including "Discuss further")
679
+ const updated = confirmCurrentQuestion(askSelectionIndex);
680
+ if (currentQuestionIndex < questions.length - 1) {
681
+ setCurrentQuestionIndex((prev) => prev + 1);
682
+ setAskSelectionIndex(0);
683
+ }
684
+ else {
685
+ submitOrReview(updated);
686
+ }
687
+ return;
688
+ }
689
+ setAskError(`Unsupported ask kind: ${pendingAsk.kind}`);
690
+ }, [pendingAsk, askSelectionIndex, currentQuestionIndex, customInputMode, inlineEditor, confirmCurrentQuestion, optionNotes, resolveAgentQuestion, submitOrReview]);
691
+ // ------------------------------------------------------------------
692
+ // Input handling
693
+ // ------------------------------------------------------------------
694
+ const handleSubmit = useCallback((input) => {
695
+ clearInputHint();
696
+ if (pendingAsk) {
697
+ if (pendingAsk.kind === "agent_question") {
698
+ showInputHint("Use ↑/↓ to select options, ←/→ to navigate questions, Enter to confirm.", 2500);
699
+ return false;
700
+ }
701
+ showInputHint(`Unsupported ask kind: ${pendingAsk.kind}`, 2500);
702
+ return true;
703
+ }
704
+ if (processing) {
705
+ if (!input.trim())
706
+ return false;
707
+ // Enqueue message for delivery via check_status
708
+ if (typeof session.deliverMessage === "function") {
709
+ session.deliverMessage("user", input);
710
+ session.appendStatusMessage?.(`[Queued user message]\n${input}`, "queued_user_message");
711
+ showInputHint("Message queued for delivery.");
712
+ return true;
713
+ }
714
+ showInputHint("Assistant is replying. Enter is temporarily disabled.");
715
+ return false;
716
+ }
717
+ // Slash command handling
718
+ if (input.startsWith("/")) {
719
+ const parts = input.split(/\s+/, 2);
720
+ const cmdName = parts[0];
721
+ const cmdArgs = input.slice(cmdName.length).trim();
722
+ const cmd = commandRegistry.lookup(cmdName);
723
+ if (cmd) {
724
+ const ctx = buildCommandContext();
725
+ cmd.handler(ctx, cmdArgs).then(() => {
726
+ setPendingAsk(session.getPendingAsk?.() ?? null);
727
+ setAskError(null);
728
+ }).catch((err) => {
729
+ if (isCommandExitSignal(err)) {
730
+ void performExit();
731
+ return;
732
+ }
733
+ const message = err instanceof Error ? err.message : String(err);
734
+ session.appendErrorMessage?.(`Command failed (${cmdName}): ${message}`, "command");
735
+ });
736
+ }
737
+ else {
738
+ session.appendErrorMessage?.(`Unknown command: ${cmdName}. Type /help for available commands.`, "command");
739
+ }
740
+ return true;
741
+ }
742
+ runTurn(input);
743
+ return true;
744
+ }, [
745
+ processing,
746
+ pendingAsk,
747
+ commandRegistry,
748
+ runTurn,
749
+ runPendingTurn,
750
+ buildCommandContext,
751
+ clearInputHint,
752
+ showInputHint,
753
+ performExit,
754
+ session,
755
+ ]);
756
+ // ------------------------------------------------------------------
757
+ // Ctrl+C / Ctrl+L / Ctrl+Y handling
758
+ // ------------------------------------------------------------------
759
+ const handleCtrlC = useCallback(() => {
760
+ if (inputPanelRef.current?.dismissOverlay()) {
761
+ clearInputHint();
762
+ return;
763
+ }
764
+ const now = Date.now();
765
+ if (now - lastCtrlCRef.current < 2000) {
766
+ if (processing) {
767
+ const decision = session.requestTurnInterrupt
768
+ ? session.requestTurnInterrupt()
769
+ : (session.cancelCurrentTurn?.(), { accepted: true });
770
+ if (decision.accepted) {
771
+ cancelledRef.current = true;
772
+ abortControllerRef.current?.abort();
773
+ }
774
+ }
775
+ void performExit();
776
+ return;
777
+ }
778
+ lastCtrlCRef.current = now;
779
+ if (!processing && inputPanelRef.current?.getValue()?.trim()) {
780
+ clearInputHint();
781
+ inputPanelRef.current.clear();
782
+ return;
783
+ }
784
+ if (processing) {
785
+ const decision = session.requestTurnInterrupt
786
+ ? session.requestTurnInterrupt()
787
+ : (session.cancelCurrentTurn?.(), { accepted: true });
788
+ if (!decision.accepted) {
789
+ if (decision.reason === "compact_in_progress") {
790
+ showInputHint("Interrupt is disabled during compact phase");
791
+ }
792
+ return;
793
+ }
794
+ cancelledRef.current = true;
795
+ abortControllerRef.current?.abort();
796
+ setProcessing(false);
797
+ setActivityPhase("idle");
798
+ setActivityToolName(undefined);
799
+ clearInputHint();
800
+ }
801
+ else {
802
+ showInputHint("Press Ctrl+C again to exit");
803
+ }
804
+ }, [
805
+ processing,
806
+ clearInputHint,
807
+ showInputHint,
808
+ performExit,
809
+ ]);
810
+ const handleCtrlL = useCallback(() => {
811
+ setHideProgress((prev) => {
812
+ const next = !prev;
813
+ showInputHint(next ? "Progress lines hidden" : "Progress lines shown");
814
+ return next;
815
+ });
816
+ }, [showInputHint]);
817
+ const handleCtrlY = useCallback(() => {
818
+ const lastReply = [...entries]
819
+ .reverse()
820
+ .find((e) => e.kind === "assistant");
821
+ if (lastReply) {
822
+ if (copyToClipboard(lastReply.text)) {
823
+ showInputHint("Copied last reply!");
824
+ }
825
+ else {
826
+ showInputHint("Copy failed");
827
+ }
828
+ }
829
+ else {
830
+ showInputHint("No reply to copy");
831
+ }
832
+ }, [entries, showInputHint]);
833
+ const handleCtrlG = useCallback(() => {
834
+ setMarkdownMode((prev) => (prev === "rendered" ? "raw" : "rendered"));
835
+ }, []);
836
+ useEffect(() => {
837
+ if (!stdin)
838
+ return;
839
+ const onData = (data) => {
840
+ const chunk = typeof data === "string" ? data : shortcutDecoderRef.current.write(data);
841
+ const events = shortcutParserRef.current.push(chunk);
842
+ for (const event of events) {
843
+ // --- Review mode for multi-question ask ---
844
+ if (pendingAsk?.kind === "agent_question" && reviewMode) {
845
+ if (event.type !== "key")
846
+ continue;
847
+ if (event.key === "enter") {
848
+ // Confirm and submit
849
+ resolveAgentQuestion(questionAnswers, optionNotes);
850
+ continue;
851
+ }
852
+ if (event.key === "escape") {
853
+ // Go back to last question
854
+ setReviewMode(false);
855
+ continue;
856
+ }
857
+ // Number keys 1-9: jump to that question for editing
858
+ const numMatch = /^[1-9]$/.exec(event.key);
859
+ if (numMatch) {
860
+ const qNum = parseInt(numMatch[0], 10) - 1;
861
+ const questions = pendingAsk.payload["questions"] ?? [];
862
+ if (qNum < questions.length) {
863
+ setReviewMode(false);
864
+ setCurrentQuestionIndex(qNum);
865
+ const existing = questionAnswers.get(qNum);
866
+ setAskSelectionIndex(existing?.optionIndex ?? 0);
867
+ }
868
+ }
869
+ continue;
870
+ }
871
+ // --- Inline editor input (custom input / note input) ---
872
+ if (pendingAsk?.kind === "agent_question" && (customInputMode || noteInputMode)) {
873
+ // Enter = confirm, Escape = cancel — handle before editor
874
+ if (event.type === "key" && event.key === "enter") {
875
+ if (noteInputMode) {
876
+ // Save note + auto-confirm the highlighted option (Bug 1 fix)
877
+ const noteText = inlineEditor.value.trim();
878
+ const noteKey = `${currentQuestionIndex}-${askSelectionIndex}`;
879
+ setOptionNotes((prev) => {
880
+ const next = new Map(prev);
881
+ if (noteText) {
882
+ next.set(noteKey, noteText);
883
+ }
884
+ else {
885
+ next.delete(noteKey);
886
+ }
887
+ return next;
888
+ });
889
+ // Also confirm the option that the note was added to
890
+ confirmCurrentQuestion(askSelectionIndex);
891
+ setNoteInputMode(false);
892
+ setInlineEditor({ value: "", cursor: 0 });
893
+ }
894
+ else {
895
+ // customInputMode — confirm via resolveSelectedPendingAsk
896
+ resolveSelectedPendingAsk();
897
+ }
898
+ continue;
899
+ }
900
+ if (event.type === "key" && event.key === "escape") {
901
+ if (noteInputMode) {
902
+ setNoteInputMode(false);
903
+ }
904
+ if (customInputMode) {
905
+ setCustomInputMode(false);
906
+ }
907
+ setInlineEditor({ value: "", cursor: 0 });
908
+ continue;
909
+ }
910
+ // Delegate to inline editor (movement, deletion, insertion)
911
+ const result = applyInlineEdit(inlineEditor.value, inlineEditor.cursor, event);
912
+ if (result)
913
+ setInlineEditor({ value: result.value, cursor: result.cursor });
914
+ continue;
915
+ }
916
+ if (event.type !== "key")
917
+ continue;
918
+ if (pendingAsk) {
919
+ if (pendingAsk.kind === "agent_question") {
920
+ const questions = pendingAsk.payload["questions"] ?? [];
921
+ const q = questions[currentQuestionIndex];
922
+ const totalOpts = q?.options?.length ?? 0;
923
+ const agentOptionCount = q?.options?.filter((opt) => !opt.systemAdded).length ?? 0;
924
+ // --- Tab to add/edit note (only on agent options) ---
925
+ if (event.key === "tab" && askSelectionIndex < agentOptionCount) {
926
+ const noteKey = `${currentQuestionIndex}-${askSelectionIndex}`;
927
+ const existing = optionNotes.get(noteKey) ?? "";
928
+ setInlineEditor({ value: existing, cursor: existing.length });
929
+ setNoteInputMode(true);
930
+ continue;
931
+ }
932
+ if (event.key === "up" && totalOpts > 0) {
933
+ setAskSelectionIndex((prev) => (prev - 1 + totalOpts) % totalOpts);
934
+ continue;
935
+ }
936
+ if (event.key === "down" && totalOpts > 0) {
937
+ setAskSelectionIndex((prev) => (prev + 1) % totalOpts);
938
+ continue;
939
+ }
940
+ if (event.key === "left" && questions.length > 1) {
941
+ setCurrentQuestionIndex((prev) => Math.max(0, prev - 1));
942
+ setAskSelectionIndex(0);
943
+ setCustomInputMode(false);
944
+ setNoteInputMode(false);
945
+ continue;
946
+ }
947
+ if (event.key === "right" && questions.length > 1) {
948
+ // Auto-confirm any non-custom option before advancing.
949
+ if (q?.options?.[askSelectionIndex]?.kind !== "custom_input") {
950
+ confirmCurrentQuestion(askSelectionIndex);
951
+ }
952
+ setCurrentQuestionIndex((prev) => Math.min(questions.length - 1, prev + 1));
953
+ setAskSelectionIndex(0);
954
+ setCustomInputMode(false);
955
+ setNoteInputMode(false);
956
+ continue;
957
+ }
958
+ if (event.key === "enter") {
959
+ resolveSelectedPendingAsk();
960
+ continue;
961
+ }
962
+ continue;
963
+ }
964
+ const optionsLen = pendingAsk.options?.length ?? 0;
965
+ if (event.key === "up" && optionsLen > 0) {
966
+ setAskSelectionIndex((prev) => (prev - 1 + optionsLen) % optionsLen);
967
+ continue;
968
+ }
969
+ if (event.key === "down" && optionsLen > 0) {
970
+ setAskSelectionIndex((prev) => (prev + 1) % optionsLen);
971
+ continue;
972
+ }
973
+ if (event.key === "enter") {
974
+ resolveSelectedPendingAsk();
975
+ continue;
976
+ }
977
+ }
978
+ if (event.key === "ctrl_c") {
979
+ handleCtrlC();
980
+ continue;
981
+ }
982
+ if (event.key === "ctrl_l") {
983
+ handleCtrlL();
984
+ continue;
985
+ }
986
+ if (event.key === "ctrl_y") {
987
+ handleCtrlY();
988
+ continue;
989
+ }
990
+ if (event.key === "ctrl_g") {
991
+ handleCtrlG();
992
+ continue;
993
+ }
994
+ }
995
+ };
996
+ stdin.on("data", onData);
997
+ return () => {
998
+ const tail = shortcutDecoderRef.current.end();
999
+ if (tail.length > 0) {
1000
+ const events = shortcutParserRef.current.push(tail);
1001
+ for (const event of events) {
1002
+ if (event.type !== "key")
1003
+ continue;
1004
+ if (event.key === "ctrl_c")
1005
+ handleCtrlC();
1006
+ if (event.key === "ctrl_l")
1007
+ handleCtrlL();
1008
+ if (event.key === "ctrl_y")
1009
+ handleCtrlY();
1010
+ if (event.key === "ctrl_g")
1011
+ handleCtrlG();
1012
+ }
1013
+ }
1014
+ stdin.off("data", onData);
1015
+ };
1016
+ }, [
1017
+ stdin,
1018
+ pendingAsk,
1019
+ resolveSelectedPendingAsk,
1020
+ resolveAgentQuestion,
1021
+ confirmCurrentQuestion,
1022
+ handleCtrlC,
1023
+ handleCtrlL,
1024
+ handleCtrlY,
1025
+ handleCtrlG,
1026
+ currentQuestionIndex,
1027
+ customInputMode,
1028
+ noteInputMode,
1029
+ reviewMode,
1030
+ inlineEditor,
1031
+ askSelectionIndex,
1032
+ questionAnswers,
1033
+ optionNotes,
1034
+ ]);
1035
+ // ------------------------------------------------------------------
1036
+ // Render
1037
+ // ------------------------------------------------------------------
1038
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { children: " " }), _jsx(LogoPanel, { cwd: process.cwd() })] }), _jsx(ConversationPanel, { entries: entries, markdownMode: markdownMode, streamingAssistantEntryId: null }), planCheckpoints ? _jsx(PlanPanel, { checkpoints: planCheckpoints }) : null, pendingAsk ? (_jsx(AskPanel, { ask: pendingAsk, error: askError, selectedIndex: askSelectionIndex, currentQuestionIndex: currentQuestionIndex, totalQuestions: pendingAsk.kind === "agent_question"
1039
+ ? (pendingAsk.payload["questions"] ?? []).length
1040
+ : 1, questionAnswers: questionAnswers, customInputMode: customInputMode, noteInputMode: noteInputMode, reviewMode: reviewMode, inlineEditorValue: inlineEditor.value, inlineEditorCursor: inlineEditor.cursor, optionNotes: optionNotes })) : null, _jsx(InputPanel, { ref: inputPanelRef, onSubmit: handleSubmit, disabled: !!pendingAsk, commandRegistry: commandRegistry, store: store, hint: inputHint, onHintRequested: showInputHint, session: session }), _jsx(StatusBar, { phase: activityPhase, toolName: activityToolName, error: statusError, modelName: formatDisplayModelName(session.primaryAgent.modelConfig?.provider, session.primaryAgent.modelConfig?.model), contextTokens: contextTokens, contextLimit: session.primaryAgent.modelConfig?.contextLength, cacheReadTokens: cacheReadTokens })] }));
1041
+ }
1042
+ //# sourceMappingURL=app.js.map