@gajae-code/coding-agent 0.15.5 → 0.16.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 (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Narrow-width survival row for the status rail.
3
+ *
4
+ * The rail evicts segments continuously as the terminal narrows. Without this
5
+ * row the last thing left at very small widths is the `…+N` overflow marker —
6
+ * the three things a user actually needs (how much context is left, whether a
7
+ * goal is running, which model is answering) are exactly what the generic
8
+ * eviction throws away first or last by accident.
9
+ *
10
+ * This row keeps them, in priority order, by stepping each item down its own
11
+ * ladder instead of dropping it:
12
+ *
13
+ * full form -> shortened form -> single glyph -> dropped
14
+ *
15
+ * Context % is never dropped and is the last survivor; the model name is
16
+ * dropped first of the three. The row is rendered as bare colored text with no
17
+ * group padding or end caps, so — like the overflow marker — it can survive at
18
+ * widths where a normal segment group cannot.
19
+ */
20
+
21
+ import { visibleWidth } from "@gajae-code/tui";
22
+ import { formatNumber } from "@gajae-code/utils";
23
+ import { theme } from "../../theme/theme";
24
+ import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
25
+ import { shortenModelId } from "./model-name";
26
+ import { goalStatusDisplay } from "./segments";
27
+ import type { SegmentContext } from "./types";
28
+
29
+ const SEPARATOR = "·";
30
+
31
+ /** Which priority items the active layout is supposed to be carrying. */
32
+ export interface PriorityItemSet {
33
+ context: boolean;
34
+ goal: boolean;
35
+ model: boolean;
36
+ }
37
+
38
+ function contextForms(ctx: SegmentContext): { full: string; short: string } | null {
39
+ const window = ctx.contextWindow;
40
+ if (!(window > 0)) return null;
41
+
42
+ const raw = ctx.contextPercent;
43
+ const pct = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
44
+ const color = pct === null ? "statusLineContext" : getContextUsageThemeColor(getContextUsageLevel(pct, window));
45
+
46
+ return {
47
+ full: theme.fg(color, `${pct === null ? "?" : `${pct.toFixed(1)}%`}/${formatNumber(window)}`),
48
+ short: theme.fg(color, pct === null ? "?" : `${Math.round(pct)}%`),
49
+ };
50
+ }
51
+
52
+ function goalForms(ctx: SegmentContext): { label: string; glyph: string } | null {
53
+ const display = goalStatusDisplay(ctx);
54
+ if (!display) return null;
55
+
56
+ const glyph = display.icon || "G";
57
+ return {
58
+ label: theme.fg(display.color, display.icon ? `${display.icon} Goal` : "Goal"),
59
+ glyph: theme.fg(display.color, glyph),
60
+ };
61
+ }
62
+
63
+ function modelForms(ctx: SegmentContext): { short: string; glyph: string } {
64
+ const model = ctx.session.state.model;
65
+ const name = shortenModelId(model?.id ?? model?.name);
66
+ return {
67
+ short: theme.fg("statusLineModel", name),
68
+ glyph: theme.fg("statusLineModel", theme.icon.model || name.slice(0, 1)),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Widest priority row that fits `width`, or null when even the minimal form
74
+ * cannot fit (callers then fall back to the overflow marker).
75
+ *
76
+ * Returns null when neither context nor goal is available: the model name alone
77
+ * is the lowest-priority item and never justifies replacing the normal rail.
78
+ */
79
+ export function buildPriorityRow(ctx: SegmentContext, width: number, include: PriorityItemSet): string | null {
80
+ if (width <= 0) return null;
81
+
82
+ const context = include.context ? contextForms(ctx) : null;
83
+ const goal = include.goal ? goalForms(ctx) : null;
84
+ const model = include.model ? modelForms(ctx) : null;
85
+ if (!context && !goal) return null;
86
+
87
+ // Ordered widest-first. Every item degrades through its own forms before any
88
+ // item is dropped, and drops run lowest-priority first (model, then goal).
89
+ const ladder: readonly (readonly (string | null)[])[] = [
90
+ [context?.full ?? null, goal?.label ?? null, model?.short ?? null],
91
+ [context?.full ?? null, goal?.glyph ?? null, model?.short ?? null],
92
+ [context?.full ?? null, goal?.glyph ?? null, model?.glyph ?? null],
93
+ [context?.short ?? null, goal?.glyph ?? null, model?.glyph ?? null],
94
+ [context?.short ?? null, goal?.glyph ?? null, null],
95
+ [context?.short ?? null, null, null],
96
+ ];
97
+
98
+ const separator = theme.fg("dim", SEPARATOR);
99
+ for (const candidate of ladder) {
100
+ const parts = candidate.filter((part): part is string => part !== null);
101
+ if (parts.length === 0) continue;
102
+ const row = parts.join(separator);
103
+ if (visibleWidth(row) <= width) return row;
104
+ }
105
+
106
+ return null;
107
+ }
@@ -8,6 +8,7 @@ import { shortenPath } from "../../../tools/render-utils";
8
8
  import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
9
9
  import { sanitizeStatusText } from "../../shared";
10
10
  import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
11
+ import { shortenModelId } from "./model-name";
11
12
  import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
12
13
 
13
14
  export type { SegmentContext } from "./types";
@@ -99,10 +100,10 @@ const modelSegment: StatusLineSegment = {
99
100
  const state = ctx.session.state;
100
101
  const opts = ctx.options.model ?? {};
101
102
 
102
- let modelName = state.model?.name || state.model?.id || "no-model";
103
- if (modelName.startsWith("Claude ")) {
104
- modelName = modelName.slice(7);
105
- }
103
+ // Shortest recognizable id-derived form (`sonnet-4.5`), the same label the
104
+ // footer renders, so a narrow rail never spends cells on a provider prefix
105
+ // or a snapshot date.
106
+ const modelName = shortenModelId(state.model?.id ?? state.model?.name);
106
107
 
107
108
  let content = withIcon(theme.icon.model, modelName);
108
109
 
@@ -148,9 +149,16 @@ function formatGoalUsage(current: number): string {
148
149
  return used;
149
150
  }
150
151
 
151
- function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: boolean }): RenderedSegment {
152
- const goal = ctx.session.getGoalModeState()?.goal;
153
- const status = goal?.status ?? (mode.paused ? "paused" : "active");
152
+ /**
153
+ * Icon and color for the active goal indicator, or null when goal mode is not
154
+ * showing. Shared with the narrow-width priority row so the compact glyph keeps
155
+ * the same status color as the full label.
156
+ */
157
+ export function goalStatusDisplay(ctx: SegmentContext): { icon: string; color: ThemeColor } | null {
158
+ const mode = ctx.goalMode;
159
+ if (!mode || (!mode.enabled && !mode.paused)) return null;
160
+
161
+ const status = ctx.session.getGoalModeState?.()?.goal?.status ?? (mode.paused ? "paused" : "active");
154
162
 
155
163
  let icon: string = theme.icon.goal;
156
164
  let color: ThemeColor = "accent";
@@ -170,13 +178,19 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
170
178
  default:
171
179
  break;
172
180
  }
181
+ return { icon, color };
182
+ }
173
183
 
174
- const parts: string[] = [withIcon(icon, "Goal")];
184
+ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: boolean }): RenderedSegment {
185
+ const display = goalStatusDisplay(ctx) ?? { icon: theme.icon.goal, color: "accent" as ThemeColor };
186
+ const goal = ctx.session.getGoalModeState?.()?.goal;
187
+
188
+ const parts: string[] = [withIcon(display.icon, "Goal")];
175
189
  const showUsage = ctx.session.settings.get("goal.statusInFooter") === true;
176
190
  if (showUsage && goal) {
177
191
  parts.push(formatGoalUsage(goal.tokensUsed));
178
192
  }
179
- return { content: theme.fg(color, parts.join(" ")), visible: true };
193
+ return { content: theme.fg(display.color, parts.join(" ")), visible: mode.enabled || mode.paused };
180
194
  }
181
195
 
182
196
  const modeSegment: StatusLineSegment = {
@@ -307,7 +321,9 @@ const jobsSegment: StatusLineSegment = {
307
321
  id: "jobs",
308
322
  render(ctx) {
309
323
  const { jobs } = ctx;
310
- const visible = jobs.activeMonitorCount > 0 || jobs.activeCronCount > 0 || jobs.worstState === "failed";
324
+ const foldedJobCount = jobs.foldedJobs?.filter(job => job.backgrounded).length ?? 0;
325
+ const visible =
326
+ jobs.activeMonitorCount > 0 || jobs.activeCronCount > 0 || foldedJobCount > 0 || jobs.worstState === "failed";
311
327
  if (!visible) {
312
328
  return { content: "", visible: false };
313
329
  }
@@ -318,6 +334,9 @@ const jobsSegment: StatusLineSegment = {
318
334
  if (jobs.activeCronCount > 0) {
319
335
  parts.push(withIcon(theme.icon.time, `${jobs.activeCronCount}`));
320
336
  }
337
+ if (foldedJobCount > 0) {
338
+ parts.push(withIcon(theme.icon.pause || theme.icon.agents, `${foldedJobCount} folded`));
339
+ }
321
340
  if (parts.length === 0) {
322
341
  // Nothing active but a failure is unacknowledged — keep a drill-in marker.
323
342
  parts.push(withIcon(theme.icon.warning, "jobs"));
@@ -28,6 +28,7 @@ import {
28
28
  resolveCurrentBranch,
29
29
  } from "./status-line/git-utils";
30
30
  import { getPreset } from "./status-line/presets";
31
+ import { buildPriorityRow, type PriorityItemSet } from "./status-line/priority-row";
31
32
  import { renderSegment, type SegmentContext } from "./status-line/segments";
32
33
  import { getSeparator } from "./status-line/separators";
33
34
  import { calculateTokensPerSecond } from "./status-line/token-rate";
@@ -132,6 +133,8 @@ interface CollectedStatusSegments {
132
133
  left: string[];
133
134
  leftSegIds: StatusLineSegmentId[];
134
135
  right: string[];
136
+ /** Parallel to `right`; null for action hints, job counts and the version tag. */
137
+ rightSegIds: (StatusLineSegmentId | null)[];
135
138
  previewHighlightSegment: StatusLineSegmentId | undefined;
136
139
  sessionAccent: boolean | undefined;
137
140
  leftSepWidth: number;
@@ -819,6 +822,7 @@ export class StatusLineComponent implements Component {
819
822
  }
820
823
 
821
824
  const right: string[] = [];
825
+ const rightSegIds: (StatusLineSegmentId | null)[] = [];
822
826
  const actionHints =
823
827
  effectiveSettings.showActionHints === false
824
828
  ? []
@@ -833,9 +837,13 @@ export class StatusLineComponent implements Component {
833
837
  const rendered = renderSegment(segId, ctx);
834
838
  if (rendered.visible && rendered.content) {
835
839
  right.push(highlightSegment(segId, rendered.content));
840
+ rightSegIds.push(segId);
836
841
  }
837
842
  }
838
- right.push(...actionHints.map(hint => hint.content));
843
+ for (const hint of actionHints) {
844
+ right.push(hint.content);
845
+ rightSegIds.push(null);
846
+ }
839
847
 
840
848
  const runningBackgroundJobs =
841
849
  this.session.getAsyncJobSnapshot()?.running.filter(job => job.metadata?.monitor !== true).length ?? 0;
@@ -843,9 +851,11 @@ export class StatusLineComponent implements Component {
843
851
  const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
844
852
  const label = `${formatCount("job", runningBackgroundJobs)} running`;
845
853
  right.push(theme.fg("statusLineSubagents", `${icon}${label}`));
854
+ rightSegIds.push(null);
846
855
  }
847
856
  if (this.#version) {
848
857
  right.push(theme.fg("dim", `v${this.#version}`));
858
+ rightSegIds.push(null);
849
859
  }
850
860
 
851
861
  return {
@@ -857,6 +867,7 @@ export class StatusLineComponent implements Component {
857
867
  left,
858
868
  leftSegIds,
859
869
  right,
870
+ rightSegIds,
860
871
  previewHighlightSegment,
861
872
  sessionAccent: effectiveSettings.sessionAccent,
862
873
  leftSepWidth: visibleWidth(separatorDef.left),
@@ -872,22 +883,108 @@ export class StatusLineComponent implements Component {
872
883
  return Math.max(1, Math.min(3, Math.trunc(raw)));
873
884
  }
874
885
 
886
+ /**
887
+ * Which priority items this layout is carrying, and where.
888
+ *
889
+ * Context % has no segment of its own in most presets — it rides inside the
890
+ * `model` segment — so `model` inherits the context rank whenever it does.
891
+ * Items whose data is absent (no context window, no active goal) carry no
892
+ * priority at all, which keeps a rail with nothing to protect evicting
893
+ * exactly as it always did.
894
+ */
895
+ #priorityItems(seg: CollectedStatusSegments): {
896
+ include: PriorityItemSet;
897
+ inlineContextPct: boolean;
898
+ } {
899
+ const collected = (id: StatusLineSegmentId): boolean =>
900
+ seg.leftSegIds.includes(id) || seg.rightSegIds.includes(id);
901
+ const inlineContextPct =
902
+ seg.ctx.options.model?.showContextPercent !== false && seg.ctx.contextPctSegmentActive !== true;
903
+ const goalMode = seg.ctx.goalMode;
904
+
905
+ return {
906
+ inlineContextPct,
907
+ include: {
908
+ context:
909
+ seg.ctx.contextWindow > 0 && (collected("context_pct") || (collected("model") && inlineContextPct)),
910
+ goal: collected("mode") && goalMode !== null && (goalMode.enabled || goalMode.paused),
911
+ model: collected("model"),
912
+ },
913
+ };
914
+ }
915
+
916
+ /**
917
+ * Eviction rank: 0 is ordinary telemetry and goes first, 3 is context % and
918
+ * goes last. The model name outranks telemetry but loses to the goal
919
+ * indicator, matching context % > goal > model.
920
+ */
921
+ #priorityRanker(seg: CollectedStatusSegments): (id: StatusLineSegmentId | null) => number {
922
+ const { include, inlineContextPct } = this.#priorityItems(seg);
923
+ const anyPriority = include.context || include.goal;
924
+
925
+ return (id: StatusLineSegmentId | null): number => {
926
+ if (id === null || !anyPriority) return 0;
927
+ if (id === "context_pct") return include.context ? 3 : 0;
928
+ if (id === "model") {
929
+ if (include.context && inlineContextPct) return 3;
930
+ return include.model ? 1 : 0;
931
+ }
932
+ if (id === "mode") return include.goal ? 2 : 0;
933
+ return 0;
934
+ };
935
+ }
936
+
937
+ /**
938
+ * Priority row for a layout that could not keep everything the user needs on
939
+ * screen, or null when the normal rail already carries context %, the goal
940
+ * indicator and the model name.
941
+ *
942
+ * Context % has no segment of its own in most presets — it rides inside the
943
+ * `model` segment — so losing `model` silently loses the highest-priority
944
+ * item too. Survival is therefore resolved per item, not per segment.
945
+ */
946
+ #priorityRowFor(
947
+ seg: CollectedStatusSegments,
948
+ survivingLeftIds: readonly (StatusLineSegmentId | null)[],
949
+ survivingRightIds: readonly (StatusLineSegmentId | null)[],
950
+ width: number,
951
+ ): string | null {
952
+ const survived = (id: StatusLineSegmentId): boolean =>
953
+ survivingLeftIds.includes(id) || survivingRightIds.includes(id);
954
+
955
+ const { include, inlineContextPct } = this.#priorityItems(seg);
956
+ if (!include.context && !include.goal) return null;
957
+
958
+ const contextLost = include.context && !survived("context_pct") && !(inlineContextPct && survived("model"));
959
+ const goalLost = include.goal && !survived("mode");
960
+ const modelLost = include.model && !survived("model");
961
+ if (!contextLost && !goalLost && !modelLost) return null;
962
+
963
+ return buildPriorityRow(seg.ctx, width, include);
964
+ }
965
+
875
966
  #buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
876
967
  const seg = precollected ?? this.#collectStatusSegments(width, this.#resolveSettings());
877
968
  const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
878
969
  const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
879
970
  const topFillWidth = Math.max(0, width);
880
971
 
972
+ const rank = this.#priorityRanker(seg);
973
+
881
974
  /**
882
- * Evict against `budget` using the existing configured-order semantics
883
- * (shrink path, then pop right, then pop left) and report how many
884
- * segments were dropped. Runs from the original segment lists each time so
885
- * the marker reservation can be recomputed without compounding evictions.
975
+ * Evict against `budget`: shrink the path first, then drop the
976
+ * lowest-priority segment still standing — right side before left, tail
977
+ * first within a side, which is the historical order for everything that
978
+ * carries no priority. Context %, the goal indicator and the model name
979
+ * outrank ordinary telemetry and are therefore the last to go. Runs from
980
+ * the original segment lists each time so the marker reservation can be
981
+ * recomputed without compounding evictions.
886
982
  */
887
983
  const layout = (budget: number) => {
888
984
  const left = [...seg.left];
889
985
  const leftIds = [...seg.leftSegIds];
890
986
  const right = [...seg.right];
987
+ const rightIds = [...seg.rightSegIds];
891
988
  let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
892
989
  let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
893
990
  const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
@@ -905,19 +1002,42 @@ export class StatusLineComponent implements Component {
905
1002
  leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
906
1003
  }
907
1004
  }
908
- while (totalWidth() > budget && right.length > 0) {
909
- right.pop();
910
- dropped += 1;
911
- rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
912
- }
913
- while (totalWidth() > budget && left.length > 0) {
914
- left.pop();
915
- leftIds.pop();
1005
+ // Lowest rank loses first; equal ranks fall back to the historical
1006
+ // right-then-left, tail-first order.
1007
+ while (totalWidth() > budget && (left.length > 0 || right.length > 0)) {
1008
+ let victimSide: "left" | "right" | null = null;
1009
+ let victimIndex = -1;
1010
+ let victimRank = Number.POSITIVE_INFINITY;
1011
+ for (let index = right.length - 1; index >= 0; index -= 1) {
1012
+ const candidate = rank(rightIds[index]);
1013
+ if (candidate < victimRank) {
1014
+ victimRank = candidate;
1015
+ victimSide = "right";
1016
+ victimIndex = index;
1017
+ }
1018
+ }
1019
+ for (let index = left.length - 1; index >= 0; index -= 1) {
1020
+ const candidate = rank(leftIds[index]);
1021
+ if (candidate < victimRank) {
1022
+ victimRank = candidate;
1023
+ victimSide = "left";
1024
+ victimIndex = index;
1025
+ }
1026
+ }
1027
+ if (victimSide === null) break;
1028
+ if (victimSide === "right") {
1029
+ right.splice(victimIndex, 1);
1030
+ rightIds.splice(victimIndex, 1);
1031
+ rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
1032
+ } else {
1033
+ left.splice(victimIndex, 1);
1034
+ leftIds.splice(victimIndex, 1);
1035
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
1036
+ }
916
1037
  dropped += 1;
917
- leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
918
1038
  }
919
1039
  }
920
- return { left, right, leftWidth, rightWidth, dropped };
1040
+ return { left, right, leftIds, rightIds, leftWidth, rightWidth, dropped };
921
1041
  };
922
1042
 
923
1043
  // First pass with the full budget establishes whether anything is lost at
@@ -939,6 +1059,11 @@ export class StatusLineComponent implements Component {
939
1059
  }
940
1060
  }
941
1061
  const marker = this.#renderOverflowMarker(placed.dropped, reserved);
1062
+ // Nothing survived that the user actually needs? Spend the rail on the
1063
+ // priority row instead of an overflow marker. Suppressing the marker here
1064
+ // is deliberate: at these widths its cells are worth more as context %.
1065
+ const priorityRow = this.#priorityRowFor(seg, placed.leftIds, placed.rightIds, topFillWidth);
1066
+ if (priorityRow !== null) return priorityRow;
942
1067
 
943
1068
  const leftGroup = this.#renderStatusGroup(placed.left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
944
1069
  const rightGroup = this.#renderStatusGroup(placed.right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
@@ -1078,6 +1203,17 @@ export class StatusLineComponent implements Component {
1078
1203
  if (row.length === 0) packedRows.splice(index, 1);
1079
1204
  }
1080
1205
 
1206
+ // Wrapping is supposed to be the lossless path. When even `maxRows` rows
1207
+ // cannot hold everything, the priority row beats a partial rail plus a
1208
+ // count of what is missing.
1209
+ if (dropped > 0) {
1210
+ const priorityRow = this.#priorityRowFor(seg, [], [], topFillWidth);
1211
+ if (priorityRow !== null) {
1212
+ this.#renderedRowsCache = { key: cacheKey, rows: [priorityRow] };
1213
+ return [priorityRow];
1214
+ }
1215
+ }
1216
+
1081
1217
  // Reserve marker cells on whichever row ends up last, evicting from its
1082
1218
  // tail until the cue fits. Never opens a new row: that would contradict
1083
1219
  // maxRows. Emptying that row is fine -- the marker then occupies it alone.
@@ -312,7 +312,7 @@ export class ExtensionUiController {
312
312
  session.retryNow();
313
313
  return { retried: true, immediate: true };
314
314
  case "bash.background":
315
- if (!session.requestForegroundBashBackground())
315
+ if (!(await session.requestForegroundBashBackground()))
316
316
  throw Object.assign(new Error("The active bash command cannot be moved to a managed background job."), {
317
317
  code: "not_foldable",
318
318
  });
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
4
- import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
4
+ import { type AutocompleteProvider, getKeybindings, matchesKey, type SlashCommand } from "@gajae-code/tui";
5
5
  import { $pickenv, logger, sanitizeText } from "@gajae-code/utils";
6
6
  import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
7
7
  import { isSettingsInitialized, settings } from "../../config/settings";
@@ -420,6 +420,34 @@ export class InputController {
420
420
  if (this.ctx.cancelPendingSubmission()) {
421
421
  return true;
422
422
  }
423
+ if (options.processes && this.ctx.session.isBashRunning) {
424
+ this.ctx.session.abortBash();
425
+ return true;
426
+ }
427
+ if (options.processes && this.ctx.session.isEvalRunning) {
428
+ this.ctx.session.abortEval();
429
+ return true;
430
+ }
431
+ // A queued steer on a live stream is owned by the steering branch below,
432
+ // which silently consumes it and auto-continues. This branch counts the same
433
+ // queue as cancellable work, and a streaming turn always has the indicator
434
+ // mounted, so without this hand-off it short-circuited first and turned the
435
+ // documented steer-on-interrupt into a loud abort that dumped the steer back
436
+ // into the composer. `cancelPendingSubmission()` above still wins: a
437
+ // submission that has not started yet is cancelled before the stream owns
438
+ // the key, and one that has started returns false there by design.
439
+ if (
440
+ options.streaming === true &&
441
+ this.ctx.session.isStreaming &&
442
+ this.ctx.session.hasQueuedSteering &&
443
+ !this.ctx.session.isBashRunning &&
444
+ !this.ctx.session.isEvalRunning &&
445
+ !this.#steerConsumePending
446
+ ) {
447
+ this.#steerConsumePending = true;
448
+ void this.#abortInteractive({ silent: true });
449
+ return true;
450
+ }
423
451
  // Only count queues this handler can actually drain. The aggregate
424
452
  // `queuedMessageCount` also counts hidden next-turn context, which
425
453
  // `restoreQueuedMessagesToEditor()` never returns and which survives turn
@@ -1855,19 +1883,36 @@ export class InputController {
1855
1883
  const now = Date.now();
1856
1884
  if (now - this.#lastBackgroundFoldKeyTime > BACKGROUND_FOLD_DOUBLE_PRESS_MS) {
1857
1885
  this.#lastBackgroundFoldKeyTime = now;
1858
- this.ctx.showStatus("Press Ctrl+B again to fold supported foreground bash into a background job");
1886
+ const foldKeys = getKeybindings().getKeys("app.tool.backgroundFold");
1887
+ this.ctx.showStatus(
1888
+ `Press ${foldKeys.join(" / ")} again to fold supported foreground bash into a background job`,
1889
+ );
1859
1890
  return true;
1860
1891
  }
1861
1892
  this.#lastBackgroundFoldKeyTime = 0;
1862
1893
 
1863
- if (!this.ctx.session.requestForegroundBashBackground?.()) {
1894
+ try {
1895
+ const request = this.ctx.session.requestForegroundBashBackground?.() ?? Promise.resolve(false);
1896
+ void request
1897
+ .then(backgrounded => {
1898
+ if (backgrounded) {
1899
+ this.ctx.showStatus("Folding foreground bash into a quiet background job…");
1900
+ return;
1901
+ }
1902
+ this.ctx.showWarning(
1903
+ "No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
1904
+ );
1905
+ })
1906
+ .catch(() => {
1907
+ this.ctx.showWarning(
1908
+ "No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
1909
+ );
1910
+ });
1911
+ } catch {
1864
1912
  this.ctx.showWarning(
1865
1913
  "No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
1866
1914
  );
1867
- return true;
1868
1915
  }
1869
-
1870
- this.ctx.showStatus("Folding foreground bash into a quiet background job…");
1871
1916
  return true;
1872
1917
  }
1873
1918