@gajae-code/coding-agent 0.2.1 → 0.2.3

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 (153) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/dist/types/cli/setup-cli.d.ts +1 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/deep-interview.d.ts +41 -0
  5. package/dist/types/commands/session.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +2 -2
  8. package/dist/types/config/models-config-schema.d.ts +17 -9
  9. package/dist/types/config/settings-schema.d.ts +37 -24
  10. package/dist/types/discovery/helpers.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +33 -0
  13. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  16. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  18. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  19. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  20. package/dist/types/goals/runtime.d.ts +3 -9
  21. package/dist/types/goals/state.d.ts +3 -6
  22. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  23. package/dist/types/hooks/skill-state.d.ts +5 -0
  24. package/dist/types/memories/index.d.ts +1 -1
  25. package/dist/types/memory-backend/local-backend.d.ts +3 -3
  26. package/dist/types/modes/components/hook-selector.d.ts +7 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +0 -2
  28. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  29. package/dist/types/modes/components/status-line.d.ts +0 -3
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  31. package/dist/types/modes/interactive-mode.d.ts +1 -12
  32. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  33. package/dist/types/modes/theme/theme.d.ts +1 -2
  34. package/dist/types/modes/types.d.ts +1 -7
  35. package/dist/types/modes/utils/context-usage.d.ts +6 -2
  36. package/dist/types/sdk.d.ts +6 -2
  37. package/dist/types/session/agent-session.d.ts +47 -1
  38. package/dist/types/session/contribution-prep.d.ts +47 -0
  39. package/dist/types/session/session-manager.d.ts +3 -0
  40. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
  41. package/dist/types/setup/provider-onboarding.d.ts +29 -5
  42. package/dist/types/skill-state/active-state.d.ts +30 -1
  43. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  44. package/dist/types/skill-state/initial-phase.d.ts +12 -0
  45. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  46. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  47. package/dist/types/task/executor.d.ts +2 -0
  48. package/dist/types/task/types.d.ts +11 -0
  49. package/dist/types/tools/index.d.ts +20 -1
  50. package/dist/types/tools/skill.d.ts +47 -0
  51. package/dist/types/utils/changelog.d.ts +18 -2
  52. package/package.json +7 -7
  53. package/src/cli/args.ts +3 -2
  54. package/src/cli/setup-cli.ts +26 -12
  55. package/src/cli.ts +7 -1
  56. package/src/commands/contribution-prep.ts +41 -0
  57. package/src/commands/deep-interview.ts +30 -23
  58. package/src/commands/launch.ts +10 -1
  59. package/src/commands/ralplan.ts +10 -22
  60. package/src/commands/session.ts +150 -0
  61. package/src/commands/setup.ts +2 -0
  62. package/src/commands/state.ts +15 -4
  63. package/src/commands/team.ts +23 -3
  64. package/src/config/model-registry.ts +10 -2
  65. package/src/config/models-config-schema.ts +120 -102
  66. package/src/config/settings-schema.ts +42 -25
  67. package/src/config.ts +1 -1
  68. package/src/defaults/gjc/skills/deep-interview/SKILL.md +32 -13
  69. package/src/defaults/gjc/skills/ralplan/SKILL.md +22 -2
  70. package/src/defaults/gjc/skills/team/SKILL.md +39 -7
  71. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -25
  72. package/src/discovery/helpers.ts +24 -1
  73. package/src/eval/py/prelude.py +1 -1
  74. package/src/extensibility/extensions/types.ts +6 -0
  75. package/src/gjc-runtime/deep-interview-runtime.ts +546 -0
  76. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  77. package/src/gjc-runtime/launch-tmux.ts +83 -43
  78. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  79. package/src/gjc-runtime/state-runtime.ts +731 -0
  80. package/src/gjc-runtime/team-runtime.ts +708 -52
  81. package/src/gjc-runtime/tmux-common.ts +119 -0
  82. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  83. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  84. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  85. package/src/goals/runtime.ts +38 -144
  86. package/src/goals/state.ts +36 -7
  87. package/src/goals/tools/goal-tool.ts +15 -172
  88. package/src/hooks/skill-state.ts +39 -18
  89. package/src/internal-urls/docs-index.generated.ts +5 -4
  90. package/src/internal-urls/memory-protocol.ts +3 -2
  91. package/src/main.ts +2 -3
  92. package/src/memories/index.ts +2 -1
  93. package/src/memory-backend/local-backend.ts +14 -6
  94. package/src/modes/components/hook-selector.ts +156 -1
  95. package/src/modes/components/settings-selector.ts +5 -12
  96. package/src/modes/components/skill-hud/render.ts +4 -0
  97. package/src/modes/components/status-line/segments.ts +5 -16
  98. package/src/modes/components/status-line/types.ts +0 -3
  99. package/src/modes/components/status-line.ts +0 -6
  100. package/src/modes/controllers/command-controller.ts +27 -4
  101. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  102. package/src/modes/controllers/input-controller.ts +0 -15
  103. package/src/modes/controllers/selector-controller.ts +4 -11
  104. package/src/modes/interactive-mode.ts +18 -219
  105. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  106. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  107. package/src/modes/theme/theme.ts +0 -6
  108. package/src/modes/types.ts +1 -7
  109. package/src/modes/utils/context-usage.ts +66 -17
  110. package/src/prompts/agents/architect.md +3 -0
  111. package/src/prompts/agents/executor.md +2 -0
  112. package/src/prompts/agents/frontmatter.md +1 -0
  113. package/src/prompts/goals/goal-continuation.md +1 -4
  114. package/src/prompts/goals/goal-mode-active.md +3 -5
  115. package/src/prompts/system/subagent-system-prompt.md +6 -0
  116. package/src/prompts/system/system-prompt.md +5 -7
  117. package/src/prompts/tools/goal.md +4 -4
  118. package/src/prompts/tools/skill.md +28 -0
  119. package/src/prompts/tools/task.md +3 -0
  120. package/src/sdk.ts +51 -11
  121. package/src/session/agent-session.ts +222 -21
  122. package/src/session/contribution-prep.ts +320 -0
  123. package/src/session/session-manager.ts +9 -1
  124. package/src/setup/model-onboarding-guidance.ts +6 -3
  125. package/src/setup/provider-onboarding.ts +177 -16
  126. package/src/skill-state/active-state.ts +188 -25
  127. package/src/skill-state/deep-interview-mutation-guard.ts +72 -21
  128. package/src/skill-state/initial-phase.ts +17 -0
  129. package/src/skill-state/workflow-hud.ts +23 -5
  130. package/src/skill-state/workflow-state-contract.ts +121 -0
  131. package/src/slash-commands/builtin-registry.ts +75 -25
  132. package/src/slash-commands/helpers/context-report.ts +123 -13
  133. package/src/task/agents.ts +1 -0
  134. package/src/task/commands.ts +1 -5
  135. package/src/task/executor.ts +9 -1
  136. package/src/task/index.ts +91 -4
  137. package/src/task/types.ts +6 -0
  138. package/src/tools/ask.ts +2 -0
  139. package/src/tools/gh.ts +212 -2
  140. package/src/tools/index.ts +25 -6
  141. package/src/tools/skill.ts +153 -0
  142. package/src/utils/changelog.ts +67 -44
  143. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  144. package/dist/types/commands/question.d.ts +0 -7
  145. package/dist/types/modes/loop-limit.d.ts +0 -22
  146. package/src/commands/gjc-runtime-bridge.ts +0 -227
  147. package/src/commands/question.ts +0 -12
  148. package/src/modes/loop-limit.ts +0 -140
  149. package/src/prompts/commands/orchestrate.md +0 -49
  150. package/src/prompts/goals/goal-budget-limit.md +0 -16
  151. package/src/prompts/tools/create-goal.md +0 -3
  152. package/src/prompts/tools/get-goal.md +0 -3
  153. package/src/prompts/tools/update-goal.md +0 -3
@@ -15,11 +15,12 @@ const MEMORY_NAMESPACE = "root";
15
15
  * may see different roots.
16
16
  */
17
17
  export function memoryRootsFromRegistry(): string[] {
18
- const agentDir = getAgentDir();
19
18
  const roots: string[] = [];
20
19
  for (const ref of AgentRegistry.global().list()) {
21
- const sm = ref.session?.sessionManager;
20
+ const session = ref.session;
21
+ const sm = session?.sessionManager;
22
22
  if (!sm) continue;
23
+ const agentDir = session.settings?.getAgentDir() ?? getAgentDir();
23
24
  const root = getMemoryRoot(agentDir, sm.getCwd());
24
25
  if (root && !roots.includes(root)) roots.push(root);
25
26
  }
package/src/main.ts CHANGED
@@ -49,7 +49,7 @@ import { formatModelOnboardingGuidance } from "./setup/model-onboarding-guidance
49
49
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
50
50
  import { resolvePromptInput } from "./system-prompt";
51
51
  import type { LspStartupServerInfo } from "./tools";
52
- import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
52
+ import { getDisplayChangelogEntries, getNewEntries } from "./utils/changelog";
53
53
  import type { EventBus } from "./utils/event-bus";
54
54
 
55
55
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
@@ -341,8 +341,7 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
341
341
  return undefined;
342
342
  }
343
343
 
344
- const changelogPath = getChangelogPath();
345
- const entries = await parseChangelog(changelogPath);
344
+ const entries = getDisplayChangelogEntries();
346
345
 
347
346
  if (!lastVersion) {
348
347
  if (entries.length > 0) {
@@ -149,10 +149,11 @@ export function startMemoryStartupTask(options: {
149
149
  export async function buildMemoryToolDeveloperInstructions(
150
150
  agentDir: string,
151
151
  settings: Settings,
152
+ session?: AgentSession,
152
153
  ): Promise<string | undefined> {
153
154
  const cfg = loadMemoryConfig(settings);
154
155
  if (!cfg.enabled) return undefined;
155
- const memoryRoot = getMemoryRoot(agentDir, settings.getCwd());
156
+ const memoryRoot = getMemoryRoot(agentDir, session?.sessionManager.getCwd() ?? settings.getCwd());
156
157
  const summaryPath = path.join(memoryRoot, "memory_summary.md");
157
158
 
158
159
  let text: string;
@@ -9,22 +9,30 @@ import type { MemoryBackend } from "./types";
9
9
  /**
10
10
  * Wraps the existing `memories/` module as a `MemoryBackend`.
11
11
  *
12
- * No behavioural change every call delegates to the legacy entry points so
13
- * the local memory pipeline (rollout summarisation SQLite memory_summary.md)
14
- * keeps working exactly as before.
12
+ * The local pipeline owns rollout summarisation, SQLite retention, and
13
+ * `memory_summary.md`. Prompt reads use the live session cwd when available so
14
+ * manual enqueue/rebuild and startup hydration address the same memory root.
15
15
  */
16
16
  export const localBackend: MemoryBackend = {
17
17
  id: "local",
18
18
  start(options) {
19
19
  startMemoryStartupTask(options);
20
20
  },
21
- async buildDeveloperInstructions(agentDir, settings) {
22
- return buildMemoryToolDeveloperInstructions(agentDir, settings);
21
+ async buildDeveloperInstructions(agentDir, settings, session) {
22
+ return buildMemoryToolDeveloperInstructions(agentDir, settings, session);
23
23
  },
24
24
  async clear(agentDir, cwd) {
25
25
  await clearMemoryData(agentDir, cwd);
26
26
  },
27
- async enqueue(agentDir, cwd) {
27
+ async enqueue(agentDir, cwd, session) {
28
28
  enqueueMemoryConsolidation(agentDir, cwd);
29
+ if (!session) return;
30
+ startMemoryStartupTask({
31
+ session,
32
+ settings: session.settings,
33
+ modelRegistry: session.modelRegistry,
34
+ agentDir,
35
+ taskDepth: session.taskDepth,
36
+ });
29
37
  },
30
38
  };
@@ -14,6 +14,7 @@ import {
14
14
  type TUI,
15
15
  truncateToWidth,
16
16
  visibleWidth,
17
+ wrapTextWithAnsi,
17
18
  } from "@gajae-code/tui";
18
19
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
19
20
  import { matchesAppExternalEditor, matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
@@ -31,6 +32,13 @@ export interface HookSelectorOptions {
31
32
  onRight?: () => void;
32
33
  onExternalEditor?: () => void;
33
34
  helpText?: string;
35
+ /**
36
+ * When true, the focused option's label wraps across multiple rows so the
37
+ * full text is visible. Non-focused options remain single-row with the
38
+ * existing `…` truncation hint. When unset/false, rendering is
39
+ * byte-identical to the previous implementation for all consumers.
40
+ */
41
+ wrapFocused?: boolean;
34
42
  }
35
43
 
36
44
  class OutlinedList extends Container {
@@ -55,12 +63,140 @@ class OutlinedList extends Container {
55
63
  }
56
64
  }
57
65
 
66
+ /**
67
+ * Width-aware list child that owns wrapped focused-option layout.
68
+ *
69
+ * Single layout owner for the `wrapFocused` branch: row budgeting, sibling
70
+ * selection, marker placement, and finalized row construction all happen
71
+ * inside `render(width)` using the actual incoming width. The outer host
72
+ * (`HookSelectorComponent`) feeds it `options`, `selectedIndex`, and
73
+ * `maxVisibleRows`; everything that depends on terminal width is recomputed
74
+ * on each render so resize Just Works.
75
+ *
76
+ * `maxVisibleRows` is a sibling budget before it is a hard cap: surrounding
77
+ * options shrink first so the focused option is never clipped. The single
78
+ * allowed overflow exception is when the focused option's wrapped block
79
+ * alone exceeds the budget — in that case the focused option is rendered
80
+ * fully with zero siblings.
81
+ */
82
+ class FocusAwareList extends Container {
83
+ #options: string[] = [];
84
+ #selectedIndex = 0;
85
+ #maxVisibleRows = 0;
86
+ #outline: boolean;
87
+
88
+ constructor(outline: boolean) {
89
+ super();
90
+ this.#outline = outline;
91
+ }
92
+
93
+ setState(options: string[], selectedIndex: number, maxVisibleRows: number): void {
94
+ this.#options = options;
95
+ this.#selectedIndex = Math.max(0, Math.min(selectedIndex, options.length - 1));
96
+ this.#maxVisibleRows = Math.max(1, maxVisibleRows);
97
+ this.invalidate();
98
+ }
99
+
100
+ render(width: number): string[] {
101
+ if (this.#options.length === 0) return this.#outline ? this.#wrapOutline([], width) : [];
102
+
103
+ const mdTheme = getMarkdownTheme();
104
+ const innerWidth = this.#outline ? Math.max(1, width - 2) : Math.max(1, width);
105
+
106
+ // Selected/non-selected prefixes mirror the legacy `#updateList` shape.
107
+ const styledSelectedPrefix = theme.fg("accent", `${theme.nav.cursor} `);
108
+ const nonSelectedPrefix = " ";
109
+ const prefixWidth = visibleWidth(styledSelectedPrefix);
110
+ const continuationPrefix = " ".repeat(prefixWidth);
111
+ const availableLabelWidth = Math.max(1, innerWidth - prefixWidth);
112
+
113
+ // Render the focused label up front so we can measure how many rows it
114
+ // will consume at the current width and budget siblings accordingly.
115
+ const focusedLabel = renderInlineMarkdown(this.#options[this.#selectedIndex] ?? "", mdTheme, t =>
116
+ theme.fg("accent", t),
117
+ );
118
+ const focusedWrappedSegments = wrapTextWithAnsi(focusedLabel, availableLabelWidth);
119
+ const focusedRows = Math.max(1, focusedWrappedSegments.length);
120
+
121
+ // Decide whether the position marker is going to be shown. We make a
122
+ // pessimistic first pass assuming the marker is needed; if the window
123
+ // ends up covering every option we drop it.
124
+ const totalOptions = this.#options.length;
125
+ const willHaveSiblings = totalOptions > 1;
126
+ const wouldNeedMarker = willHaveSiblings; // tentative; refined below
127
+ const markerSlot = wouldNeedMarker ? 1 : 0;
128
+
129
+ // Sibling budget. If the focused block alone is over budget, render it
130
+ // fully with zero siblings (only allowed overflow exception).
131
+ const siblingBudget = Math.max(0, this.#maxVisibleRows - focusedRows - markerSlot);
132
+
133
+ // Distribute sibling slots around focus, preferring closest options.
134
+ const availableAbove = this.#selectedIndex;
135
+ const availableBelow = totalOptions - this.#selectedIndex - 1;
136
+ let above = Math.min(availableAbove, Math.floor(siblingBudget / 2));
137
+ let below = Math.min(availableBelow, siblingBudget - above);
138
+ // Transfer unused quota across the focus when one side has fewer
139
+ // options than its share.
140
+ const unusedBelow = siblingBudget - above - below;
141
+ if (unusedBelow > 0) above = Math.min(availableAbove, above + unusedBelow);
142
+ const unusedAbove = siblingBudget - above - below;
143
+ if (unusedAbove > 0) below = Math.min(availableBelow, below + unusedAbove);
144
+
145
+ const startIndex = this.#selectedIndex - above;
146
+ const endIndex = this.#selectedIndex + below + 1;
147
+ const showMarker = startIndex > 0 || endIndex < totalOptions;
148
+
149
+ const rows: string[] = [];
150
+ for (let i = startIndex; i < endIndex; i++) {
151
+ if (i === this.#selectedIndex) {
152
+ // Emit focused wrapped rows. Cursor only on row 0; continuation
153
+ // rows are whitespace-aligned under the label start.
154
+ for (let r = 0; r < focusedWrappedSegments.length; r++) {
155
+ const segment = focusedWrappedSegments[r] ?? "";
156
+ rows.push(r === 0 ? styledSelectedPrefix + segment : continuationPrefix + segment);
157
+ }
158
+ } else {
159
+ const label = renderInlineMarkdown(this.#options[i] ?? "", mdTheme, t => theme.fg("text", t));
160
+ // Non-focused rows stay single-line. Truncate here so the
161
+ // outline (post-padded by `#wrapOutline`) and non-outline
162
+ // paths render the same `…` hint for over-wide labels.
163
+ const fittedLabel = truncateToWidth(label, availableLabelWidth);
164
+ rows.push(nonSelectedPrefix + fittedLabel);
165
+ }
166
+ }
167
+
168
+ if (showMarker) {
169
+ rows.push(theme.fg("dim", ` (${this.#selectedIndex + 1}/${totalOptions})`));
170
+ }
171
+
172
+ return this.#outline ? this.#wrapOutline(rows, width) : rows;
173
+ }
174
+
175
+ #wrapOutline(rows: string[], width: number): string[] {
176
+ // Mirror the outline border drawn by `OutlinedList.render(width)`. The
177
+ // rows passed in are already constrained to `innerWidth` by
178
+ // `wrapTextWithAnsi`, so we only normalize tabs and pad — no further
179
+ // truncation, which would clip wrapped focused labels.
180
+ const borderColor = (text: string) => theme.fg("border", text);
181
+ const horizontal = borderColor(theme.boxSharp.horizontal.repeat(Math.max(1, width)));
182
+ const innerWidth = Math.max(1, width - 2);
183
+ const content = rows.map(line => {
184
+ const normalized = replaceTabs(line);
185
+ const fitted = truncateToWidth(normalized, innerWidth);
186
+ const pad = Math.max(0, innerWidth - visibleWidth(fitted));
187
+ return `${borderColor(theme.boxSharp.vertical)}${fitted}${padding(pad)}${borderColor(theme.boxSharp.vertical)}`;
188
+ });
189
+ return [horizontal, ...content, horizontal];
190
+ }
191
+ }
192
+
58
193
  export class HookSelectorComponent extends Container {
59
194
  #options: string[];
60
195
  #selectedIndex: number;
61
196
  #maxVisible: number;
62
197
  #listContainer: Container | undefined;
63
198
  #outlinedList: OutlinedList | undefined;
199
+ #focusAwareList: FocusAwareList | undefined;
64
200
  #onSelectCallback: (option: string) => void;
65
201
  #onCancelCallback: () => void;
66
202
  #titleComponent: Markdown;
@@ -69,6 +205,8 @@ export class HookSelectorComponent extends Container {
69
205
  #onLeftCallback: (() => void) | undefined;
70
206
  #onRightCallback: (() => void) | undefined;
71
207
  #onExternalEditorCallback: (() => void) | undefined;
208
+ #wrapFocused: boolean;
209
+ #outline: boolean;
72
210
  constructor(
73
211
  title: string,
74
212
  options: string[],
@@ -87,6 +225,8 @@ export class HookSelectorComponent extends Container {
87
225
  this.#onLeftCallback = opts?.onLeft;
88
226
  this.#onRightCallback = opts?.onRight;
89
227
  this.#onExternalEditorCallback = opts?.onExternalEditor;
228
+ this.#wrapFocused = opts?.wrapFocused === true;
229
+ this.#outline = opts?.outline === true;
90
230
 
91
231
  this.addChild(new DynamicBorder());
92
232
  this.addChild(new Spacer(1));
@@ -113,7 +253,13 @@ export class HookSelectorComponent extends Container {
113
253
  );
114
254
  }
115
255
 
116
- if (opts?.outline) {
256
+ if (this.#wrapFocused) {
257
+ // Width-aware child owns wrapped layout. It handles both outline
258
+ // and non-outline rendering paths internally so the cursor signal
259
+ // + continuation indent are identical across branches.
260
+ this.#focusAwareList = new FocusAwareList(this.#outline);
261
+ this.addChild(this.#focusAwareList);
262
+ } else if (this.#outline) {
117
263
  this.#outlinedList = new OutlinedList();
118
264
  this.addChild(this.#outlinedList);
119
265
  } else {
@@ -130,6 +276,15 @@ export class HookSelectorComponent extends Container {
130
276
  }
131
277
 
132
278
  #updateList(): void {
279
+ if (this.#wrapFocused && this.#focusAwareList) {
280
+ this.#focusAwareList.setState(this.#options, this.#selectedIndex, this.#maxVisible);
281
+ return;
282
+ }
283
+
284
+ // Legacy branch — byte-identical to the previous implementation. Any
285
+ // change here is a regression against
286
+ // `BASELINE_OUTLINED_RENDER_80_STRIPPED` in
287
+ // `packages/coding-agent/test/hook-selector-overflow.test.ts`.
133
288
  const lines: string[] = [];
134
289
  const startIndex = Math.max(
135
290
  0,
@@ -21,7 +21,7 @@ import type {
21
21
  StatusLineSeparatorStyle,
22
22
  } from "../../config/settings-schema";
23
23
  import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
24
- import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
24
+ import { getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
25
25
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
26
26
  import { getTabBarTheme } from "../shared";
27
27
  import { DynamicBorder } from "./dynamic-border";
@@ -200,8 +200,6 @@ export interface StatusLinePreviewSettings {
200
200
  export interface SettingsCallbacks {
201
201
  /** Called when any setting value changes */
202
202
  onChange: (path: SettingPath, newValue: unknown) => void;
203
- /** Called for theme preview while browsing */
204
- onThemePreview?: (theme: string) => void | Promise<void>;
205
203
  /** Called for status line preview while configuring */
206
204
  onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
207
205
  /** Get current rendered status line for inline preview */
@@ -376,15 +374,10 @@ export class SettingsSelectorComponent extends Container {
376
374
  let onPreview: ((value: string) => void | Promise<void>) | undefined;
377
375
  let onPreviewCancel: (() => void) | undefined;
378
376
 
379
- const activeThemeBeforePreview = getCurrentThemeName() ?? currentValue;
380
- if (def.path === "theme.dark" || def.path === "theme.light") {
381
- onPreview = value => {
382
- return this.callbacks.onThemePreview?.(value);
383
- };
384
- onPreviewCancel = () => {
385
- this.callbacks.onThemePreview?.(activeThemeBeforePreview);
386
- };
387
- } else if (def.path === "statusLine.preset") {
377
+ // Theme selection is confirm-only: moving through the list must not mutate
378
+ // the rendered theme while the displayed/persisted setting still names
379
+ // the previous value. Confirmation persists through Settings hooks.
380
+ if (def.path === "statusLine.preset") {
388
381
  onPreview = value => {
389
382
  const presetDef = getPreset(
390
383
  value as "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "custom",
@@ -1,4 +1,5 @@
1
1
  import type { SkillActiveEntry, WorkflowHudChip } from "../../../skill-state/active-state";
2
+ import { workflowReceiptStatus } from "../../../skill-state/workflow-state-contract";
2
3
 
3
4
  const ANSI_RESET_FG = "\x1b[39m";
4
5
  const ANSI_RESET_BOLD = "\x1b[22m";
@@ -60,6 +61,9 @@ function formatEntry(entry: SkillActiveEntry): string {
60
61
  .map(formatChip)
61
62
  .filter((chip): chip is string => Boolean(chip));
62
63
  if (entry.stale === true) chips.unshift("warn:stale");
64
+ const receiptStatus = workflowReceiptStatus(entry.receipt);
65
+ if (receiptStatus === "stale") chips.unshift("warn:receipt=stale");
66
+ if (receiptStatus === "fresh") chips.push("receipt=fresh");
63
67
  const summary = sanitizeHudPart(entry.hud?.summary);
64
68
  return [base, summary, ...chips].filter(Boolean).join(" ");
65
69
  }
@@ -110,10 +110,9 @@ const modelSegment: StatusLineSegment = {
110
110
  },
111
111
  };
112
112
 
113
- function formatGoalBudget(current: number, budget?: number): string {
113
+ function formatGoalUsage(current: number): string {
114
114
  const used = formatNumber(current);
115
- if (budget === undefined) return used;
116
- return `${used}/${formatNumber(budget)}`;
115
+ return used;
117
116
  }
118
117
 
119
118
  function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: boolean }): RenderedSegment {
@@ -131,10 +130,6 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
131
130
  icon = theme.symbol("status.success");
132
131
  color = "success";
133
132
  break;
134
- case "budget-limited":
135
- icon = theme.symbol("status.warning");
136
- color = "warning";
137
- break;
138
133
  case "dropped":
139
134
  icon = theme.symbol("status.aborted");
140
135
  color = "dim";
@@ -144,9 +139,9 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
144
139
  }
145
140
 
146
141
  const parts: string[] = [withIcon(icon, "Goal")];
147
- const showBudget = ctx.session.settings.get("goal.statusInFooter") === true;
148
- if (showBudget && goal) {
149
- parts.push(formatGoalBudget(goal.tokensUsed, goal.tokenBudget));
142
+ const showUsage = ctx.session.settings.get("goal.statusInFooter") === true;
143
+ if (showUsage && goal) {
144
+ parts.push(formatGoalUsage(goal.tokensUsed));
150
145
  }
151
146
  return { content: theme.fg(color, parts.join(" ")), visible: true };
152
147
  }
@@ -169,12 +164,6 @@ const modeSegment: StatusLineSegment = {
169
164
  return renderGoalMode(ctx, goal);
170
165
  }
171
166
 
172
- const loop = ctx.loopMode;
173
- if (loop?.enabled) {
174
- const content = withIcon(theme.icon.loop, "Loop");
175
- return { content: theme.fg("customMessageLabel", content), visible: true };
176
- }
177
-
178
167
  return { content: "", visible: false };
179
168
  },
180
169
  };
@@ -24,9 +24,6 @@ export interface SegmentContext {
24
24
  enabled: boolean;
25
25
  paused: boolean;
26
26
  } | null;
27
- loopMode: {
28
- enabled: boolean;
29
- } | null;
30
27
  goalMode: {
31
28
  enabled: boolean;
32
29
  paused: boolean;
@@ -155,7 +155,6 @@ export class StatusLineComponent implements Component {
155
155
  #subagentCount: number = 0;
156
156
  #sessionStartTime: number = Date.now();
157
157
  #planModeStatus: { enabled: boolean; paused: boolean } | null = null;
158
- #loopModeStatus: { enabled: boolean } | null = null;
159
158
  #goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
160
159
  #skillHudEntries: SkillActiveEntry[] = [];
161
160
  #skillHudLastFetch = 0;
@@ -229,10 +228,6 @@ export class StatusLineComponent implements Component {
229
228
  this.#planModeStatus = status ?? null;
230
229
  }
231
230
 
232
- setLoopModeStatus(status: { enabled: boolean } | undefined): void {
233
- this.#loopModeStatus = status ?? null;
234
- }
235
-
236
231
  setGoalModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
237
232
  this.#goalModeStatus = status ?? null;
238
233
  }
@@ -611,7 +606,6 @@ export class StatusLineComponent implements Component {
611
606
  width,
612
607
  options: this.#resolveSettings().segmentOptions ?? {},
613
608
  planMode: this.#planModeStatus,
614
- loopMode: this.#loopModeStatus,
615
609
  goalMode: this.#goalModeStatus,
616
610
  usageStats,
617
611
  contextPercent,
@@ -42,7 +42,7 @@ import type { NewSessionOptions } from "../../session/session-manager";
42
42
  import { outputMeta } from "../../tools/output-meta";
43
43
  import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
44
44
  import { replaceTabs } from "../../tools/render-utils";
45
- import { getChangelogPath, parseChangelog } from "../../utils/changelog";
45
+ import { getDisplayChangelogEntries } from "../../utils/changelog";
46
46
  import { copyToClipboard } from "../../utils/clipboard";
47
47
  import { openPath } from "../../utils/open";
48
48
  import { setSessionTerminalTitle } from "../../utils/title-generator";
@@ -525,8 +525,7 @@ export class CommandController {
525
525
  }
526
526
 
527
527
  async handleChangelogCommand(showFull = false): Promise<void> {
528
- const changelogPath = getChangelogPath();
529
- const allEntries = await parseChangelog(changelogPath);
528
+ const allEntries = getDisplayChangelogEntries();
530
529
  // Default to showing only the latest 3 versions unless --full is specified
531
530
  // allEntries comes from parseChangelog with newest first, reverse to show oldest->newest
532
531
  const entriesToShow = showFull ? allEntries : allEntries.slice(0, 3);
@@ -1248,7 +1247,31 @@ export class CommandController {
1248
1247
  this.ctx.statusContainer.clear();
1249
1248
  this.ctx.editor.onEscape = originalOnEscape;
1250
1249
  }
1251
- this.ctx.ui.requestRender();
1250
+ }
1251
+
1252
+ async handleContributionPrepCommand(customInstructions?: string): Promise<void> {
1253
+ this.ctx.editor.setText("");
1254
+ try {
1255
+ const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker: true });
1256
+ this.ctx.showStatus(
1257
+ [
1258
+ "Contribution prep artifacts written.",
1259
+ `Manifest: ${result.manifestPath}`,
1260
+ `Worker prompt: ${result.workerPromptPath}`,
1261
+ ].join("\n"),
1262
+ );
1263
+ this.ctx.chatContainer.addChild(
1264
+ new Text(
1265
+ `${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\nManifest: ${result.manifestPath}`,
1266
+ 1,
1267
+ 1,
1268
+ ),
1269
+ );
1270
+ this.ctx.ui.requestRender();
1271
+ } catch (error) {
1272
+ const message = error instanceof Error ? error.message : String(error);
1273
+ this.ctx.showError(`Contribution prep failed: ${message}`);
1274
+ }
1252
1275
  }
1253
1276
  }
1254
1277
 
@@ -623,6 +623,7 @@ export class ExtensionUiController {
623
623
  onTimeout: dialogOptions?.onTimeout,
624
624
  tui: this.ctx.ui,
625
625
  outline: dialogOptions?.outline,
626
+ wrapFocused: dialogOptions?.wrapFocused,
626
627
  maxVisible,
627
628
  },
628
629
  );
@@ -51,15 +51,6 @@ export class InputController {
51
51
  this.ctx.retryEscapeHandler,
52
52
  );
53
53
  this.ctx.editor.onEscape = () => {
54
- if (this.ctx.loopModeEnabled) {
55
- this.ctx.pauseLoop();
56
- if (this.ctx.session.isStreaming) {
57
- void this.#abortInteractive();
58
- } else {
59
- this.ctx.cancelPendingSubmission();
60
- }
61
- return;
62
- }
63
54
  if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
64
55
  return;
65
56
  }
@@ -292,12 +283,6 @@ export class InputController {
292
283
  }
293
284
  }
294
285
 
295
- // While loop mode is on, every user-typed prompt becomes the new loop
296
- // prompt that auto-resubmits after each yield.
297
- if (this.ctx.loopModeEnabled) {
298
- this.ctx.loopPrompt = text;
299
- }
300
-
301
286
  // Queue input during compaction
302
287
  if (this.ctx.session.isCompacting) {
303
288
  if (this.ctx.pendingImages.length > 0) {
@@ -18,7 +18,6 @@ import {
18
18
  import {
19
19
  getAvailableThemes,
20
20
  getSymbolTheme,
21
- previewTheme,
22
21
  setColorBlindMode,
23
22
  setSymbolPreset,
24
23
  setTheme,
@@ -29,6 +28,7 @@ import { type SessionInfo, SessionManager } from "../../session/session-manager"
29
28
  import { FileSessionStorage } from "../../session/session-storage";
30
29
  import {
31
30
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
31
+ MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
32
32
  MODEL_ONBOARDING_SETUP_COMMAND,
33
33
  } from "../../setup/model-onboarding-guidance";
34
34
  import { isSearchProviderPreference, setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
@@ -64,7 +64,9 @@ const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect UR
64
64
 
65
65
  function formatProviderOnboardingCommandGuide(): string {
66
66
  return [
67
- "API-compatible provider setup:",
67
+ "Provider preset setup:",
68
+ MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
69
+ "Custom API-compatible provider setup:",
68
70
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
69
71
  MODEL_ONBOARDING_SETUP_COMMAND,
70
72
  ].join("\n");
@@ -132,15 +134,6 @@ export class SelectorController {
132
134
  },
133
135
  {
134
136
  onChange: (id, value) => this.handleSettingChange(id, value),
135
- onThemePreview: async themeName => {
136
- const result = await previewTheme(themeName);
137
- if (result.success) {
138
- this.ctx.statusLine.invalidate();
139
- this.ctx.updateEditorTopBorder();
140
- this.ctx.ui.invalidate();
141
- this.ctx.ui.requestRender();
142
- }
143
- },
144
137
  onStatusLinePreview: previewSettings => {
145
138
  // Update status line with preview settings
146
139
  this.ctx.statusLine.updateSettings({