@gajae-code/coding-agent 0.2.2 → 0.2.4

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 (96) hide show
  1. package/CHANGELOG.md +45 -8600
  2. package/dist/types/cli/setup-cli.d.ts +1 -0
  3. package/dist/types/cli/update-cli.d.ts +3 -0
  4. package/dist/types/commands/deep-interview.d.ts +41 -0
  5. package/dist/types/commands/setup.d.ts +3 -0
  6. package/dist/types/config/settings-schema.d.ts +56 -0
  7. package/dist/types/defaults/gjc-defaults.d.ts +19 -6
  8. package/dist/types/discovery/helpers.d.ts +2 -0
  9. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  10. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +18 -0
  11. package/dist/types/hooks/skill-state.d.ts +5 -0
  12. package/dist/types/memories/index.d.ts +1 -1
  13. package/dist/types/memory-backend/local-backend.d.ts +3 -3
  14. package/dist/types/modes/components/hook-selector.d.ts +7 -0
  15. package/dist/types/modes/components/settings-selector.d.ts +3 -1
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +1 -0
  18. package/dist/types/modes/theme/defaults/index.d.ts +126 -0
  19. package/dist/types/modes/theme/theme.d.ts +5 -0
  20. package/dist/types/modes/types.d.ts +1 -0
  21. package/dist/types/modes/utils/context-usage.d.ts +6 -2
  22. package/dist/types/sdk.d.ts +6 -2
  23. package/dist/types/session/agent-session.d.ts +45 -1
  24. package/dist/types/session/session-manager.d.ts +3 -0
  25. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
  26. package/dist/types/setup/provider-onboarding.d.ts +29 -5
  27. package/dist/types/skill-state/active-state.d.ts +26 -1
  28. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  29. package/dist/types/skill-state/initial-phase.d.ts +12 -0
  30. package/dist/types/task/executor.d.ts +2 -0
  31. package/dist/types/task/types.d.ts +11 -0
  32. package/dist/types/tools/index.d.ts +20 -1
  33. package/dist/types/tools/skill.d.ts +47 -0
  34. package/dist/types/utils/changelog.d.ts +18 -2
  35. package/package.json +7 -7
  36. package/src/cli/setup-cli.ts +26 -12
  37. package/src/cli/update-cli.ts +67 -16
  38. package/src/cli.ts +1 -0
  39. package/src/commands/deep-interview.ts +25 -2
  40. package/src/commands/setup.ts +2 -0
  41. package/src/commands/state.ts +1 -0
  42. package/src/config/settings-schema.ts +63 -0
  43. package/src/defaults/gjc/skills/deep-interview/SKILL.md +58 -5
  44. package/src/defaults/gjc/skills/deep-interview/auto-answer-uncertain.md +37 -0
  45. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +42 -0
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -0
  47. package/src/defaults/gjc/skills/team/SKILL.md +10 -0
  48. package/src/defaults/gjc/skills/ultragoal/SKILL.md +19 -6
  49. package/src/defaults/gjc-defaults.ts +68 -16
  50. package/src/discovery/helpers.ts +24 -1
  51. package/src/extensibility/extensions/types.ts +6 -0
  52. package/src/gjc-runtime/deep-interview-runtime.ts +312 -1
  53. package/src/gjc-runtime/state-runtime.ts +175 -5
  54. package/src/goals/tools/goal-tool.ts +5 -1
  55. package/src/hooks/skill-state.ts +8 -6
  56. package/src/internal-urls/docs-index.generated.ts +6 -4
  57. package/src/internal-urls/memory-protocol.ts +3 -2
  58. package/src/main.ts +2 -3
  59. package/src/memories/index.ts +6 -4
  60. package/src/memory-backend/local-backend.ts +14 -6
  61. package/src/modes/components/hook-selector.ts +156 -1
  62. package/src/modes/components/settings-selector.ts +16 -12
  63. package/src/modes/controllers/command-controller.ts +3 -4
  64. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  65. package/src/modes/controllers/selector-controller.ts +69 -9
  66. package/src/modes/interactive-mode.ts +14 -1
  67. package/src/modes/theme/defaults/blue-crab.json +126 -0
  68. package/src/modes/theme/defaults/index.ts +2 -0
  69. package/src/modes/theme/theme.ts +40 -1
  70. package/src/modes/types.ts +1 -0
  71. package/src/modes/utils/context-usage.ts +66 -17
  72. package/src/prompts/agents/architect.md +3 -0
  73. package/src/prompts/agents/executor.md +2 -0
  74. package/src/prompts/agents/frontmatter.md +1 -0
  75. package/src/prompts/memories/unavailable.md +9 -0
  76. package/src/prompts/system/subagent-system-prompt.md +6 -0
  77. package/src/prompts/tools/skill.md +28 -0
  78. package/src/prompts/tools/task.md +3 -0
  79. package/src/sdk.ts +54 -10
  80. package/src/session/agent-session.ts +204 -21
  81. package/src/session/session-manager.ts +9 -1
  82. package/src/setup/model-onboarding-guidance.ts +6 -3
  83. package/src/setup/provider-onboarding.ts +177 -16
  84. package/src/skill-state/active-state.ts +150 -25
  85. package/src/skill-state/deep-interview-mutation-guard.ts +11 -24
  86. package/src/skill-state/initial-phase.ts +17 -0
  87. package/src/slash-commands/builtin-registry.ts +62 -14
  88. package/src/slash-commands/helpers/context-report.ts +123 -13
  89. package/src/task/agents.ts +1 -0
  90. package/src/task/executor.ts +9 -1
  91. package/src/task/index.ts +91 -4
  92. package/src/task/types.ts +6 -0
  93. package/src/tools/ask.ts +2 -0
  94. package/src/tools/index.ts +23 -1
  95. package/src/tools/skill.ts +153 -0
  96. package/src/utils/changelog.ts +67 -44
@@ -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) {
@@ -12,6 +12,7 @@ import consolidationTemplate from "../prompts/memories/consolidation.md" with {
12
12
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
13
13
  import stageOneInputTemplate from "../prompts/memories/stage_one_input.md" with { type: "text" };
14
14
  import stageOneSystemTemplate from "../prompts/memories/stage_one_system.md" with { type: "text" };
15
+ import unavailableTemplate from "../prompts/memories/unavailable.md" with { type: "text" };
15
16
  import type { AgentSession } from "../session/agent-session";
16
17
  import {
17
18
  claimStage1Jobs,
@@ -149,23 +150,24 @@ export function startMemoryStartupTask(options: {
149
150
  export async function buildMemoryToolDeveloperInstructions(
150
151
  agentDir: string,
151
152
  settings: Settings,
153
+ session?: AgentSession,
152
154
  ): Promise<string | undefined> {
153
155
  const cfg = loadMemoryConfig(settings);
154
156
  if (!cfg.enabled) return undefined;
155
- const memoryRoot = getMemoryRoot(agentDir, settings.getCwd());
157
+ const memoryRoot = getMemoryRoot(agentDir, session?.sessionManager?.getCwd() ?? settings.getCwd());
156
158
  const summaryPath = path.join(memoryRoot, "memory_summary.md");
157
159
 
158
160
  let text: string;
159
161
  try {
160
162
  text = await Bun.file(summaryPath).text();
161
163
  } catch {
162
- return undefined;
164
+ return unavailableTemplate;
163
165
  }
164
166
 
165
167
  const summary = text.trim();
166
- if (!summary) return undefined;
168
+ if (!summary) return unavailableTemplate;
167
169
  const truncated = truncateByApproxTokens(summary, cfg.summaryInjectionTokenLimit);
168
- if (!truncated.trim()) return undefined;
170
+ if (!truncated.trim()) return unavailableTemplate;
169
171
 
170
172
  return prompt.render(readPathTemplate, {
171
173
  memory_summary: truncated,
@@ -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,
@@ -200,8 +200,10 @@ 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 */
203
+ /** Called for theme preview while browsing theme settings */
204
204
  onThemePreview?: (theme: string) => void | Promise<void>;
205
+ /** Called to restore the rendered theme when theme settings preview is cancelled */
206
+ onThemePreviewCancel?: (theme: string) => void | Promise<void>;
205
207
  /** Called for status line preview while configuring */
206
208
  onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
207
209
  /** Get current rendered status line for inline preview */
@@ -219,7 +221,6 @@ export interface SettingsCallbacks {
219
221
  export class SettingsSelectorComponent extends Container {
220
222
  #tabBar: TabBar;
221
223
  #currentList: SettingsList | null = null;
222
- #currentSubmenu: Container | null = null;
223
224
  #pluginComponent: PluginSettingsComponent | null = null;
224
225
  #statusPreviewContainer: Container | null = null;
225
226
  #statusPreviewText: Text | null = null;
@@ -376,13 +377,13 @@ export class SettingsSelectorComponent extends Container {
376
377
  let onPreview: ((value: string) => void | Promise<void>) | undefined;
377
378
  let onPreviewCancel: (() => void) | undefined;
378
379
 
379
- const activeThemeBeforePreview = getCurrentThemeName() ?? currentValue;
380
380
  if (def.path === "theme.dark" || def.path === "theme.light") {
381
+ const activeThemeBeforePreview = getCurrentThemeName() ?? currentValue;
381
382
  onPreview = value => {
382
383
  return this.callbacks.onThemePreview?.(value);
383
384
  };
384
385
  onPreviewCancel = () => {
385
- this.callbacks.onThemePreview?.(activeThemeBeforePreview);
386
+ return this.callbacks.onThemePreviewCancel?.(activeThemeBeforePreview);
386
387
  };
387
388
  } else if (def.path === "statusLine.preset") {
388
389
  onPreview = value => {
@@ -619,17 +620,20 @@ export class SettingsSelectorComponent extends Container {
619
620
  return;
620
621
  }
621
622
 
622
- // Escape at top level cancels
623
- if (matchesAppInterrupt(data) && !this.#currentSubmenu) {
624
- this.callbacks.onCancel();
625
- return;
626
- }
627
-
628
- // Pass to current content
623
+ // Pass to current content. SettingsList owns Escape routing so open
624
+ // submenus can run their cancel/restore callbacks before closing.
629
625
  if (this.#currentList) {
630
626
  this.#currentList.handleInput(data);
631
- } else if (this.#pluginComponent) {
627
+ return;
628
+ }
629
+ if (this.#pluginComponent) {
632
630
  this.#pluginComponent.handleInput(data);
631
+ return;
632
+ }
633
+
634
+ // Fallback for future top-level content that does not own cancellation.
635
+ if (matchesAppInterrupt(data)) {
636
+ this.callbacks.onCancel();
633
637
  }
634
638
  }
635
639
  }
@@ -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);
@@ -586,7 +585,7 @@ export class CommandController {
586
585
  if (action === "view") {
587
586
  const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings, this.ctx.session);
588
587
  if (!payload) {
589
- this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
588
+ this.ctx.showWarning("Memory payload is empty; durable memory is unavailable or unconfirmed.");
590
589
  return;
591
590
  }
592
591
  this.ctx.chatContainer.addChild(new Spacer(1));
@@ -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
  );
@@ -17,8 +17,11 @@ import {
17
17
  } from "../../extensibility/plugins/marketplace";
18
18
  import {
19
19
  getAvailableThemes,
20
+ getCurrentThemeName,
21
+ getDetectedThemeSettingsPath,
20
22
  getSymbolTheme,
21
23
  previewTheme,
24
+ restoreThemePreview,
22
25
  setColorBlindMode,
23
26
  setSymbolPreset,
24
27
  setTheme,
@@ -29,6 +32,7 @@ import { type SessionInfo, SessionManager } from "../../session/session-manager"
29
32
  import { FileSessionStorage } from "../../session/session-storage";
30
33
  import {
31
34
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
35
+ MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
32
36
  MODEL_ONBOARDING_SETUP_COMMAND,
33
37
  } from "../../setup/model-onboarding-guidance";
34
38
  import { isSearchProviderPreference, setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
@@ -47,6 +51,7 @@ import {
47
51
  import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
48
52
  import { SessionSelectorComponent } from "../components/session-selector";
49
53
  import { SettingsSelectorComponent } from "../components/settings-selector";
54
+ import { ThemeSelectorComponent } from "../components/theme-selector";
50
55
  import { ToolExecutionComponent } from "../components/tool-execution";
51
56
  import { TreeSelectorComponent } from "../components/tree-selector";
52
57
  import { UserMessageSelectorComponent } from "../components/user-message-selector";
@@ -62,9 +67,15 @@ const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
62
67
 
63
68
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
64
69
 
70
+ function isThemePreviewSuperseded(result: { success: boolean; error?: string }): boolean {
71
+ return !result.success && result.error?.includes("superseded by a newer request") === true;
72
+ }
73
+
65
74
  function formatProviderOnboardingCommandGuide(): string {
66
75
  return [
67
- "API-compatible provider setup:",
76
+ "Provider preset setup:",
77
+ MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
78
+ "Custom API-compatible provider setup:",
68
79
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
69
80
  MODEL_ONBOARDING_SETUP_COMMAND,
70
81
  ].join("\n");
@@ -132,14 +143,21 @@ export class SelectorController {
132
143
  },
133
144
  {
134
145
  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
- }
146
+ onThemePreview: themeName => {
147
+ return previewTheme(themeName).then(result => {
148
+ if (!result.success && result.error && !isThemePreviewSuperseded(result)) {
149
+ this.ctx.showError(`Failed to preview theme: ${result.error}`);
150
+ }
151
+ this.#refreshThemeUi();
152
+ });
153
+ },
154
+ onThemePreviewCancel: themeName => {
155
+ return restoreThemePreview(themeName).then(result => {
156
+ if (!result.success && result.error && !isThemePreviewSuperseded(result)) {
157
+ this.ctx.showError(`Failed to restore theme preview: ${result.error}`);
158
+ }
159
+ this.#refreshThemeUi();
160
+ });
143
161
  },
144
162
  onStatusLinePreview: previewSettings => {
145
163
  // Update status line with preview settings
@@ -184,6 +202,48 @@ export class SelectorController {
184
202
  });
185
203
  }
186
204
 
205
+ #refreshThemeUi(): void {
206
+ this.ctx.statusLine.invalidate();
207
+ this.ctx.updateEditorTopBorder();
208
+ this.ctx.ui.requestRender();
209
+ }
210
+
211
+ showThemeSelector(): void {
212
+ getAvailableThemes().then(availableThemes => {
213
+ const initialTheme = getCurrentThemeName() ?? "red-claw";
214
+ this.showSelector(done => {
215
+ const selector = new ThemeSelectorComponent(
216
+ initialTheme,
217
+ availableThemes,
218
+ themeName => {
219
+ const settingPath = getDetectedThemeSettingsPath();
220
+ settings.set(settingPath, themeName);
221
+ this.#refreshThemeUi();
222
+ done();
223
+ },
224
+ () => {
225
+ void restoreThemePreview(initialTheme).then(result => {
226
+ if (!result.success && result.error) {
227
+ this.ctx.showError(`Failed to restore theme preview: ${result.error}`);
228
+ }
229
+ this.#refreshThemeUi();
230
+ });
231
+ done();
232
+ },
233
+ themeName => {
234
+ void previewTheme(themeName).then(result => {
235
+ if (!result.success && result.error) {
236
+ this.ctx.showError(`Failed to preview theme: ${result.error}`);
237
+ }
238
+ this.#refreshThemeUi();
239
+ });
240
+ },
241
+ );
242
+ return { component: selector, focus: selector };
243
+ });
244
+ });
245
+ }
246
+
187
247
  showHistorySearch(): void {
188
248
  const historyStorage = this.ctx.historyStorage;
189
249
  if (!historyStorage) return;
@@ -1281,7 +1281,16 @@ export class InteractiveMode implements InteractiveModeContext {
1281
1281
  reason?: "completed" | "paused" | "dropped";
1282
1282
  }): Promise<void> {
1283
1283
  const previousTools = this.#goalModePreviousTools;
1284
- if (this.goalModeEnabled && previousTools) {
1284
+ // Drop keeps the `goal` tool callable so the agent can immediately create a new
1285
+ // goal in the same session without a leader-side cleanup. Complete (and pause)
1286
+ // exit goal mode and restore the pre-goal tool set even when the goal_updated
1287
+ // event has already cleared goalModeEnabled (see #completeGoalFromTool emitting
1288
+ // state.enabled = false before #exitGoalMode runs). Spec: deep-interview-ultragoal-goal-tool-wiring AC1+AC2.
1289
+ const shouldRestoreTools =
1290
+ previousTools &&
1291
+ options?.reason !== "dropped" &&
1292
+ (this.goalModeEnabled || options?.reason === "completed" || options?.paused === true);
1293
+ if (shouldRestoreTools) {
1285
1294
  await this.session.setActiveToolsByName(previousTools);
1286
1295
  }
1287
1296
  const currentState = this.session.getGoalModeState();
@@ -2339,6 +2348,10 @@ export class InteractiveMode implements InteractiveModeContext {
2339
2348
  this.#selectorController.showSettingsSelector();
2340
2349
  }
2341
2350
 
2351
+ showThemeSelector(): void {
2352
+ this.#selectorController.showThemeSelector();
2353
+ }
2354
+
2342
2355
  showHistorySearch(): void {
2343
2356
  this.#selectorController.showHistorySearch();
2344
2357
  }