@gajae-code/coding-agent 0.8.1 → 0.9.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 (253) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/completion-cli.d.ts +41 -0
  5. package/dist/types/cli/config-cli.d.ts +1 -0
  6. package/dist/types/cli/fixture-report.d.ts +20 -0
  7. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  8. package/dist/types/cli/nofile-limit.d.ts +11 -0
  9. package/dist/types/cli/notify-cli.d.ts +7 -0
  10. package/dist/types/cli/update-cli.d.ts +22 -0
  11. package/dist/types/cli.d.ts +124 -1
  12. package/dist/types/commands/completion.d.ts +29 -0
  13. package/dist/types/commands/config.d.ts +3 -0
  14. package/dist/types/commands/deep-interview.d.ts +3 -0
  15. package/dist/types/config/file-lock.d.ts +7 -6
  16. package/dist/types/config/keybindings.d.ts +2 -2
  17. package/dist/types/config/model-profile-activation.d.ts +6 -0
  18. package/dist/types/config/settings-schema.d.ts +105 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  26. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  27. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -2
  29. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  30. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  31. package/dist/types/gjc-runtime/team-runtime.d.ts +28 -1
  32. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
  34. package/dist/types/goals/runtime.d.ts +1 -0
  35. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  36. package/dist/types/main.d.ts +4 -1
  37. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  38. package/dist/types/modes/components/model-selector.d.ts +4 -0
  39. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  40. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  41. package/dist/types/modes/components/status-line.d.ts +9 -0
  42. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  43. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  45. package/dist/types/modes/interactive-mode.d.ts +2 -0
  46. package/dist/types/modes/print-mode.d.ts +13 -1
  47. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  48. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  49. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  50. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  51. package/dist/types/modes/types.d.ts +1 -0
  52. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  53. package/dist/types/notifications/config.d.ts +14 -1
  54. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  55. package/dist/types/notifications/index.d.ts +6 -3
  56. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  57. package/dist/types/notifications/telegram-daemon.d.ts +23 -3
  58. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  59. package/dist/types/notifications/threaded-render.d.ts +6 -0
  60. package/dist/types/runtime-api-key.d.ts +7 -0
  61. package/dist/types/sdk.d.ts +3 -0
  62. package/dist/types/session/agent-session.d.ts +53 -1
  63. package/dist/types/session/agent-storage.d.ts +1 -0
  64. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  65. package/dist/types/session/messages.d.ts +4 -0
  66. package/dist/types/session/session-manager.d.ts +17 -1
  67. package/dist/types/skill-state/active-state.d.ts +18 -1
  68. package/dist/types/system-prompt.d.ts +18 -2
  69. package/dist/types/task/executor.d.ts +3 -0
  70. package/dist/types/task/fork-context-budget.d.ts +7 -0
  71. package/dist/types/task/token-log.d.ts +30 -0
  72. package/dist/types/task/token-log.test.d.ts +1 -0
  73. package/dist/types/tools/bisect.d.ts +81 -0
  74. package/dist/types/tools/computer.d.ts +2 -2
  75. package/dist/types/tools/index.d.ts +1 -0
  76. package/dist/types/tools/search.d.ts +2 -2
  77. package/dist/types/utils/file-mentions.d.ts +14 -0
  78. package/dist/types/utils/git.d.ts +32 -0
  79. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  80. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  81. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  82. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  83. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  84. package/package.json +7 -7
  85. package/src/append-only-mode.ts +58 -0
  86. package/src/autoresearch/prompt.md +1 -1
  87. package/src/autoresearch/tools/log-experiment.ts +1 -1
  88. package/src/capability/rule.ts +9 -0
  89. package/src/cli/completion-cli.ts +234 -0
  90. package/src/cli/config-cli.ts +61 -9
  91. package/src/cli/fast-help.ts +1 -0
  92. package/src/cli/fixture-report.test.ts +230 -0
  93. package/src/cli/fixture-report.ts +171 -0
  94. package/src/cli/nofile-limit.ts +48 -0
  95. package/src/cli/notify-cli.ts +91 -7
  96. package/src/cli/update-cli.ts +85 -9
  97. package/src/cli.ts +29 -1
  98. package/src/commands/completion.ts +84 -0
  99. package/src/commands/config.ts +2 -0
  100. package/src/commands/contribution-prep.ts +1 -1
  101. package/src/commands/deep-interview.ts +2 -1
  102. package/src/config/file-lock.ts +73 -34
  103. package/src/config/keybindings.ts +2 -2
  104. package/src/config/model-profile-activation.ts +65 -0
  105. package/src/config/model-registry.ts +14 -2
  106. package/src/config/settings-schema.ts +68 -1
  107. package/src/config/settings.ts +22 -0
  108. package/src/coordinator-mcp/server.ts +34 -8
  109. package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
  110. package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
  111. package/src/defaults/gjc/skills/team/SKILL.md +17 -14
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  113. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  114. package/src/defaults/gjc-defaults.ts +9 -0
  115. package/src/discovery/helpers.ts +10 -0
  116. package/src/discovery/index.ts +1 -0
  117. package/src/export/html/template.generated.ts +1 -1
  118. package/src/export/html/template.js +60 -2
  119. package/src/export/ttsr.ts +96 -18
  120. package/src/extensibility/extensions/runner.ts +183 -219
  121. package/src/extensibility/extensions/types.ts +9 -0
  122. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  123. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  124. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  125. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  126. package/src/gjc-runtime/deep-interview-state.ts +44 -2
  127. package/src/gjc-runtime/session-state-sidecar.ts +263 -20
  128. package/src/gjc-runtime/state-runtime.ts +12 -2
  129. package/src/gjc-runtime/state-writer.ts +15 -0
  130. package/src/gjc-runtime/team-runtime.ts +187 -15
  131. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  132. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  133. package/src/gjc-runtime/ultragoal-runtime.ts +1663 -159
  134. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  135. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  136. package/src/goals/runtime.ts +10 -2
  137. package/src/hindsight/backend.ts +1 -3
  138. package/src/hooks/native-skill-hook.ts +124 -4
  139. package/src/internal-urls/docs-index.generated.ts +16 -7
  140. package/src/main.ts +95 -43
  141. package/src/memories/index.ts +12 -1
  142. package/src/modes/components/assistant-message.ts +241 -82
  143. package/src/modes/components/custom-editor.ts +1 -0
  144. package/src/modes/components/model-selector.ts +54 -11
  145. package/src/modes/components/queued-message-selector.ts +92 -0
  146. package/src/modes/components/settings-selector.ts +4 -0
  147. package/src/modes/components/status-line.ts +167 -54
  148. package/src/modes/components/tool-execution.ts +44 -19
  149. package/src/modes/components/welcome.ts +160 -44
  150. package/src/modes/controllers/command-controller.ts +40 -4
  151. package/src/modes/controllers/event-controller.ts +66 -47
  152. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  153. package/src/modes/controllers/input-controller.ts +180 -18
  154. package/src/modes/controllers/selector-controller.ts +122 -27
  155. package/src/modes/interactive-mode.ts +51 -16
  156. package/src/modes/print-mode.ts +43 -4
  157. package/src/modes/prompt-action-autocomplete.ts +21 -20
  158. package/src/modes/rpc/rpc-mode.ts +29 -3
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +1 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/ui-helpers.ts +19 -6
  167. package/src/notifications/config.ts +25 -5
  168. package/src/notifications/daemon-paths.ts +2 -0
  169. package/src/notifications/index.ts +117 -14
  170. package/src/notifications/lifecycle-commands.ts +37 -7
  171. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  172. package/src/notifications/telegram-daemon-cli.ts +2 -2
  173. package/src/notifications/telegram-daemon-control.ts +5 -3
  174. package/src/notifications/telegram-daemon.ts +302 -51
  175. package/src/notifications/telegram-reference.ts +0 -16
  176. package/src/notifications/threaded-render.ts +17 -1
  177. package/src/prompts/agents/architect.md +19 -10
  178. package/src/prompts/agents/critic.md +16 -18
  179. package/src/prompts/agents/executor.md +14 -11
  180. package/src/prompts/agents/planner.md +18 -13
  181. package/src/prompts/ci-green-request.md +3 -8
  182. package/src/prompts/goals/goal-continuation.md +0 -5
  183. package/src/prompts/goals/goal-mode-active.md +0 -5
  184. package/src/prompts/system/agent-creation-architect.md +2 -2
  185. package/src/prompts/system/plan-mode-active.md +1 -1
  186. package/src/prompts/system/plan-mode-subagent.md +3 -3
  187. package/src/prompts/system/project-prompt.md +0 -13
  188. package/src/prompts/system/subagent-system-prompt.md +3 -2
  189. package/src/prompts/system/system-prompt.md +29 -19
  190. package/src/prompts/system/volatile-project-context.md +10 -0
  191. package/src/prompts/system/web-search.md +4 -7
  192. package/src/prompts/tools/apply-patch.md +6 -0
  193. package/src/prompts/tools/ast-edit.md +1 -0
  194. package/src/prompts/tools/bash.md +1 -1
  195. package/src/prompts/tools/bisect.md +22 -0
  196. package/src/prompts/tools/browser.md +1 -1
  197. package/src/prompts/tools/computer.md +2 -2
  198. package/src/prompts/tools/cron.md +0 -2
  199. package/src/prompts/tools/eval.md +2 -2
  200. package/src/prompts/tools/find.md +1 -1
  201. package/src/prompts/tools/github.md +6 -6
  202. package/src/prompts/tools/goal.md +1 -3
  203. package/src/prompts/tools/image-gen.md +12 -5
  204. package/src/prompts/tools/irc.md +5 -4
  205. package/src/prompts/tools/job.md +2 -2
  206. package/src/prompts/tools/lsp.md +1 -1
  207. package/src/prompts/tools/monitor.md +2 -2
  208. package/src/prompts/tools/patch.md +2 -2
  209. package/src/prompts/tools/replace.md +2 -21
  210. package/src/prompts/tools/rewind.md +1 -1
  211. package/src/prompts/tools/search-tool-bm25.md +1 -11
  212. package/src/prompts/tools/search.md +1 -1
  213. package/src/prompts/tools/skill.md +3 -2
  214. package/src/prompts/tools/ssh.md +2 -1
  215. package/src/prompts/tools/subagent.md +2 -2
  216. package/src/prompts/tools/task.md +4 -5
  217. package/src/prompts/tools/todo-write.md +1 -1
  218. package/src/prompts/tools/vim.md +1 -0
  219. package/src/prompts/tools/web-search.md +2 -0
  220. package/src/prompts/tools/write.md +8 -2
  221. package/src/runtime-api-key.ts +16 -0
  222. package/src/sdk.ts +52 -28
  223. package/src/session/agent-session.ts +951 -132
  224. package/src/session/agent-storage.ts +4 -0
  225. package/src/session/file-mention-pruning.ts +64 -0
  226. package/src/session/messages.ts +4 -0
  227. package/src/session/session-manager.ts +103 -4
  228. package/src/skill-state/active-state.ts +141 -5
  229. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  230. package/src/slash-commands/builtin-registry.ts +218 -71
  231. package/src/system-prompt.ts +57 -16
  232. package/src/task/executor.ts +132 -24
  233. package/src/task/fork-context-advisory.ts +2 -7
  234. package/src/task/fork-context-budget.ts +9 -0
  235. package/src/task/index.ts +13 -6
  236. package/src/task/output-manager.ts +20 -8
  237. package/src/task/receipt.ts +26 -7
  238. package/src/task/token-log.test.ts +188 -0
  239. package/src/task/token-log.ts +179 -0
  240. package/src/tools/bisect.ts +367 -0
  241. package/src/tools/computer.ts +246 -43
  242. package/src/tools/index.ts +3 -0
  243. package/src/tools/output-meta.ts +116 -4
  244. package/src/tools/search.ts +8 -2
  245. package/src/tools/skill.ts +15 -0
  246. package/src/tools/telegram-send.ts +6 -0
  247. package/src/utils/file-mentions.ts +39 -7
  248. package/src/utils/git.ts +73 -0
  249. package/src/web/scrapers/youtube.ts +24 -12
  250. package/src/workflow/workflow-intent-diff.ts +29 -6
  251. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  252. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  253. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -21,6 +21,25 @@ export interface WelcomeComponentOptions {
21
21
  collapseChangelog?: boolean;
22
22
  }
23
23
 
24
+ const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 9;
25
+ const DEFAULT_WHATS_NEW_ROWS = 3;
26
+ const MAX_WHATS_NEW_ROWS = 12;
27
+ const NEWLINE_FLOW_KEY =
28
+ process.platform === "win32" ? { key: "alt+enter", label: "newline" } : { key: "ctrl+j", label: "newline" };
29
+
30
+ const FLOW_KEY_ITEMS: ReadonlyArray<{ key: string; label: string }> = [
31
+ { key: "/", label: "commands" },
32
+ { key: "#", label: "actions" },
33
+ { key: "!", label: "shell" },
34
+ { key: "$", label: "python" },
35
+ { key: "?", label: "keymap" },
36
+ { key: "ctrl+l", label: "model" },
37
+ { key: "shift+tab", label: "reasoning" },
38
+ { key: "tab", label: "complete" },
39
+ NEWLINE_FLOW_KEY,
40
+ { key: "ctrl+c", label: "clear" },
41
+ ];
42
+
24
43
  /**
25
44
  * GJC-native launch surface with compact command affordances, project
26
45
  * signals, and a claw/talon mark without copying another agent shell.
@@ -97,7 +116,6 @@ export class WelcomeComponent implements Component {
97
116
  }
98
117
  const targetContentRows = targetRows === undefined ? undefined : Math.max(0, targetRows - 2);
99
118
  const dualContentWidth = boxWidth - 3; // 3 = │ + │ + │
100
- const preferredLeftCol = 40;
101
119
  const minLeftCol = 20; // logo mark plus GJC identity labels
102
120
  const minRightCol = 24;
103
121
  const modelPill = this.#pill(theme.icon.model || "model", this.modelName, "statusLineModel");
@@ -112,10 +130,12 @@ export class WelcomeComponent implements Component {
112
130
  visibleWidth(modelPill),
113
131
  visibleWidth(providerPill),
114
132
  );
115
- const desiredLeftCol = Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.38)));
133
+ const evenLeftCol = Math.floor(dualContentWidth / 2);
134
+ const maxLeftColWithRightMinimum = Math.max(1, dualContentWidth - minRightCol);
135
+ const desiredLeftCol = Math.max(leftMinContentWidth, evenLeftCol);
116
136
  const dualLeftCol =
117
137
  dualContentWidth >= minRightCol + 1
118
- ? Math.min(desiredLeftCol, dualContentWidth - minRightCol)
138
+ ? Math.min(desiredLeftCol, maxLeftColWithRightMinimum)
119
139
  : Math.max(1, dualContentWidth - 1);
120
140
  const dualRightCol = Math.max(1, dualContentWidth - dualLeftCol);
121
141
  const showRightColumn = dualLeftCol >= leftMinContentWidth && dualRightCol >= minRightCol;
@@ -142,26 +162,6 @@ export class WelcomeComponent implements Component {
142
162
 
143
163
  const rightColumnWidth = showRightColumn ? rightCol : leftCol;
144
164
  const separator = buildSeparator(rightColumnWidth);
145
- const changelogBudget = Math.max(1, Math.min(6, Math.floor((targetContentRows ?? 18) * 0.3)));
146
-
147
- const sessionLines: string[] = [];
148
- if (this.recentSessions.length === 0) {
149
- sessionLines.push(` ${theme.fg("dim", "No saved trails")}`);
150
- } else {
151
- const bulletPrefix = ` ${theme.md.bullet} `;
152
- const prefixWidth = visibleWidth(bulletPrefix);
153
- for (const session of this.recentSessions.slice(0, 3)) {
154
- const timeSuffixRaw = ` (${session.timeAgo})`;
155
- const timeWidth = visibleWidth(timeSuffixRaw);
156
- const nameBudget = Math.max(1, rightColumnWidth - prefixWidth - timeWidth);
157
- const nameVis = visibleWidth(session.name);
158
- const name = nameVis > nameBudget ? truncateToWidth(session.name, nameBudget) : session.name;
159
- sessionLines.push(
160
- `${theme.fg("dim", bulletPrefix)}${theme.fg("muted", name)}${theme.fg("dim", timeSuffixRaw)}`,
161
- );
162
- }
163
- }
164
-
165
165
  const lspLines: string[] = [];
166
166
  if (this.lspServers.length === 0) {
167
167
  lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
@@ -178,25 +178,31 @@ export class WelcomeComponent implements Component {
178
178
  }
179
179
  }
180
180
 
181
+ const flowPreferredRows = this.#flowKeyRows(rightColumnWidth).length;
182
+ const changelogRowLimit = this.#whatsNewRowLimit(targetContentRows, lspLines.length, flowPreferredRows);
183
+ const changelogLines = this.#whatsNewLines(rightColumnWidth, changelogRowLimit);
184
+ const flowRowLimit = this.#flowKeyRowLimit(
185
+ targetContentRows,
186
+ changelogLines.length,
187
+ lspLines.length,
188
+ rightColumnWidth,
189
+ );
190
+ const flowLines = this.#flowKeyLines(rightColumnWidth, flowRowLimit);
191
+ const sessionLimit = this.#sessionTrailLimit(
192
+ targetContentRows,
193
+ changelogLines.length,
194
+ lspLines.length,
195
+ flowLines.length,
196
+ );
197
+ const sessionLines = this.#sessionTrailLines(rightColumnWidth, sessionLimit);
198
+
181
199
  const rightLines = [
182
200
  "",
183
201
  ` ${theme.bold(theme.fg("accent", "What's New"))}`,
184
- ...this.#whatsNewLines(rightColumnWidth, changelogBudget),
202
+ ...changelogLines,
185
203
  separator,
186
204
  ` ${theme.bold(theme.fg("accent", "Flow keys"))}`,
187
- ` ${theme.fg("dim", "/")}${theme.fg("muted", " commands")} ${theme.fg("dim", "·")} ${theme.fg(
188
- "dim",
189
- "#",
190
- )}${theme.fg("muted", " actions")}`,
191
- ` ${theme.fg("dim", "!")}${theme.fg("muted", " shell")} ${theme.fg("dim", "·")} ${theme.fg("dim", "$")}${theme.fg(
192
- "muted",
193
- " python",
194
- )}`,
195
- ` ${theme.fg("dim", "?")}${theme.fg("muted", " keymap")} ${theme.fg("dim", "·")} ${theme.fg(
196
- "dim",
197
- "ctrl+l",
198
- )}${theme.fg("muted", " model")}`,
199
- ` ${theme.fg("dim", "shift+tab")}${theme.fg("muted", " reasoning")}`,
205
+ ...flowLines,
200
206
  separator,
201
207
  ` ${theme.bold(theme.fg("accent", "Project pulse"))}`,
202
208
  ...lspLines,
@@ -315,7 +321,118 @@ export class WelcomeComponent implements Component {
315
321
  return [...Array.from({ length: topPad }, () => ""), ...clipped, ...Array.from({ length: bottomPad }, () => "")];
316
322
  }
317
323
 
318
- #whatsNewLines(width: number, maxItems: number): string[] {
324
+ #flowKeyItemText(item: { key: string; label: string }): string {
325
+ return `${theme.fg("dim", item.key)}${theme.fg("muted", ` ${item.label}`)}`;
326
+ }
327
+
328
+ #flowKeyRows(width: number): string[] {
329
+ const contentWidth = Math.max(1, width - 1);
330
+ const separator = ` ${theme.fg("dim", "·")} `;
331
+ const rows: string[] = [];
332
+ let current = "";
333
+ for (const item of FLOW_KEY_ITEMS) {
334
+ const segment = this.#flowKeyItemText(item);
335
+ const next = current ? `${current}${separator}${segment}` : segment;
336
+ if (current && visibleWidth(next) > contentWidth) {
337
+ rows.push(` ${current}`);
338
+ current = segment;
339
+ } else {
340
+ current = next;
341
+ }
342
+ }
343
+ if (current) rows.push(` ${current}`);
344
+ return rows.length > 0 ? rows : [` ${theme.fg("dim", "No flow keys")}`];
345
+ }
346
+
347
+ #flowKeyLines(width: number, maxRows: number): string[] {
348
+ const rows = this.#flowKeyRows(width);
349
+ const rowLimit = Math.max(1, Math.floor(maxRows));
350
+ if (rows.length <= rowLimit) return rows;
351
+ if (rowLimit === 1) {
352
+ const firstItem = FLOW_KEY_ITEMS[0];
353
+ const firstSegment = firstItem ? this.#flowKeyItemText(firstItem) : theme.fg("dim", "keys");
354
+ return [this.#fitToWidth(` ${firstSegment} ${theme.fg("dim", "· … ")}${theme.bold("/help")}`, width)];
355
+ }
356
+ return [...rows.slice(0, rowLimit - 1), ` ${theme.fg("dim", `… ${theme.bold("/help")} for more`)}`];
357
+ }
358
+
359
+ #flowKeyRowLimit(
360
+ targetContentRows: number | undefined,
361
+ changelogLineCount: number,
362
+ lspLineCount: number,
363
+ rightColumnWidth: number,
364
+ ): number {
365
+ const preferredRows = this.#flowKeyRows(rightColumnWidth).length;
366
+ if (targetContentRows === undefined) return preferredRows;
367
+
368
+ const sessionBaselineRows = this.recentSessions.length === 0 ? 1 : Math.min(3, this.recentSessions.length);
369
+ const availableRows =
370
+ targetContentRows -
371
+ WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS -
372
+ changelogLineCount -
373
+ lspLineCount -
374
+ sessionBaselineRows;
375
+ return Math.max(1, Math.min(preferredRows, availableRows));
376
+ }
377
+
378
+ #whatsNewRowLimit(targetContentRows: number | undefined, lspLineCount: number, flowLineCount: number): number {
379
+ if (targetContentRows === undefined) return 5;
380
+
381
+ const sessionBaselineRows = this.recentSessions.length === 0 ? 1 : Math.min(3, this.recentSessions.length);
382
+ const dynamicRows = Math.max(
383
+ 1,
384
+ targetContentRows - WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS - flowLineCount - lspLineCount,
385
+ );
386
+ const rowsAfterBaselineSessions = Math.max(1, dynamicRows - sessionBaselineRows);
387
+ const spareRows = Math.max(0, rowsAfterBaselineSessions - DEFAULT_WHATS_NEW_ROWS);
388
+ return Math.max(
389
+ 1,
390
+ Math.min(MAX_WHATS_NEW_ROWS, rowsAfterBaselineSessions, DEFAULT_WHATS_NEW_ROWS + Math.floor(spareRows / 2)),
391
+ );
392
+ }
393
+
394
+ #sessionTrailLimit(
395
+ targetContentRows: number | undefined,
396
+ changelogLineCount: number,
397
+ lspLineCount: number,
398
+ flowLineCount: number,
399
+ ): number {
400
+ if (this.recentSessions.length === 0) return 0;
401
+
402
+ const defaultLimit = Math.min(3, this.recentSessions.length);
403
+ if (targetContentRows === undefined) return defaultLimit;
404
+
405
+ const rowsWithDefaultTrail =
406
+ WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS +
407
+ changelogLineCount +
408
+ flowLineCount +
409
+ lspLineCount +
410
+ defaultLimit;
411
+ const extraRows = Math.max(0, targetContentRows - rowsWithDefaultTrail);
412
+ return Math.min(this.recentSessions.length, defaultLimit + extraRows);
413
+ }
414
+
415
+ #sessionTrailLines(rightColumnWidth: number, limit: number): string[] {
416
+ if (this.recentSessions.length === 0) {
417
+ return [` ${theme.fg("dim", "No saved trails")}`];
418
+ }
419
+
420
+ const bulletPrefix = ` ${theme.md.bullet} `;
421
+ const prefixWidth = visibleWidth(bulletPrefix);
422
+ const lines: string[] = [];
423
+ for (const session of this.recentSessions.slice(0, limit)) {
424
+ const timeSuffixRaw = ` (${session.timeAgo})`;
425
+ const timeWidth = visibleWidth(timeSuffixRaw);
426
+ const nameBudget = Math.max(1, rightColumnWidth - prefixWidth - timeWidth);
427
+ const nameVis = visibleWidth(session.name);
428
+ const name = nameVis > nameBudget ? truncateToWidth(session.name, nameBudget) : session.name;
429
+ lines.push(`${theme.fg("dim", bulletPrefix)}${theme.fg("muted", name)}${theme.fg("dim", timeSuffixRaw)}`);
430
+ }
431
+ return lines;
432
+ }
433
+
434
+ #whatsNewLines(width: number, maxRows: number): string[] {
435
+ const rowLimit = Math.max(1, Math.floor(maxRows));
319
436
  const changelog = this.options.changelogMarkdown?.trim();
320
437
  if (!changelog) {
321
438
  return [` ${theme.fg("dim", "Ready for your next prompt")}`];
@@ -326,25 +443,25 @@ export class WelcomeComponent implements Component {
326
443
  return [
327
444
  ` ${theme.fg("muted", `Updated to v${version}`)}`,
328
445
  ` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
329
- ];
446
+ ].slice(0, rowLimit);
330
447
  }
331
448
 
332
- const itemLimit = Math.max(1, Math.floor(maxItems));
333
449
  const items = this.#changelogItems(changelog);
334
450
  if (items.length === 0) {
335
451
  return [
336
452
  ` ${theme.fg("muted", `Updated to v${version}`)}`,
337
453
  ` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
338
- ];
454
+ ].slice(0, rowLimit);
339
455
  }
340
456
 
341
457
  const prefix = ` ${theme.md.bullet} `;
342
458
  const textWidth = Math.max(1, width - visibleWidth(prefix));
343
- const visibleItems = items.slice(0, itemLimit).map(item => {
459
+ const visibleItemCount = items.length > rowLimit ? Math.max(1, rowLimit - 1) : rowLimit;
460
+ const visibleItems = items.slice(0, visibleItemCount).map(item => {
344
461
  const text = visibleWidth(item) > textWidth ? truncateToWidth(item, textWidth) : item;
345
462
  return `${theme.fg("dim", prefix)}${theme.fg("muted", text)}`;
346
463
  });
347
- if (items.length > visibleItems.length) {
464
+ if (items.length > visibleItems.length && visibleItems.length < rowLimit) {
348
465
  visibleItems.push(` ${theme.fg("dim", `… ${theme.bold("/changelog")} for full notes`)}`);
349
466
  }
350
467
  return visibleItems;
@@ -472,7 +589,6 @@ interface ShineConfig {
472
589
  /** Center of the shine band along the diagonal, in [0, 1]. */
473
590
  pos: number;
474
591
  }
475
-
476
592
  /**
477
593
  * Apply a multi-stop diagonal gradient (bottom-left → top-right) plus an
478
594
  * optional sliding shine band across multi-line art. `phase` (0..1) shifts the
@@ -13,6 +13,7 @@ import {
13
13
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
14
14
  import { formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
15
15
  import { $ } from "bun";
16
+ import { resolveAppendOnlyMode } from "../../append-only-mode";
16
17
  import { jobElapsedMs } from "../../async";
17
18
  import { reset as resetCapabilities } from "../../capability";
18
19
  import { clearClaudePluginRootsCache } from "../../discovery/helpers";
@@ -400,7 +401,7 @@ export class CommandController {
400
401
  {
401
402
  const setting = this.ctx.settings.get("provider.appendOnlyContext") ?? "auto";
402
403
  const provider = this.ctx.session.model?.provider;
403
- const mode = setting === "on" ? true : setting === "off" ? false : provider === "deepseek";
404
+ const mode = resolveAppendOnlyMode(setting, provider ?? "");
404
405
  const activeLabel = mode ? theme.fg("success", "active") : theme.fg("dim", "inactive");
405
406
  const settingLabel = setting === "auto" ? `${setting} (${provider ?? "?"})` : setting;
406
407
  info += `${theme.fg("dim", "Append-Only:")} ${activeLabel} (setting: ${settingLabel})\n`;
@@ -540,7 +541,7 @@ export class CommandController {
540
541
  const title = showFull ? "Full Changelog" : "Recent Changes";
541
542
  const hint = showFull
542
543
  ? ""
543
- : `\n\n${theme.fg("dim", "Use")} ${theme.bold("/changelog full")} ${theme.fg("dim", "to view the complete changelog.")}`;
544
+ : `\n\n${theme.fg("dim", "Use")} ${theme.bold("/changelog --full")} ${theme.fg("dim", "to view the complete changelog.")}`;
544
545
 
545
546
  this.ctx.chatContainer.addChild(new Spacer(1));
546
547
  this.ctx.chatContainer.addChild(new DynamicBorder());
@@ -926,8 +927,7 @@ export class CommandController {
926
927
  this.ctx.streamingMessage = undefined;
927
928
  this.ctx.pendingTools.clear();
928
929
 
929
- this.ctx.chatContainer.addChild(new Spacer(1));
930
- this.ctx.chatContainer.addChild(new Text(`${theme.fg("accent", `${theme.status.success} ${label}`)}`, 1, 1));
930
+ this.ctx.chatContainer.addChild(new Text(`${theme.fg("accent", `${theme.status.success} ${label}`)}`, 1, 0));
931
931
  await this.ctx.reloadTodos();
932
932
  this.ctx.ui.requestRender();
933
933
  }
@@ -936,6 +936,42 @@ export class CommandController {
936
936
  await this.#runNewSessionFlow();
937
937
  }
938
938
 
939
+ async handleContextClearCommand(): Promise<void> {
940
+ if (this.ctx.loadingAnimation) {
941
+ this.ctx.loadingAnimation.stop();
942
+ this.ctx.loadingAnimation = undefined;
943
+ }
944
+ this.ctx.statusContainer.clear();
945
+
946
+ if (this.ctx.session.isCompacting) {
947
+ this.ctx.session.abortCompaction();
948
+ while (this.ctx.session.isCompacting) {
949
+ await Bun.sleep(10);
950
+ }
951
+ }
952
+ if (!(await this.ctx.session.clearContext())) return;
953
+ setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
954
+
955
+ this.ctx.statusLine.invalidate();
956
+ this.ctx.statusLine.setSessionStartTime(Date.now());
957
+ this.ctx.updateEditorTopBorder();
958
+ this.ctx.updateEditorBorderColor();
959
+ this.ctx.ui.requestRender();
960
+
961
+ this.ctx.chatContainer.clear();
962
+ this.ctx.pendingMessagesContainer.clear();
963
+ this.ctx.compactionQueuedMessages = [];
964
+ this.ctx.streamingComponent = undefined;
965
+ this.ctx.streamingMessage = undefined;
966
+ this.ctx.pendingTools.clear();
967
+
968
+ this.ctx.chatContainer.addChild(
969
+ new Text(`${theme.fg("accent", `${theme.status.success} Context cleared`)}`, 1, 0),
970
+ );
971
+ await this.ctx.reloadTodos();
972
+ this.ctx.ui.requestRender();
973
+ }
974
+
939
975
  async handleDropCommand(): Promise<void> {
940
976
  if (!this.ctx.sessionManager.getSessionFile()) {
941
977
  this.ctx.showError("Nothing to drop (in-memory session)");
@@ -24,10 +24,20 @@ import type { ResolveToolDetails } from "../../tools/resolve";
24
24
  import { interruptHint } from "../shared";
25
25
  import { buildAbortDisplayMessage } from "../utils/abort-message";
26
26
  import { ringTerminalBell } from "../utils/terminal-bell";
27
+ import { argsWithPartialJson } from "../utils/ui-helpers";
27
28
 
28
29
  type AgentSessionEventKind = AgentSessionEvent["type"];
29
30
 
30
31
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
32
+
33
+ /** Test-only performance counters for advisory baseline tests. */
34
+ export const __eventControllerPerfCounters = {
35
+ messageUpdateContentVisits: 0,
36
+ reset(): void {
37
+ this.messageUpdateContentVisits = 0;
38
+ },
39
+ };
40
+
31
41
  const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
32
42
 
33
43
  interface CompletionNotifyPayload {
@@ -100,6 +110,8 @@ export class EventController {
100
110
  #lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
101
111
  #idleCompactionTimer?: NodeJS.Timeout;
102
112
  #ircExpiryTimers = new Map<string, NodeJS.Timeout>();
113
+ #toolIntentCache = new Map<string, { args: unknown; intent: string | undefined }>();
114
+ #thinkingContentIndices = new Set<number>();
103
115
  #handlers: AgentSessionEventHandlers;
104
116
 
105
117
  constructor(private ctx: InteractiveModeContext) {
@@ -307,12 +319,14 @@ export class EventController {
307
319
  } else if (event.message.role === "assistant") {
308
320
  this.#lastThinkingCount = 0;
309
321
  this.#resetReadGroup();
322
+ this.#toolIntentCache.clear();
323
+ this.#thinkingContentIndices.clear();
310
324
  this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock, () =>
311
325
  this.ctx.ui.requestRender(),
312
326
  );
313
327
  this.ctx.streamingMessage = event.message;
314
328
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
315
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
329
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
316
330
  this.ctx.ui.requestRender();
317
331
  }
318
332
  }
@@ -375,46 +389,53 @@ export class EventController {
375
389
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
376
390
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
377
391
  this.ctx.streamingMessage = event.message;
378
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
379
-
380
- const thinkingCount = this.ctx.streamingMessage.content.filter(
381
- content => content.type === "thinking" && content.thinking.trim(),
382
- ).length;
383
- if (thinkingCount > this.#lastThinkingCount) {
384
- this.#resetReadGroup();
385
- this.#lastThinkingCount = thinkingCount;
392
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
393
+ const contentIndex = event.assistantMessageEvent?.contentIndex;
394
+ const changedContent =
395
+ typeof contentIndex === "number" && contentIndex >= 0
396
+ ? this.ctx.streamingMessage.content[contentIndex]
397
+ : undefined;
398
+ const contentsToProcess = changedContent ? [changedContent] : this.ctx.streamingMessage.content;
399
+
400
+ if (changedContent?.type === "thinking" && changedContent.thinking.trim()) {
401
+ if (typeof contentIndex === "number") this.#thinkingContentIndices.add(contentIndex);
402
+ const thinkingCount = this.#thinkingContentIndices.size;
403
+ if (thinkingCount > this.#lastThinkingCount) {
404
+ this.#resetReadGroup();
405
+ this.#lastThinkingCount = thinkingCount;
406
+ }
407
+ } else if (!changedContent) {
408
+ const thinkingCount = this.ctx.streamingMessage.content.filter(
409
+ content => content.type === "thinking" && content.thinking.trim(),
410
+ ).length;
411
+ if (thinkingCount > this.#lastThinkingCount) {
412
+ this.#resetReadGroup();
413
+ this.#lastThinkingCount = thinkingCount;
414
+ }
386
415
  }
387
416
 
388
- for (const content of this.ctx.streamingMessage.content) {
417
+ for (const content of contentsToProcess) {
418
+ __eventControllerPerfCounters.messageUpdateContentVisits += 1;
389
419
  if (content.type !== "toolCall") continue;
390
420
  if (content.name === "read") {
391
- if (!readArgsHaveTarget(content.arguments)) {
392
- // Args still streaming — defer until path is parseable so we can route to the
393
- // read group (regular files) vs ToolExecutionComponent (internal URLs).
394
- // Creating either component now would lock the read into the wrong shape.
395
- continue;
396
- }
421
+ if (!readArgsHaveTarget(content.arguments)) continue;
397
422
  if (!readArgsTargetInternalUrl(content.arguments)) {
398
423
  this.#trackReadToolCall(content.id, content.arguments);
399
424
  const component = this.ctx.pendingTools.get(content.id);
400
- if (component) {
401
- component.updateArgs(content.arguments, content.id);
402
- } else {
425
+ if (component) component.updateArgs(content.arguments, content.id);
426
+ else {
403
427
  const group = this.#getReadGroup();
404
428
  group.updateArgs(content.arguments, content.id);
405
429
  this.ctx.pendingTools.set(content.id, group);
406
430
  }
407
431
  continue;
408
432
  }
409
- // Internal URL read falls through to ToolExecutionComponent below.
410
433
  }
411
434
 
412
- // Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
413
- // Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
414
- const renderArgs =
415
- "partialJson" in content
416
- ? { ...content.arguments, __partialJson: content.partialJson }
417
- : content.arguments;
435
+ const renderArgs = argsWithPartialJson(
436
+ content.arguments,
437
+ "partialJson" in content ? content.partialJson : undefined,
438
+ );
418
439
  if (!this.ctx.pendingTools.has(content.id)) {
419
440
  this.#resetReadGroup();
420
441
  this.ctx.chatContainer.addChild(new Text("", 0, 0));
@@ -437,32 +458,30 @@ export class EventController {
437
458
  this.ctx.chatContainer.addChild(component);
438
459
  this.ctx.pendingTools.set(content.id, component);
439
460
  } else {
440
- const component = this.ctx.pendingTools.get(content.id);
441
- if (component) {
442
- component.updateArgs(renderArgs, content.id);
443
- }
461
+ this.ctx.pendingTools.get(content.id)?.updateArgs(renderArgs, content.id);
444
462
  }
445
- }
446
463
 
447
- // Update working message with intent from streamed tool arguments
448
- for (const content of this.ctx.streamingMessage.content) {
449
- if (content.type !== "toolCall") continue;
450
464
  const args = content.arguments;
451
465
  if (!args || typeof args !== "object") continue;
466
+ let intent: string | undefined;
452
467
  if (INTENT_FIELD in args) {
453
- this.#updateWorkingMessageFromIntent(args[INTENT_FIELD]);
454
- continue;
455
- }
456
- const tool = this.ctx.session.getToolByName(content.name);
457
- if (typeof tool?.intent !== "function") continue;
458
- try {
459
- const derived = tool.intent(args as never)?.trim();
460
- if (derived) {
461
- this.#updateWorkingMessageFromIntent(derived);
468
+ intent = String(args[INTENT_FIELD]);
469
+ } else {
470
+ const cached = this.#toolIntentCache.get(content.id);
471
+ if (cached?.args === args) intent = cached.intent;
472
+ else {
473
+ const tool = this.ctx.session.getToolByName(content.name);
474
+ if (typeof tool?.intent === "function") {
475
+ try {
476
+ intent = tool.intent(args as never)?.trim();
477
+ } catch {
478
+ // intent function must never break the UI
479
+ }
480
+ }
481
+ this.#toolIntentCache.set(content.id, { args, intent });
462
482
  }
463
- } catch {
464
- // intent function must never break the UI
465
483
  }
484
+ if (intent) this.#updateWorkingMessageFromIntent(intent);
466
485
  }
467
486
 
468
487
  this.ctx.ui.requestRender();
@@ -495,9 +514,9 @@ export class EventController {
495
514
  // display only — does NOT mutate the persisted message's stopReason
496
515
  // (the marker on errorMessage drives replay-side suppression).
497
516
  const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
498
- this.ctx.streamingComponent.updateContent(msgWithoutAbort);
517
+ this.ctx.streamingComponent.updateContent(msgWithoutAbort, { streaming: false });
499
518
  } else {
500
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
519
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: false });
501
520
  }
502
521
 
503
522
  if (this.ctx.streamingMessage.stopReason !== "aborted" && this.ctx.streamingMessage.stopReason !== "error") {
@@ -191,9 +191,8 @@ export class ExtensionUiController {
191
191
  this.ctx.streamingMessage = undefined;
192
192
  this.ctx.pendingTools.clear();
193
193
 
194
- this.ctx.chatContainer.addChild(new Spacer(1));
195
194
  this.ctx.chatContainer.addChild(
196
- new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
195
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
197
196
  );
198
197
  await this.ctx.reloadTodos();
199
198
  this.ctx.ui.requestRender();
@@ -299,15 +298,14 @@ export class ExtensionUiController {
299
298
  }
300
299
 
301
300
  #rebuildHookWidgets(): void {
302
- this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true, true);
303
- this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false, false);
301
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true);
302
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false);
304
303
  this.ctx.ui.requestRender();
305
304
  }
306
305
 
307
306
  #renderHookWidgetContainer(
308
307
  container: Container,
309
308
  widgets: Map<string, ExtensionUiComponent>,
310
- spacerWhenEmpty: boolean,
311
309
  leadingSpacer: boolean,
312
310
  ): void {
313
311
  // Detach (not dispose): hook widgets are persistent instances owned by the
@@ -317,9 +315,6 @@ export class ExtensionUiController {
317
315
  container.detachAll();
318
316
 
319
317
  if (widgets.size === 0) {
320
- if (spacerWhenEmpty) {
321
- container.addChild(new Spacer(1));
322
- }
323
318
  return;
324
319
  }
325
320
 
@@ -434,9 +429,8 @@ export class ExtensionUiController {
434
429
  this.ctx.streamingMessage = undefined;
435
430
  this.ctx.pendingTools.clear();
436
431
 
437
- this.ctx.chatContainer.addChild(new Spacer(1));
438
432
  this.ctx.chatContainer.addChild(
439
- new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
433
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
440
434
  );
441
435
  await this.ctx.reloadTodos();
442
436
  this.ctx.ui.requestRender();