@gajae-code/coding-agent 0.2.4 → 0.3.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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +145 -2
  4. package/dist/types/commands/harness.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +13 -3
  6. package/dist/types/config/settings.d.ts +3 -1
  7. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  8. package/dist/types/discovery/helpers.d.ts +1 -0
  9. package/dist/types/exec/bash-executor.d.ts +8 -1
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/extensibility/shared-events.d.ts +1 -0
  13. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  16. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  17. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  18. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  20. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  21. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  22. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  23. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  24. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  25. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  26. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  27. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  28. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  29. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  30. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  31. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  32. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  33. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  34. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  35. package/dist/types/harness-control-plane/types.d.ts +162 -0
  36. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  37. package/dist/types/hooks/skill-state.d.ts +2 -29
  38. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  39. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  40. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +2 -0
  42. package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
  43. package/dist/types/modes/theme/theme.d.ts +1 -5
  44. package/dist/types/modes/types.d.ts +2 -0
  45. package/dist/types/sdk.d.ts +4 -0
  46. package/dist/types/session/agent-session.d.ts +8 -0
  47. package/dist/types/session/streaming-output.d.ts +11 -0
  48. package/dist/types/skill-state/active-state.d.ts +3 -0
  49. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  50. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  51. package/dist/types/task/executor.d.ts +3 -0
  52. package/dist/types/task/types.d.ts +56 -3
  53. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  54. package/dist/types/tools/bash.d.ts +24 -0
  55. package/dist/types/tools/cron.d.ts +110 -0
  56. package/dist/types/tools/index.d.ts +4 -0
  57. package/dist/types/tools/monitor.d.ts +54 -0
  58. package/dist/types/tools/subagent.d.ts +11 -1
  59. package/dist/types/web/search/index.d.ts +1 -0
  60. package/dist/types/web/search/provider.d.ts +11 -4
  61. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +7 -7
  64. package/src/async/job-manager.ts +522 -6
  65. package/src/cli/agents-cli.ts +3 -0
  66. package/src/cli/auth-broker-cli.ts +1 -0
  67. package/src/cli/config-cli.ts +10 -2
  68. package/src/cli.ts +2 -0
  69. package/src/commands/harness.ts +592 -0
  70. package/src/commands/team.ts +36 -39
  71. package/src/config/settings-schema.ts +15 -2
  72. package/src/config/settings.ts +49 -7
  73. package/src/deep-interview/render-middleware.ts +366 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
  75. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  76. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  78. package/src/discovery/helpers.ts +5 -0
  79. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  80. package/src/exec/bash-executor.ts +20 -9
  81. package/src/extensibility/custom-tools/types.ts +1 -0
  82. package/src/extensibility/extensions/types.ts +6 -0
  83. package/src/extensibility/shared-events.ts +1 -0
  84. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  85. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  86. package/src/gjc-runtime/ralplan-runtime.ts +27 -10
  87. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  88. package/src/gjc-runtime/state-graph.ts +86 -0
  89. package/src/gjc-runtime/state-migrations.ts +132 -0
  90. package/src/gjc-runtime/state-renderer.ts +345 -0
  91. package/src/gjc-runtime/state-runtime.ts +733 -21
  92. package/src/gjc-runtime/state-validation.ts +49 -0
  93. package/src/gjc-runtime/state-writer.ts +718 -0
  94. package/src/gjc-runtime/team-runtime.ts +1083 -89
  95. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  96. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  97. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  98. package/src/harness-control-plane/classifier.ts +128 -0
  99. package/src/harness-control-plane/control-endpoint.ts +137 -0
  100. package/src/harness-control-plane/finalize.ts +222 -0
  101. package/src/harness-control-plane/frame-mapper.ts +286 -0
  102. package/src/harness-control-plane/operate.ts +225 -0
  103. package/src/harness-control-plane/owner.ts +553 -0
  104. package/src/harness-control-plane/preserve.ts +102 -0
  105. package/src/harness-control-plane/receipts.ts +216 -0
  106. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  107. package/src/harness-control-plane/seams.ts +39 -0
  108. package/src/harness-control-plane/session-lease.ts +388 -0
  109. package/src/harness-control-plane/state-machine.ts +97 -0
  110. package/src/harness-control-plane/storage.ts +257 -0
  111. package/src/harness-control-plane/types.ts +214 -0
  112. package/src/hooks/skill-keywords.ts +4 -2
  113. package/src/hooks/skill-state.ts +25 -42
  114. package/src/internal-urls/docs-index.generated.ts +6 -4
  115. package/src/lsp/render.ts +1 -1
  116. package/src/modes/acp/acp-agent.ts +1 -1
  117. package/src/modes/acp/acp-client-bridge.ts +1 -1
  118. package/src/modes/components/agent-dashboard.ts +1 -1
  119. package/src/modes/components/assistant-message.ts +5 -1
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/hook-selector.ts +72 -2
  122. package/src/modes/components/skill-hud/render.ts +7 -2
  123. package/src/modes/controllers/event-controller.ts +71 -6
  124. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  125. package/src/modes/controllers/input-controller.ts +19 -3
  126. package/src/modes/controllers/selector-controller.ts +3 -2
  127. package/src/modes/interactive-mode.ts +21 -2
  128. package/src/modes/theme/defaults/index.ts +0 -196
  129. package/src/modes/theme/theme.ts +35 -35
  130. package/src/modes/types.ts +2 -0
  131. package/src/prompts/agents/architect.md +5 -1
  132. package/src/prompts/agents/critic.md +5 -1
  133. package/src/prompts/agents/executor.md +13 -0
  134. package/src/prompts/agents/frontmatter.md +1 -0
  135. package/src/prompts/agents/planner.md +5 -1
  136. package/src/prompts/tools/bash.md +9 -0
  137. package/src/prompts/tools/cron.md +25 -0
  138. package/src/prompts/tools/monitor.md +30 -0
  139. package/src/prompts/tools/subagent.md +33 -3
  140. package/src/runtime-mcp/oauth-flow.ts +4 -2
  141. package/src/sdk.ts +7 -0
  142. package/src/session/agent-session.ts +247 -38
  143. package/src/session/session-manager.ts +13 -1
  144. package/src/session/streaming-output.ts +21 -0
  145. package/src/skill-state/active-state.ts +222 -78
  146. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  147. package/src/skill-state/initial-phase.ts +2 -0
  148. package/src/skill-state/workflow-state-contract.ts +26 -0
  149. package/src/task/agents.ts +1 -0
  150. package/src/task/executor.ts +51 -8
  151. package/src/task/index.ts +120 -8
  152. package/src/task/render.ts +6 -3
  153. package/src/task/types.ts +57 -3
  154. package/src/tools/ask.ts +28 -7
  155. package/src/tools/bash-allowed-prefixes.ts +169 -0
  156. package/src/tools/bash.ts +190 -29
  157. package/src/tools/browser/tab-worker.ts +1 -1
  158. package/src/tools/cron.ts +665 -0
  159. package/src/tools/index.ts +20 -2
  160. package/src/tools/monitor.ts +136 -0
  161. package/src/tools/subagent.ts +255 -64
  162. package/src/vim/engine.ts +3 -3
  163. package/src/web/search/index.ts +31 -18
  164. package/src/web/search/provider.ts +57 -12
  165. package/src/web/search/providers/duckduckgo.ts +279 -0
  166. package/src/web/search/types.ts +2 -0
  167. package/src/modes/theme/dark.json +0 -95
  168. package/src/modes/theme/defaults/alabaster.json +0 -93
  169. package/src/modes/theme/defaults/amethyst.json +0 -96
  170. package/src/modes/theme/defaults/anthracite.json +0 -93
  171. package/src/modes/theme/defaults/basalt.json +0 -91
  172. package/src/modes/theme/defaults/birch.json +0 -95
  173. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  174. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  175. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  176. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  177. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  178. package/src/modes/theme/defaults/dark-copper.json +0 -95
  179. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  180. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  181. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  182. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  183. package/src/modes/theme/defaults/dark-ember.json +0 -95
  184. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  185. package/src/modes/theme/defaults/dark-forest.json +0 -96
  186. package/src/modes/theme/defaults/dark-github.json +0 -105
  187. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  188. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  189. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  190. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  191. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  192. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  193. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  194. package/src/modes/theme/defaults/dark-nord.json +0 -97
  195. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  196. package/src/modes/theme/defaults/dark-one.json +0 -100
  197. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  198. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  199. package/src/modes/theme/defaults/dark-reef.json +0 -91
  200. package/src/modes/theme/defaults/dark-retro.json +0 -92
  201. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  202. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  203. package/src/modes/theme/defaults/dark-slate.json +0 -95
  204. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  205. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  206. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  207. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  208. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  209. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  210. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  211. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  212. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  213. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  214. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  215. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  216. package/src/modes/theme/defaults/graphite.json +0 -92
  217. package/src/modes/theme/defaults/light-arctic.json +0 -107
  218. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  219. package/src/modes/theme/defaults/light-canyon.json +0 -91
  220. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  221. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  222. package/src/modes/theme/defaults/light-coral.json +0 -95
  223. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  224. package/src/modes/theme/defaults/light-dawn.json +0 -90
  225. package/src/modes/theme/defaults/light-dunes.json +0 -91
  226. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  227. package/src/modes/theme/defaults/light-forest.json +0 -100
  228. package/src/modes/theme/defaults/light-frost.json +0 -95
  229. package/src/modes/theme/defaults/light-github.json +0 -115
  230. package/src/modes/theme/defaults/light-glacier.json +0 -91
  231. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  232. package/src/modes/theme/defaults/light-haze.json +0 -90
  233. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  234. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  235. package/src/modes/theme/defaults/light-lavender.json +0 -95
  236. package/src/modes/theme/defaults/light-meadow.json +0 -91
  237. package/src/modes/theme/defaults/light-mint.json +0 -95
  238. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  239. package/src/modes/theme/defaults/light-ocean.json +0 -99
  240. package/src/modes/theme/defaults/light-one.json +0 -99
  241. package/src/modes/theme/defaults/light-opal.json +0 -91
  242. package/src/modes/theme/defaults/light-orchard.json +0 -91
  243. package/src/modes/theme/defaults/light-paper.json +0 -95
  244. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  245. package/src/modes/theme/defaults/light-prism.json +0 -90
  246. package/src/modes/theme/defaults/light-retro.json +0 -98
  247. package/src/modes/theme/defaults/light-sand.json +0 -95
  248. package/src/modes/theme/defaults/light-savanna.json +0 -91
  249. package/src/modes/theme/defaults/light-solarized.json +0 -102
  250. package/src/modes/theme/defaults/light-soleil.json +0 -90
  251. package/src/modes/theme/defaults/light-sunset.json +0 -99
  252. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  253. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  254. package/src/modes/theme/defaults/light-wetland.json +0 -91
  255. package/src/modes/theme/defaults/light-zenith.json +0 -89
  256. package/src/modes/theme/defaults/limestone.json +0 -94
  257. package/src/modes/theme/defaults/mahogany.json +0 -97
  258. package/src/modes/theme/defaults/marble.json +0 -93
  259. package/src/modes/theme/defaults/obsidian.json +0 -91
  260. package/src/modes/theme/defaults/onyx.json +0 -91
  261. package/src/modes/theme/defaults/pearl.json +0 -93
  262. package/src/modes/theme/defaults/porcelain.json +0 -91
  263. package/src/modes/theme/defaults/quartz.json +0 -96
  264. package/src/modes/theme/defaults/sandstone.json +0 -95
  265. package/src/modes/theme/defaults/titanium.json +0 -90
  266. package/src/modes/theme/light.json +0 -93
package/src/lsp/render.ts CHANGED
@@ -103,7 +103,7 @@ export function renderResult(
103
103
  args?: LspParams,
104
104
  ): Component {
105
105
  const content = result.content?.[0];
106
- if (!content || content.type !== "text" || !("text" in content) || !content.text) {
106
+ if (content?.type !== "text" || !("text" in content) || !content.text) {
107
107
  const icon = formatStatusIcon("warning", theme, options.spinnerFrame);
108
108
  const header = `${icon} LSP`;
109
109
  return new Text([header, theme.fg("dim", "No result")].join("\n"), 0, 0);
@@ -272,7 +272,7 @@ async function elicitFromAcpClient(
272
272
  finish(undefined);
273
273
  });
274
274
  const response = await promise;
275
- if (!response || response.action !== "accept" || !response.content) {
275
+ if (response?.action !== "accept" || !response.content) {
276
276
  return undefined;
277
277
  }
278
278
  return response.content.value;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ACP-side `ClientBridge` implementation. Wraps `AgentSideConnection` so the
3
- * `read`/`write`/`bash`/`edit` tools (and the permission gate in
3
+ * `read`/`write`/`bash`/`monitor`/`edit` tools (and the permission gate in
4
4
  * `AgentSession`) can route through the client when it advertises the
5
5
  * relevant capabilities at `initialize` time.
6
6
  */
@@ -121,7 +121,7 @@ function matchAgent(agent: DashboardAgent, query: string): boolean {
121
121
  function extractAssistantText(messages: AgentMessage[]): string | null {
122
122
  for (let i = messages.length - 1; i >= 0; i--) {
123
123
  const message = messages[i];
124
- if (!message || message.role !== "assistant") continue;
124
+ if (message?.role !== "assistant") continue;
125
125
  const blocks = message.content;
126
126
  if (!Array.isArray(blocks)) continue;
127
127
  const text = blocks
@@ -2,6 +2,7 @@ import type { AssistantMessage, ImageContent, Usage } from "@gajae-code/ai";
2
2
  import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@gajae-code/tui";
3
3
  import { formatNumber } from "@gajae-code/utils";
4
4
  import { settings } from "../../config/settings";
5
+ import { renderDeepInterviewAssistantText } from "../../deep-interview/render-middleware";
5
6
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
6
7
  import { isSilentAbort } from "../../session/messages";
7
8
  import { resolveImageOptions } from "../../tools/render-utils";
@@ -153,7 +154,10 @@ export class AssistantMessageComponent extends Container {
153
154
  if (content.type === "text" && content.text.trim()) {
154
155
  // Assistant text messages with no background - trim the text
155
156
  // Set paddingY=0 to avoid extra spacing before tool executions
156
- this.#contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, getMarkdownTheme()));
157
+ const text = content.text.trim();
158
+ this.#contentContainer.addChild(
159
+ renderDeepInterviewAssistantText(text, theme) ?? new Markdown(text, 1, 0, getMarkdownTheme()),
160
+ );
157
161
  } else if (content.type === "thinking" && content.thinking.trim()) {
158
162
  // Add spacing only when another visible assistant content block follows.
159
163
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
@@ -151,7 +151,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
151
151
  const removedLines: { lineNum: string; content: string }[] = [];
152
152
  while (i < lines.length) {
153
153
  const p = parseDiffLine(lines[i]);
154
- if (!p || p.prefix !== "-") break;
154
+ if (p?.prefix !== "-") break;
155
155
  removedLines.push({ lineNum: p.lineNum, content: p.content });
156
156
  i++;
157
157
  }
@@ -159,7 +159,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
159
159
  const addedLines: { lineNum: string; content: string }[] = [];
160
160
  while (i < lines.length) {
161
161
  const p = parseDiffLine(lines[i]);
162
- if (!p || p.prefix !== "+") break;
162
+ if (p?.prefix !== "+") break;
163
163
  addedLines.push({ lineNum: p.lineNum, content: p.content });
164
164
  i++;
165
165
  }
@@ -39,6 +39,7 @@ export interface HookSelectorOptions {
39
39
  * byte-identical to the previous implementation for all consumers.
40
40
  */
41
41
  wrapFocused?: boolean;
42
+ scrollTitleRows?: number;
42
43
  }
43
44
 
44
45
  class OutlinedList extends Container {
@@ -63,6 +64,57 @@ class OutlinedList extends Container {
63
64
  }
64
65
  }
65
66
 
67
+ class ScrollableTitle extends Container {
68
+ #markdown: Markdown;
69
+ #maxRows: number;
70
+ #scrollOffset = 0;
71
+ #lastMaxScrollOffset = 0;
72
+
73
+ constructor(title: string, maxRows: number) {
74
+ super();
75
+ this.#maxRows = Math.max(1, Math.floor(maxRows));
76
+ this.#markdown = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
77
+ }
78
+
79
+ setText(text: string): void {
80
+ this.#markdown.setText(text);
81
+ this.#scrollOffset = 0;
82
+ this.invalidate();
83
+ }
84
+
85
+ scrollBy(rows: number): void {
86
+ if (rows === 0) return;
87
+ const nextOffset = Math.max(0, Math.min(this.#lastMaxScrollOffset, this.#scrollOffset + rows));
88
+ if (nextOffset === this.#scrollOffset) return;
89
+ this.#scrollOffset = nextOffset;
90
+ this.invalidate();
91
+ }
92
+
93
+ render(width: number): string[] {
94
+ const lines = this.#markdown.render(width);
95
+ const maxScrollOffset = Math.max(0, lines.length - this.#maxRows);
96
+ this.#lastMaxScrollOffset = maxScrollOffset;
97
+ this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, maxScrollOffset));
98
+
99
+ const visibleLines = lines.slice(this.#scrollOffset, this.#scrollOffset + this.#maxRows);
100
+ if (maxScrollOffset === 0 || visibleLines.length === 0) {
101
+ return visibleLines;
102
+ }
103
+
104
+ const indicator =
105
+ this.#scrollOffset === 0
106
+ ? theme.fg("dim", " PgDn↓")
107
+ : this.#scrollOffset >= maxScrollOffset
108
+ ? theme.fg("dim", " PgUp↑")
109
+ : theme.fg("dim", " PgUp/PgDn↕");
110
+ const lastIndex = visibleLines.length - 1;
111
+ const availableWidth = Math.max(1, width - visibleWidth(indicator));
112
+ const fittedLine = truncateToWidth(visibleLines[lastIndex] ?? "", availableWidth);
113
+ visibleLines[lastIndex] = `${fittedLine}${indicator}`;
114
+ return visibleLines;
115
+ }
116
+ }
117
+
66
118
  /**
67
119
  * Width-aware list child that owns wrapped focused-option layout.
68
120
  *
@@ -199,7 +251,8 @@ export class HookSelectorComponent extends Container {
199
251
  #focusAwareList: FocusAwareList | undefined;
200
252
  #onSelectCallback: (option: string) => void;
201
253
  #onCancelCallback: () => void;
202
- #titleComponent: Markdown;
254
+ #titleComponent: Markdown | ScrollableTitle;
255
+ #scrollableTitle: ScrollableTitle | undefined;
203
256
  #baseTitle: string;
204
257
  #countdown: CountdownTimer | undefined;
205
258
  #onLeftCallback: (() => void) | undefined;
@@ -207,6 +260,7 @@ export class HookSelectorComponent extends Container {
207
260
  #onExternalEditorCallback: (() => void) | undefined;
208
261
  #wrapFocused: boolean;
209
262
  #outline: boolean;
263
+ #scrollTitleRows: number | undefined;
210
264
  constructor(
211
265
  title: string,
212
266
  options: string[],
@@ -231,7 +285,15 @@ export class HookSelectorComponent extends Container {
231
285
  this.addChild(new DynamicBorder());
232
286
  this.addChild(new Spacer(1));
233
287
 
234
- this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
288
+ const scrollTitleRows =
289
+ opts?.scrollTitleRows === undefined ? undefined : Math.max(1, Math.floor(opts.scrollTitleRows));
290
+ this.#scrollTitleRows = scrollTitleRows;
291
+ if (scrollTitleRows === undefined) {
292
+ this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
293
+ } else {
294
+ this.#scrollableTitle = new ScrollableTitle(title, scrollTitleRows);
295
+ this.#titleComponent = this.#scrollableTitle;
296
+ }
235
297
  this.addChild(this.#titleComponent);
236
298
  this.addChild(new Spacer(1));
237
299
 
@@ -319,6 +381,14 @@ export class HookSelectorComponent extends Container {
319
381
  // Reset countdown on any interaction
320
382
  this.#countdown?.reset();
321
383
 
384
+ if (this.#scrollTitleRows !== undefined && matchesKey(keyData, "pageUp")) {
385
+ this.#scrollableTitle?.scrollBy(-this.#scrollTitleRows);
386
+ return;
387
+ }
388
+ if (this.#scrollTitleRows !== undefined && matchesKey(keyData, "pageDown")) {
389
+ this.#scrollableTitle?.scrollBy(this.#scrollTitleRows);
390
+ return;
391
+ }
322
392
  if (matchesKey(keyData, "up") || keyData === "k") {
323
393
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
324
394
  this.#updateList();
@@ -1,4 +1,8 @@
1
- import type { SkillActiveEntry, WorkflowHudChip } from "../../../skill-state/active-state";
1
+ import {
2
+ collapsePlanningPipeline,
3
+ type SkillActiveEntry,
4
+ type WorkflowHudChip,
5
+ } from "../../../skill-state/active-state";
2
6
  import { workflowReceiptStatus } from "../../../skill-state/workflow-state-contract";
3
7
 
4
8
  const ANSI_RESET_FG = "\x1b[39m";
@@ -69,7 +73,8 @@ function formatEntry(entry: SkillActiveEntry): string {
69
73
  }
70
74
 
71
75
  export function renderSkillHudBar(entries: readonly SkillActiveEntry[], width: number): string | null {
72
- const active = entries.filter(entry => entry.active !== false && sanitizeHudPart(entry.skill)).sort(compareEntries);
76
+ const visible = collapsePlanningPipeline(entries.filter(entry => entry.active !== false));
77
+ const active = visible.filter(entry => sanitizeHudPart(entry.skill)).sort(compareEntries);
73
78
  if (active.length === 0 || width <= 0) return null;
74
79
  const body = active.map(formatEntry).join(" + ");
75
80
  const prefix = `${ANSI_BORDER}◆${ANSI_RESET_FG} ${ANSI_BOLD}${ANSI_ACCENT}hud${ANSI_RESET_FG}${ANSI_RESET_BOLD} `;
@@ -1,6 +1,7 @@
1
1
  import { INTENT_FIELD } from "@gajae-code/agent-core";
2
2
  import { calculatePromptTokens } from "@gajae-code/agent-core/compaction/compaction";
3
3
  import type { AssistantMessage, ImageContent } from "@gajae-code/ai";
4
+ import { parseRateLimitReason } from "@gajae-code/ai";
4
5
  import { type Component, Loader, TERMINAL, Text } from "@gajae-code/tui";
5
6
  import { settings } from "../../config/settings";
6
7
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
@@ -24,6 +25,24 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
24
25
 
25
26
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
26
27
 
28
+ function friendlyRetryReason(errorMessage: string | undefined): string {
29
+ if (!errorMessage) return "";
30
+ switch (parseRateLimitReason(errorMessage)) {
31
+ case "RATE_LIMIT_EXCEEDED":
32
+ return "rate limited";
33
+ case "QUOTA_EXHAUSTED":
34
+ return "usage limit";
35
+ case "MODEL_CAPACITY_EXHAUSTED":
36
+ return "overloaded";
37
+ case "SERVER_ERROR":
38
+ return "server error";
39
+ default:
40
+ return /network|connection|socket|fetch failed|terminated|timeout|timed out|stream/i.test(errorMessage)
41
+ ? "connection error"
42
+ : "transient error";
43
+ }
44
+ }
45
+
27
46
  type AgentSessionEventHandlers = {
28
47
  [E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
29
48
  };
@@ -71,6 +90,15 @@ export class EventController {
71
90
 
72
91
  dispose(): void {
73
92
  this.#cancelIdleCompaction();
93
+ this.#clearRetryCountdown();
94
+ if (this.ctx.retryEscapeHandler) {
95
+ this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
96
+ this.ctx.retryEscapeHandler = undefined;
97
+ }
98
+ if (this.ctx.retryLoader) {
99
+ this.ctx.retryLoader.stop();
100
+ this.ctx.retryLoader = undefined;
101
+ }
74
102
  for (const timer of this.#ircExpiryTimers.values()) {
75
103
  clearTimeout(timer);
76
104
  }
@@ -166,6 +194,7 @@ export class EventController {
166
194
  }
167
195
  if (this.ctx.retryLoader) {
168
196
  this.ctx.retryLoader.stop();
197
+ this.#clearRetryCountdown();
169
198
  this.ctx.retryLoader = undefined;
170
199
  this.ctx.statusContainer.clear();
171
200
  }
@@ -648,21 +677,56 @@ export class EventController {
648
677
  this.ctx.ui.requestRender();
649
678
  }
650
679
 
680
+ #clearRetryCountdown(): void {
681
+ if (this.ctx.retryCountdownTimer) {
682
+ clearInterval(this.ctx.retryCountdownTimer);
683
+ this.ctx.retryCountdownTimer = undefined;
684
+ }
685
+ }
686
+
651
687
  async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
652
- this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
688
+ // Preserve the ORIGINAL editor Escape handler across repeated retry
689
+ // starts: auto_retry_end only fires at final success/failure, so a
690
+ // second auto_retry_start must not snapshot the prior retry handler.
691
+ if (!this.ctx.retryEscapeHandler) {
692
+ this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
693
+ }
694
+ let escPressed = false;
653
695
  this.ctx.editor.onEscape = () => {
654
- this.ctx.session.abortRetry();
696
+ if (!escPressed) {
697
+ // First Esc: skip the backoff and retry immediately.
698
+ escPressed = true;
699
+ this.ctx.session.retryNow();
700
+ } else {
701
+ // Second Esc: cancel the retry entirely.
702
+ this.ctx.session.abortRetry();
703
+ }
655
704
  };
656
705
  this.ctx.statusContainer.clear();
657
- const delaySeconds = Math.round(event.delayMs / 1000);
658
- this.ctx.retryLoader = new Loader(
706
+ // Stop any prior retry loader/timer before installing a new one.
707
+ this.ctx.retryLoader?.stop();
708
+ this.#clearRetryCountdown();
709
+ const reason = friendlyRetryReason(event.errorMessage);
710
+ const attemptLabel = event.unbounded ? `attempt ${event.attempt}` : `${event.attempt}/${event.maxAttempts}`;
711
+ const reasonSuffix = reason ? ` — ${reason}` : "";
712
+ const deadline = Date.now() + event.delayMs;
713
+ const buildMessage = () => {
714
+ const remainingSeconds = Math.max(0, Math.round((deadline - Date.now()) / 1000));
715
+ // First Esc retries immediately; a second Esc cancels.
716
+ return `Retrying (${attemptLabel})${reasonSuffix}, next in ${remainingSeconds}s… (esc to retry now)`;
717
+ };
718
+ const retryLoader = new Loader(
659
719
  this.ctx.ui,
660
720
  spinner => theme.fg("warning", spinner),
661
721
  text => theme.fg("muted", text),
662
- `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s… (esc to cancel)`,
722
+ buildMessage(),
663
723
  getSymbolTheme().spinnerFrames,
664
724
  );
665
- this.ctx.statusContainer.addChild(this.ctx.retryLoader);
725
+ this.ctx.retryLoader = retryLoader;
726
+ this.ctx.retryCountdownTimer = setInterval(() => {
727
+ retryLoader.setMessage(buildMessage());
728
+ }, 1000);
729
+ this.ctx.statusContainer.addChild(retryLoader);
666
730
  this.ctx.ui.requestRender();
667
731
  }
668
732
 
@@ -673,6 +737,7 @@ export class EventController {
673
737
  }
674
738
  if (this.ctx.retryLoader) {
675
739
  this.ctx.retryLoader.stop();
740
+ this.#clearRetryCountdown();
676
741
  this.ctx.retryLoader = undefined;
677
742
  this.ctx.statusContainer.clear();
678
743
  }
@@ -590,6 +590,11 @@ export class ExtensionUiController {
590
590
  dialogOptions?.signal,
591
591
  );
592
592
  const maxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
593
+ const requestedTitleRows = dialogOptions?.scrollTitleRows;
594
+ const selectorChromeRows = 7;
595
+ const availableTitleRows = this.ctx.ui.terminal.rows - maxVisible - selectorChromeRows;
596
+ const scrollTitleRows =
597
+ requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
593
598
  this.ctx.hookSelector = new HookSelectorComponent(
594
599
  title,
595
600
  options,
@@ -624,6 +629,7 @@ export class ExtensionUiController {
624
629
  tui: this.ctx.ui,
625
630
  outline: dialogOptions?.outline,
626
631
  wrapFocused: dialogOptions?.wrapFocused,
632
+ scrollTitleRows,
627
633
  maxVisible,
628
634
  },
629
635
  );
@@ -31,7 +31,7 @@ export class InputController {
31
31
  constructor(private ctx: InteractiveModeContext) {}
32
32
 
33
33
  #abortInteractive(): Promise<void> {
34
- return this.ctx.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS });
34
+ return this.ctx.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS, cause: "user_interrupt" });
35
35
  }
36
36
 
37
37
  setupKeyHandlers(): void {
@@ -64,6 +64,7 @@ export class InputController {
64
64
  } else if (this.ctx.isBashMode) {
65
65
  this.ctx.editor.setText("");
66
66
  this.ctx.isBashMode = false;
67
+ this.ctx.isBashNoContext = false;
67
68
  this.ctx.updateEditorBorderColor();
68
69
  } else if (this.ctx.session.isEvalRunning) {
69
70
  this.ctx.session.abortEval();
@@ -172,11 +173,17 @@ export class InputController {
172
173
 
173
174
  this.ctx.editor.onChange = (text: string) => {
174
175
  const wasBashMode = this.ctx.isBashMode;
176
+ const wasBashNoContext = this.ctx.isBashNoContext;
175
177
  const wasPythonMode = this.ctx.isPythonMode;
176
178
  const trimmed = text.trimStart();
177
- this.ctx.isBashMode = text.trimStart().startsWith("!");
179
+ this.ctx.isBashMode = trimmed.startsWith("!");
180
+ this.ctx.isBashNoContext = trimmed.startsWith("!!");
178
181
  this.ctx.isPythonMode = trimmed.startsWith("$") && !trimmed.startsWith("${");
179
- if (wasBashMode !== this.ctx.isBashMode || wasPythonMode !== this.ctx.isPythonMode) {
182
+ if (
183
+ wasBashMode !== this.ctx.isBashMode ||
184
+ wasBashNoContext !== this.ctx.isBashNoContext ||
185
+ wasPythonMode !== this.ctx.isPythonMode
186
+ ) {
180
187
  this.ctx.updateEditorBorderColor();
181
188
  }
182
189
  };
@@ -260,6 +267,7 @@ export class InputController {
260
267
  this.ctx.editor.addToHistory(text);
261
268
  await this.ctx.handleBashCommand(command, isExcluded);
262
269
  this.ctx.isBashMode = false;
270
+ this.ctx.isBashNoContext = false;
263
271
  this.ctx.updateEditorBorderColor();
264
272
  return;
265
273
  }
@@ -560,6 +568,14 @@ export class InputController {
560
568
  this.ctx.retryLoader.stop();
561
569
  this.ctx.retryLoader = undefined;
562
570
  }
571
+ if (this.ctx.retryCountdownTimer) {
572
+ clearInterval(this.ctx.retryCountdownTimer);
573
+ this.ctx.retryCountdownTimer = undefined;
574
+ }
575
+ if (this.ctx.retryEscapeHandler) {
576
+ this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
577
+ this.ctx.retryEscapeHandler = undefined;
578
+ }
563
579
  this.ctx.statusContainer.clear();
564
580
  this.ctx.statusLine.dispose();
565
581
 
@@ -57,12 +57,13 @@ import { TreeSelectorComponent } from "../components/tree-selector";
57
57
  import { UserMessageSelectorComponent } from "../components/user-message-selector";
58
58
  import type { SessionObserverRegistry } from "../session-observer-registry";
59
59
 
60
- const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
60
+ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
61
61
  "anthropic",
62
62
  "openai-codex",
63
63
  "gitlab-duo",
64
64
  "google-gemini-cli",
65
65
  "google-antigravity",
66
+ "xai",
66
67
  ]);
67
68
 
68
69
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
@@ -239,7 +240,7 @@ export class SelectorController {
239
240
  });
240
241
  },
241
242
  );
242
- return { component: selector, focus: selector };
243
+ return { component: selector, focus: selector.getSelectList() };
243
244
  });
244
245
  });
245
246
  }
@@ -112,12 +112,23 @@ const HINT_SHIMMER_PALETTE: ShimmerPalette = {
112
112
  high: "borderAccent",
113
113
  };
114
114
 
115
+ function getDefaultInputPrefix(): string {
116
+ return `${theme.fg("accent", ">")} `;
117
+ }
118
+
119
+ function getShellInputPrefix(isNoContext: boolean): string {
120
+ const shellLabel = isNoContext
121
+ ? theme.fg("warning", theme.bold("shell no-context"))
122
+ : theme.fg("bashMode", theme.bold("shell"));
123
+ return `${shellLabel} ${getDefaultInputPrefix()}`;
124
+ }
125
+
115
126
  function configureDefaultComposerChrome(editor: CustomEditor): void {
116
127
  editor.setBorderVisible(true);
117
128
  editor.setBorderStyle("sharp");
118
129
  editor.setClosedBorderBox(true);
119
130
  editor.setPromptGutter(undefined);
120
- editor.setInputPrefix(`${theme.fg("accent", ">")} `);
131
+ editor.setInputPrefix(getDefaultInputPrefix());
121
132
  editor.setPlaceholder("Type your message...");
122
133
  editor.setPaddingX(1);
123
134
  editor.setTopBorder(undefined);
@@ -236,6 +247,7 @@ export class InteractiveMode implements InteractiveModeContext {
236
247
  isInitialized = false;
237
248
  isBackgrounded = false;
238
249
  isBashMode = false;
250
+ isBashNoContext = false;
239
251
  toolOutputExpanded = false;
240
252
  todoExpanded = false;
241
253
  planModeEnabled = false;
@@ -264,6 +276,7 @@ export class InteractiveMode implements InteractiveModeContext {
264
276
  }
265
277
  autoCompactionEscapeHandler?: () => void;
266
278
  retryEscapeHandler?: () => void;
279
+ retryCountdownTimer?: ReturnType<typeof setInterval>;
267
280
  unsubscribe?: () => void;
268
281
  onInputCallback?: (input: SubmittedUserInput) => void;
269
282
  optimisticUserMessageSignature: string | undefined = undefined;
@@ -808,7 +821,10 @@ export class InteractiveMode implements InteractiveModeContext {
808
821
 
809
822
  updateEditorChrome(): void {
810
823
  if (this.isBashMode) {
811
- this.editor.borderColor = theme.getBashModeBorderColor();
824
+ this.editor.borderColor = this.isBashNoContext
825
+ ? (str: string) => theme.fg("warning", str)
826
+ : theme.getBashModeBorderColor();
827
+ this.editor.setInputPrefix(getShellInputPrefix(this.isBashNoContext));
812
828
  } else if (this.isPythonMode) {
813
829
  this.editor.borderColor = theme.getPythonModeBorderColor();
814
830
  } else {
@@ -823,6 +839,9 @@ export class InteractiveMode implements InteractiveModeContext {
823
839
  this.editor.borderColor = theme.getThinkingBorderColor(level);
824
840
  }
825
841
  }
842
+ if (!this.isBashMode) {
843
+ this.editor.setInputPrefix(getDefaultInputPrefix());
844
+ }
826
845
  this.#setComposerTopBorder();
827
846
  this.ui.requestRender();
828
847
  }