@gajae-code/coding-agent 0.8.0 → 0.8.2

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 (116) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/completion-cli.d.ts +41 -0
  3. package/dist/types/cli/config-cli.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +7 -0
  5. package/dist/types/cli.d.ts +124 -1
  6. package/dist/types/commands/completion.d.ts +29 -0
  7. package/dist/types/commands/config.d.ts +3 -0
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +7 -6
  10. package/dist/types/config/keybindings.d.ts +2 -2
  11. package/dist/types/config/model-profile-activation.d.ts +6 -0
  12. package/dist/types/config/settings.d.ts +8 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  14. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  17. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  19. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  21. package/dist/types/main.d.ts +4 -1
  22. package/dist/types/modes/components/model-selector.d.ts +4 -0
  23. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  26. package/dist/types/modes/print-mode.d.ts +13 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  28. package/dist/types/notifications/config.d.ts +8 -1
  29. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  30. package/dist/types/notifications/index.d.ts +6 -3
  31. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  32. package/dist/types/notifications/telegram-daemon.d.ts +18 -3
  33. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  34. package/dist/types/runtime-api-key.d.ts +7 -0
  35. package/dist/types/session/agent-session.d.ts +10 -0
  36. package/dist/types/session/agent-storage.d.ts +1 -0
  37. package/dist/types/task/executor.d.ts +2 -0
  38. package/dist/types/tools/computer.d.ts +2 -2
  39. package/dist/types/tools/skill.d.ts +1 -1
  40. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  41. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  42. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  43. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  44. package/package.json +7 -7
  45. package/src/cli/completion-cli.ts +234 -0
  46. package/src/cli/config-cli.ts +37 -9
  47. package/src/cli/fast-help.ts +1 -0
  48. package/src/cli/notify-cli.ts +91 -7
  49. package/src/cli.ts +3 -1
  50. package/src/commands/completion.ts +84 -0
  51. package/src/commands/config.ts +2 -0
  52. package/src/commands/deep-interview.ts +2 -1
  53. package/src/config/file-lock.ts +73 -34
  54. package/src/config/keybindings.ts +2 -2
  55. package/src/config/model-profile-activation.ts +65 -0
  56. package/src/config/model-registry.ts +14 -2
  57. package/src/config/settings.ts +22 -0
  58. package/src/coordinator-mcp/server.ts +67 -12
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
  61. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  62. package/src/extensibility/extensions/runner.ts +2 -0
  63. package/src/extensibility/extensions/types.ts +9 -0
  64. package/src/extensibility/skills.ts +30 -2
  65. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  66. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  67. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  68. package/src/gjc-runtime/deep-interview-state.ts +13 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +59 -11
  70. package/src/gjc-runtime/state-runtime.ts +12 -2
  71. package/src/gjc-runtime/team-runtime.ts +23 -0
  72. package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
  73. package/src/hooks/native-skill-hook.ts +29 -0
  74. package/src/internal-urls/docs-index.generated.ts +2 -2
  75. package/src/main.ts +52 -41
  76. package/src/modes/acp/acp-agent.ts +7 -6
  77. package/src/modes/components/model-selector.ts +54 -11
  78. package/src/modes/components/queued-message-selector.ts +92 -0
  79. package/src/modes/components/settings-selector.ts +4 -0
  80. package/src/modes/components/welcome.ts +160 -44
  81. package/src/modes/controllers/command-controller.ts +4 -4
  82. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  83. package/src/modes/controllers/input-controller.ts +186 -25
  84. package/src/modes/controllers/selector-controller.ts +122 -27
  85. package/src/modes/interactive-mode.ts +43 -16
  86. package/src/modes/print-mode.ts +43 -4
  87. package/src/modes/prompt-action-autocomplete.ts +24 -13
  88. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  89. package/src/modes/utils/ui-helpers.ts +2 -2
  90. package/src/notifications/config.ts +9 -1
  91. package/src/notifications/daemon-paths.ts +2 -0
  92. package/src/notifications/index.ts +39 -9
  93. package/src/notifications/lifecycle-commands.ts +37 -7
  94. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  95. package/src/notifications/telegram-daemon-control.ts +3 -1
  96. package/src/notifications/telegram-daemon.ts +231 -43
  97. package/src/notifications/telegram-reference.ts +0 -16
  98. package/src/prompts/system/system-prompt.md +3 -0
  99. package/src/prompts/tools/computer.md +2 -2
  100. package/src/runtime-api-key.ts +16 -0
  101. package/src/sdk.ts +20 -6
  102. package/src/session/agent-session.ts +225 -31
  103. package/src/session/agent-storage.ts +4 -0
  104. package/src/session/session-manager.ts +44 -2
  105. package/src/slash-commands/builtin-registry.ts +153 -71
  106. package/src/task/executor.ts +7 -4
  107. package/src/task/index.ts +9 -3
  108. package/src/task/output-manager.ts +16 -6
  109. package/src/task/receipt.ts +26 -7
  110. package/src/tools/computer.ts +246 -43
  111. package/src/tools/skill.ts +1 -1
  112. package/src/tools/telegram-send.ts +6 -0
  113. package/src/workflow/workflow-intent-diff.ts +29 -6
  114. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  115. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  116. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -21,6 +21,25 @@ export interface WelcomeComponentOptions {
21
21
  collapseChangelog?: boolean;
22
22
  }
23
23
 
24
+ const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 9;
25
+ const DEFAULT_WHATS_NEW_ROWS = 3;
26
+ const MAX_WHATS_NEW_ROWS = 12;
27
+ const NEWLINE_FLOW_KEY =
28
+ process.platform === "win32" ? { key: "alt+enter", label: "newline" } : { key: "ctrl+j", label: "newline" };
29
+
30
+ const FLOW_KEY_ITEMS: ReadonlyArray<{ key: string; label: string }> = [
31
+ { key: "/", label: "commands" },
32
+ { key: "#", label: "actions" },
33
+ { key: "!", label: "shell" },
34
+ { key: "$", label: "python" },
35
+ { key: "?", label: "keymap" },
36
+ { key: "ctrl+l", label: "model" },
37
+ { key: "shift+tab", label: "reasoning" },
38
+ { key: "tab", label: "complete" },
39
+ NEWLINE_FLOW_KEY,
40
+ { key: "ctrl+c", label: "clear" },
41
+ ];
42
+
24
43
  /**
25
44
  * GJC-native launch surface with compact command affordances, project
26
45
  * signals, and a claw/talon mark without copying another agent shell.
@@ -97,7 +116,6 @@ export class WelcomeComponent implements Component {
97
116
  }
98
117
  const targetContentRows = targetRows === undefined ? undefined : Math.max(0, targetRows - 2);
99
118
  const dualContentWidth = boxWidth - 3; // 3 = │ + │ + │
100
- const preferredLeftCol = 40;
101
119
  const minLeftCol = 20; // logo mark plus GJC identity labels
102
120
  const minRightCol = 24;
103
121
  const modelPill = this.#pill(theme.icon.model || "model", this.modelName, "statusLineModel");
@@ -112,10 +130,12 @@ export class WelcomeComponent implements Component {
112
130
  visibleWidth(modelPill),
113
131
  visibleWidth(providerPill),
114
132
  );
115
- const desiredLeftCol = Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.38)));
133
+ const evenLeftCol = Math.floor(dualContentWidth / 2);
134
+ const maxLeftColWithRightMinimum = Math.max(1, dualContentWidth - minRightCol);
135
+ const desiredLeftCol = Math.max(leftMinContentWidth, evenLeftCol);
116
136
  const dualLeftCol =
117
137
  dualContentWidth >= minRightCol + 1
118
- ? Math.min(desiredLeftCol, dualContentWidth - minRightCol)
138
+ ? Math.min(desiredLeftCol, maxLeftColWithRightMinimum)
119
139
  : Math.max(1, dualContentWidth - 1);
120
140
  const dualRightCol = Math.max(1, dualContentWidth - dualLeftCol);
121
141
  const showRightColumn = dualLeftCol >= leftMinContentWidth && dualRightCol >= minRightCol;
@@ -142,26 +162,6 @@ export class WelcomeComponent implements Component {
142
162
 
143
163
  const rightColumnWidth = showRightColumn ? rightCol : leftCol;
144
164
  const separator = buildSeparator(rightColumnWidth);
145
- const changelogBudget = Math.max(1, Math.min(6, Math.floor((targetContentRows ?? 18) * 0.3)));
146
-
147
- const sessionLines: string[] = [];
148
- if (this.recentSessions.length === 0) {
149
- sessionLines.push(` ${theme.fg("dim", "No saved trails")}`);
150
- } else {
151
- const bulletPrefix = ` ${theme.md.bullet} `;
152
- const prefixWidth = visibleWidth(bulletPrefix);
153
- for (const session of this.recentSessions.slice(0, 3)) {
154
- const timeSuffixRaw = ` (${session.timeAgo})`;
155
- const timeWidth = visibleWidth(timeSuffixRaw);
156
- const nameBudget = Math.max(1, rightColumnWidth - prefixWidth - timeWidth);
157
- const nameVis = visibleWidth(session.name);
158
- const name = nameVis > nameBudget ? truncateToWidth(session.name, nameBudget) : session.name;
159
- sessionLines.push(
160
- `${theme.fg("dim", bulletPrefix)}${theme.fg("muted", name)}${theme.fg("dim", timeSuffixRaw)}`,
161
- );
162
- }
163
- }
164
-
165
165
  const lspLines: string[] = [];
166
166
  if (this.lspServers.length === 0) {
167
167
  lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
@@ -178,25 +178,31 @@ export class WelcomeComponent implements Component {
178
178
  }
179
179
  }
180
180
 
181
+ const flowPreferredRows = this.#flowKeyRows(rightColumnWidth).length;
182
+ const changelogRowLimit = this.#whatsNewRowLimit(targetContentRows, lspLines.length, flowPreferredRows);
183
+ const changelogLines = this.#whatsNewLines(rightColumnWidth, changelogRowLimit);
184
+ const flowRowLimit = this.#flowKeyRowLimit(
185
+ targetContentRows,
186
+ changelogLines.length,
187
+ lspLines.length,
188
+ rightColumnWidth,
189
+ );
190
+ const flowLines = this.#flowKeyLines(rightColumnWidth, flowRowLimit);
191
+ const sessionLimit = this.#sessionTrailLimit(
192
+ targetContentRows,
193
+ changelogLines.length,
194
+ lspLines.length,
195
+ flowLines.length,
196
+ );
197
+ const sessionLines = this.#sessionTrailLines(rightColumnWidth, sessionLimit);
198
+
181
199
  const rightLines = [
182
200
  "",
183
201
  ` ${theme.bold(theme.fg("accent", "What's New"))}`,
184
- ...this.#whatsNewLines(rightColumnWidth, changelogBudget),
202
+ ...changelogLines,
185
203
  separator,
186
204
  ` ${theme.bold(theme.fg("accent", "Flow keys"))}`,
187
- ` ${theme.fg("dim", "/")}${theme.fg("muted", " commands")} ${theme.fg("dim", "·")} ${theme.fg(
188
- "dim",
189
- "#",
190
- )}${theme.fg("muted", " actions")}`,
191
- ` ${theme.fg("dim", "!")}${theme.fg("muted", " shell")} ${theme.fg("dim", "·")} ${theme.fg("dim", "$")}${theme.fg(
192
- "muted",
193
- " python",
194
- )}`,
195
- ` ${theme.fg("dim", "?")}${theme.fg("muted", " keymap")} ${theme.fg("dim", "·")} ${theme.fg(
196
- "dim",
197
- "ctrl+l",
198
- )}${theme.fg("muted", " model")}`,
199
- ` ${theme.fg("dim", "shift+tab")}${theme.fg("muted", " reasoning")}`,
205
+ ...flowLines,
200
206
  separator,
201
207
  ` ${theme.bold(theme.fg("accent", "Project pulse"))}`,
202
208
  ...lspLines,
@@ -315,7 +321,118 @@ export class WelcomeComponent implements Component {
315
321
  return [...Array.from({ length: topPad }, () => ""), ...clipped, ...Array.from({ length: bottomPad }, () => "")];
316
322
  }
317
323
 
318
- #whatsNewLines(width: number, maxItems: number): string[] {
324
+ #flowKeyItemText(item: { key: string; label: string }): string {
325
+ return `${theme.fg("dim", item.key)}${theme.fg("muted", ` ${item.label}`)}`;
326
+ }
327
+
328
+ #flowKeyRows(width: number): string[] {
329
+ const contentWidth = Math.max(1, width - 1);
330
+ const separator = ` ${theme.fg("dim", "·")} `;
331
+ const rows: string[] = [];
332
+ let current = "";
333
+ for (const item of FLOW_KEY_ITEMS) {
334
+ const segment = this.#flowKeyItemText(item);
335
+ const next = current ? `${current}${separator}${segment}` : segment;
336
+ if (current && visibleWidth(next) > contentWidth) {
337
+ rows.push(` ${current}`);
338
+ current = segment;
339
+ } else {
340
+ current = next;
341
+ }
342
+ }
343
+ if (current) rows.push(` ${current}`);
344
+ return rows.length > 0 ? rows : [` ${theme.fg("dim", "No flow keys")}`];
345
+ }
346
+
347
+ #flowKeyLines(width: number, maxRows: number): string[] {
348
+ const rows = this.#flowKeyRows(width);
349
+ const rowLimit = Math.max(1, Math.floor(maxRows));
350
+ if (rows.length <= rowLimit) return rows;
351
+ if (rowLimit === 1) {
352
+ const firstItem = FLOW_KEY_ITEMS[0];
353
+ const firstSegment = firstItem ? this.#flowKeyItemText(firstItem) : theme.fg("dim", "keys");
354
+ return [this.#fitToWidth(` ${firstSegment} ${theme.fg("dim", "· … ")}${theme.bold("/help")}`, width)];
355
+ }
356
+ return [...rows.slice(0, rowLimit - 1), ` ${theme.fg("dim", `… ${theme.bold("/help")} for more`)}`];
357
+ }
358
+
359
+ #flowKeyRowLimit(
360
+ targetContentRows: number | undefined,
361
+ changelogLineCount: number,
362
+ lspLineCount: number,
363
+ rightColumnWidth: number,
364
+ ): number {
365
+ const preferredRows = this.#flowKeyRows(rightColumnWidth).length;
366
+ if (targetContentRows === undefined) return preferredRows;
367
+
368
+ const sessionBaselineRows = this.recentSessions.length === 0 ? 1 : Math.min(3, this.recentSessions.length);
369
+ const availableRows =
370
+ targetContentRows -
371
+ WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS -
372
+ changelogLineCount -
373
+ lspLineCount -
374
+ sessionBaselineRows;
375
+ return Math.max(1, Math.min(preferredRows, availableRows));
376
+ }
377
+
378
+ #whatsNewRowLimit(targetContentRows: number | undefined, lspLineCount: number, flowLineCount: number): number {
379
+ if (targetContentRows === undefined) return 5;
380
+
381
+ const sessionBaselineRows = this.recentSessions.length === 0 ? 1 : Math.min(3, this.recentSessions.length);
382
+ const dynamicRows = Math.max(
383
+ 1,
384
+ targetContentRows - WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS - flowLineCount - lspLineCount,
385
+ );
386
+ const rowsAfterBaselineSessions = Math.max(1, dynamicRows - sessionBaselineRows);
387
+ const spareRows = Math.max(0, rowsAfterBaselineSessions - DEFAULT_WHATS_NEW_ROWS);
388
+ return Math.max(
389
+ 1,
390
+ Math.min(MAX_WHATS_NEW_ROWS, rowsAfterBaselineSessions, DEFAULT_WHATS_NEW_ROWS + Math.floor(spareRows / 2)),
391
+ );
392
+ }
393
+
394
+ #sessionTrailLimit(
395
+ targetContentRows: number | undefined,
396
+ changelogLineCount: number,
397
+ lspLineCount: number,
398
+ flowLineCount: number,
399
+ ): number {
400
+ if (this.recentSessions.length === 0) return 0;
401
+
402
+ const defaultLimit = Math.min(3, this.recentSessions.length);
403
+ if (targetContentRows === undefined) return defaultLimit;
404
+
405
+ const rowsWithDefaultTrail =
406
+ WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS +
407
+ changelogLineCount +
408
+ flowLineCount +
409
+ lspLineCount +
410
+ defaultLimit;
411
+ const extraRows = Math.max(0, targetContentRows - rowsWithDefaultTrail);
412
+ return Math.min(this.recentSessions.length, defaultLimit + extraRows);
413
+ }
414
+
415
+ #sessionTrailLines(rightColumnWidth: number, limit: number): string[] {
416
+ if (this.recentSessions.length === 0) {
417
+ return [` ${theme.fg("dim", "No saved trails")}`];
418
+ }
419
+
420
+ const bulletPrefix = ` ${theme.md.bullet} `;
421
+ const prefixWidth = visibleWidth(bulletPrefix);
422
+ const lines: string[] = [];
423
+ for (const session of this.recentSessions.slice(0, limit)) {
424
+ const timeSuffixRaw = ` (${session.timeAgo})`;
425
+ const timeWidth = visibleWidth(timeSuffixRaw);
426
+ const nameBudget = Math.max(1, rightColumnWidth - prefixWidth - timeWidth);
427
+ const nameVis = visibleWidth(session.name);
428
+ const name = nameVis > nameBudget ? truncateToWidth(session.name, nameBudget) : session.name;
429
+ lines.push(`${theme.fg("dim", bulletPrefix)}${theme.fg("muted", name)}${theme.fg("dim", timeSuffixRaw)}`);
430
+ }
431
+ return lines;
432
+ }
433
+
434
+ #whatsNewLines(width: number, maxRows: number): string[] {
435
+ const rowLimit = Math.max(1, Math.floor(maxRows));
319
436
  const changelog = this.options.changelogMarkdown?.trim();
320
437
  if (!changelog) {
321
438
  return [` ${theme.fg("dim", "Ready for your next prompt")}`];
@@ -326,25 +443,25 @@ export class WelcomeComponent implements Component {
326
443
  return [
327
444
  ` ${theme.fg("muted", `Updated to v${version}`)}`,
328
445
  ` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
329
- ];
446
+ ].slice(0, rowLimit);
330
447
  }
331
448
 
332
- const itemLimit = Math.max(1, Math.floor(maxItems));
333
449
  const items = this.#changelogItems(changelog);
334
450
  if (items.length === 0) {
335
451
  return [
336
452
  ` ${theme.fg("muted", `Updated to v${version}`)}`,
337
453
  ` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
338
- ];
454
+ ].slice(0, rowLimit);
339
455
  }
340
456
 
341
457
  const prefix = ` ${theme.md.bullet} `;
342
458
  const textWidth = Math.max(1, width - visibleWidth(prefix));
343
- const visibleItems = items.slice(0, itemLimit).map(item => {
459
+ const visibleItemCount = items.length > rowLimit ? Math.max(1, rowLimit - 1) : rowLimit;
460
+ const visibleItems = items.slice(0, visibleItemCount).map(item => {
344
461
  const text = visibleWidth(item) > textWidth ? truncateToWidth(item, textWidth) : item;
345
462
  return `${theme.fg("dim", prefix)}${theme.fg("muted", text)}`;
346
463
  });
347
- if (items.length > visibleItems.length) {
464
+ if (items.length > visibleItems.length && visibleItems.length < rowLimit) {
348
465
  visibleItems.push(` ${theme.fg("dim", `… ${theme.bold("/changelog")} for full notes`)}`);
349
466
  }
350
467
  return visibleItems;
@@ -472,7 +589,6 @@ interface ShineConfig {
472
589
  /** Center of the shine band along the diagonal, in [0, 1]. */
473
590
  pos: number;
474
591
  }
475
-
476
592
  /**
477
593
  * Apply a multi-stop diagonal gradient (bottom-left → top-right) plus an
478
594
  * optional sliding shine band across multi-line art. `phase` (0..1) shifts the
@@ -926,8 +926,7 @@ export class CommandController {
926
926
  this.ctx.streamingMessage = undefined;
927
927
  this.ctx.pendingTools.clear();
928
928
 
929
- this.ctx.chatContainer.addChild(new Spacer(1));
930
- this.ctx.chatContainer.addChild(new Text(`${theme.fg("accent", `${theme.status.success} ${label}`)}`, 1, 1));
929
+ this.ctx.chatContainer.addChild(new Text(`${theme.fg("accent", `${theme.status.success} ${label}`)}`, 1, 0));
931
930
  await this.ctx.reloadTodos();
932
931
  this.ctx.ui.requestRender();
933
932
  }
@@ -1276,17 +1275,18 @@ export class CommandController {
1276
1275
  async handleContributionPrepCommand(customInstructions?: string): Promise<void> {
1277
1276
  this.ctx.editor.setText("");
1278
1277
  try {
1279
- const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker: true });
1278
+ const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker: false });
1280
1279
  this.ctx.showStatus(
1281
1280
  [
1282
1281
  "Contribution prep artifacts written.",
1283
1282
  `Manifest: ${result.manifestPath}`,
1284
1283
  `Worker prompt: ${result.workerPromptPath}`,
1284
+ "Open the worker prompt from a separate terminal to keep this TUI session stable.",
1285
1285
  ].join("\n"),
1286
1286
  );
1287
1287
  this.ctx.chatContainer.addChild(
1288
1288
  new Text(
1289
- `${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\nManifest: ${result.manifestPath}`,
1289
+ `${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\nWorker prompt: ${result.workerPromptPath}`,
1290
1290
  1,
1291
1291
  1,
1292
1292
  ),
@@ -191,9 +191,8 @@ export class ExtensionUiController {
191
191
  this.ctx.streamingMessage = undefined;
192
192
  this.ctx.pendingTools.clear();
193
193
 
194
- this.ctx.chatContainer.addChild(new Spacer(1));
195
194
  this.ctx.chatContainer.addChild(
196
- new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
195
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
197
196
  );
198
197
  await this.ctx.reloadTodos();
199
198
  this.ctx.ui.requestRender();
@@ -299,15 +298,14 @@ export class ExtensionUiController {
299
298
  }
300
299
 
301
300
  #rebuildHookWidgets(): void {
302
- this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true, true);
303
- this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false, false);
301
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true);
302
+ this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false);
304
303
  this.ctx.ui.requestRender();
305
304
  }
306
305
 
307
306
  #renderHookWidgetContainer(
308
307
  container: Container,
309
308
  widgets: Map<string, ExtensionUiComponent>,
310
- spacerWhenEmpty: boolean,
311
309
  leadingSpacer: boolean,
312
310
  ): void {
313
311
  // Detach (not dispose): hook widgets are persistent instances owned by the
@@ -317,9 +315,6 @@ export class ExtensionUiController {
317
315
  container.detachAll();
318
316
 
319
317
  if (widgets.size === 0) {
320
- if (spacerWhenEmpty) {
321
- container.addChild(new Spacer(1));
322
- }
323
318
  return;
324
319
  }
325
320
 
@@ -434,9 +429,8 @@ export class ExtensionUiController {
434
429
  this.ctx.streamingMessage = undefined;
435
430
  this.ctx.pendingTools.clear();
436
431
 
437
- this.ctx.chatContainer.addChild(new Spacer(1));
438
432
  this.ctx.chatContainer.addChild(
439
- new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
433
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
440
434
  );
441
435
  await this.ctx.reloadTodos();
442
436
  this.ctx.ui.requestRender();
@@ -11,7 +11,7 @@ import { createPromptActionAutocompleteProvider } from "../../modes/prompt-actio
11
11
  import { theme } from "../../modes/theme/theme";
12
12
  import { scrollTmuxToPreviousUserInput as scrollTmuxPaneToPreviousUserInput } from "../../modes/tmux-scroll";
13
13
  import type { InteractiveModeContext } from "../../modes/types";
14
- import type { AgentSessionEvent } from "../../session/agent-session";
14
+ import type { AgentSessionEvent, QueuedMessageEditEntry } from "../../session/agent-session";
15
15
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
16
16
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
17
17
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
@@ -19,6 +19,7 @@ import { getEditorCommand, openInEditor } from "../../utils/external-editor";
19
19
  import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
20
20
  import { resizeImage } from "../../utils/image-resize";
21
21
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
22
+ import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
22
23
 
23
24
  interface Expandable {
24
25
  setExpanded(expanded: boolean): void;
@@ -313,14 +314,7 @@ export class InputController {
313
314
  this.ctx.editor.onDequeue = () => this.handleDequeue();
314
315
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
315
316
  this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
316
- this.ctx.editor.onTab = () => {
317
- if (!this.ctx.session.isStreaming && !this.ctx.session.isCompacting) return false;
318
- void this.handleQueueSubmit();
319
- return true;
320
- };
321
- this.ctx.editor.onTabDeclined = () => {
322
- if (this.ctx.session.isStreaming || this.ctx.session.isCompacting) void this.handleQueueSubmit();
323
- };
317
+
324
318
  this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
325
319
  this.ctx.editor.onViewportFollowLive = () => {
326
320
  this.ctx.ui.followLiveViewport();
@@ -614,12 +608,182 @@ export class InputController {
614
608
  }
615
609
 
616
610
  handleDequeue(): void {
617
- const restored = this.restoreLatestQueuedMessageToEditor();
618
- if (restored === 0) {
611
+ const entries = this.#getEditableQueuedMessages();
612
+ if (entries.length === 0) {
613
+ this.ctx.updatePendingMessagesDisplay();
619
614
  this.ctx.showStatus("No queued messages to restore");
620
- } else {
621
- this.ctx.showStatus(`Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`);
615
+ return;
616
+ }
617
+ if (entries.length === 1) {
618
+ const restored = this.#restoreQueuedMessageToEditor(entries[0]);
619
+ this.ctx.showStatus(
620
+ restored === 0 ? "Queued message is no longer available" : "Restored queued message to editor",
621
+ );
622
+ return;
623
+ }
624
+ this.#showQueuedMessageSelector(entries, this.#newestQueuedMessageIndex(entries));
625
+ }
626
+
627
+ #compactionQueuedMessageId(index: number): string {
628
+ return `compaction:${index}`;
629
+ }
630
+
631
+ #getEditableQueuedMessages(): QueuedMessageEditEntry[] {
632
+ const compactionEntries = this.ctx.compactionQueuedMessages.map((entry, index): QueuedMessageEditEntry => {
633
+ const label = entry.mode === "steer" ? "Steer" : "Queued";
634
+ return {
635
+ id: this.#compactionQueuedMessageId(index),
636
+ text: entry.text,
637
+ mode: entry.mode,
638
+ label,
639
+ };
640
+ });
641
+ return [...compactionEntries, ...this.ctx.session.getQueuedMessageEntries()];
642
+ }
643
+
644
+ #queuedMessageStableSequence(entry: QueuedMessageEditEntry): number | undefined {
645
+ const [mode, sequenceText] = entry.id.split(":");
646
+ if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return undefined;
647
+ const sequence = Number(sequenceText);
648
+ return Number.isInteger(sequence) ? sequence : undefined;
649
+ }
650
+
651
+ #newestQueuedMessageIndex(entries: QueuedMessageEditEntry[]): number {
652
+ let selectedIndex = entries.length - 1;
653
+ let newestSequence = Number.NEGATIVE_INFINITY;
654
+ for (let index = 0; index < entries.length; index += 1) {
655
+ const sequence = this.#queuedMessageStableSequence(entries[index]);
656
+ if (sequence !== undefined && sequence > newestSequence) {
657
+ newestSequence = sequence;
658
+ selectedIndex = index;
659
+ }
660
+ }
661
+ return Math.max(0, selectedIndex);
662
+ }
663
+
664
+ #restoreEditorFocus(): void {
665
+ this.ctx.editorContainer.clear();
666
+ this.ctx.editorContainer.addChild(this.ctx.editor);
667
+ this.ctx.ui.setFocus(this.ctx.editor);
668
+ }
669
+
670
+ #showQueuedMessageSelector(entries: QueuedMessageEditEntry[], selectedIndex = 0): void {
671
+ const selector = new QueuedMessageSelectorComponent(
672
+ entries,
673
+ entry => {
674
+ const restored = this.#restoreQueuedMessageToEditor(entry);
675
+ this.#restoreEditorFocus();
676
+ this.ctx.showStatus(
677
+ restored === 0 ? "Queued message is no longer available" : "Restored queued message to editor",
678
+ );
679
+ this.ctx.ui.requestRender();
680
+ },
681
+ (entry, index) => {
682
+ const deleted = this.#deleteQueuedMessage(entry);
683
+ const nextEntries = this.#getEditableQueuedMessages();
684
+ if (nextEntries.length === 0) {
685
+ this.#restoreEditorFocus();
686
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
687
+ this.ctx.ui.requestRender();
688
+ return;
689
+ }
690
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
691
+ this.#showQueuedMessageSelector(nextEntries, Math.min(index, nextEntries.length - 1));
692
+ },
693
+ (entry, index, direction) => {
694
+ const moved = this.#moveQueuedMessage(entry, direction);
695
+ const nextEntries = this.#getEditableQueuedMessages();
696
+ if (nextEntries.length === 0) {
697
+ this.#restoreEditorFocus();
698
+ this.ctx.showStatus("Queued message is no longer available");
699
+ this.ctx.ui.requestRender();
700
+ return;
701
+ }
702
+ const nextIndex = direction === "up" ? index - 1 : index + 1;
703
+ const selectedNextIndex = moved ? nextIndex : index;
704
+ this.ctx.showStatus(moved ? "Moved queued message" : "Queued message cannot move further");
705
+ this.#showQueuedMessageSelector(
706
+ nextEntries,
707
+ Math.max(0, Math.min(selectedNextIndex, nextEntries.length - 1)),
708
+ );
709
+ },
710
+ () => {
711
+ this.#restoreEditorFocus();
712
+ this.ctx.ui.requestRender();
713
+ },
714
+ { selectedIndex },
715
+ );
716
+ this.ctx.editorContainer.clear();
717
+ this.ctx.editorContainer.addChild(selector);
718
+ this.ctx.ui.setFocus(selector);
719
+ this.ctx.ui.requestRender();
720
+ }
721
+
722
+ #removeQueuedMessageForEditing(id: string): string | undefined {
723
+ const compactionPrefix = "compaction:";
724
+ if (id.startsWith(compactionPrefix)) {
725
+ const index = Number(id.slice(compactionPrefix.length));
726
+ if (!Number.isInteger(index)) return undefined;
727
+ const [entry] = this.ctx.compactionQueuedMessages.splice(index, 1);
728
+ return entry?.text;
729
+ }
730
+ return this.ctx.session.removeQueuedMessageForEditing(id);
731
+ }
732
+ #parseCompactionQueuedMessageId(id: string): number | undefined {
733
+ const compactionPrefix = "compaction:";
734
+ if (!id.startsWith(compactionPrefix)) return undefined;
735
+ const index = Number(id.slice(compactionPrefix.length));
736
+ return Number.isInteger(index) ? index : undefined;
737
+ }
738
+
739
+ #moveCompactionQueuedMessage(index: number, direction: QueuedMessageMoveDirection): boolean {
740
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
741
+ if (index < 0 || index >= this.ctx.compactionQueuedMessages.length) return false;
742
+ if (targetIndex < 0 || targetIndex >= this.ctx.compactionQueuedMessages.length) return false;
743
+ const current = this.ctx.compactionQueuedMessages[index];
744
+ const target = this.ctx.compactionQueuedMessages[targetIndex];
745
+ if (!current || !target) return false;
746
+ this.ctx.compactionQueuedMessages[index] = target;
747
+ this.ctx.compactionQueuedMessages[targetIndex] = current;
748
+ this.ctx.updatePendingMessagesDisplay();
749
+ return true;
750
+ }
751
+
752
+ #moveQueuedMessage(entry: QueuedMessageEditEntry, direction: QueuedMessageMoveDirection): boolean {
753
+ const compactionIndex = this.#parseCompactionQueuedMessageId(entry.id);
754
+ if (compactionIndex !== undefined) {
755
+ return this.#moveCompactionQueuedMessage(compactionIndex, direction);
756
+ }
757
+ const moved = this.ctx.session.moveQueuedMessageForEditing(entry.id, direction);
758
+ if (moved) {
759
+ this.ctx.updatePendingMessagesDisplay();
622
760
  }
761
+ return moved;
762
+ }
763
+
764
+ #deleteQueuedMessage(entry: QueuedMessageEditEntry): boolean {
765
+ const queuedText = this.#removeQueuedMessageForEditing(entry.id);
766
+ this.ctx.updatePendingMessagesDisplay();
767
+ if (!queuedText) return false;
768
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
769
+ return true;
770
+ }
771
+
772
+ #restoreQueuedMessageToEditor(entry: QueuedMessageEditEntry | undefined): number {
773
+ if (!entry) {
774
+ this.ctx.updatePendingMessagesDisplay();
775
+ return 0;
776
+ }
777
+ const queuedText = this.#removeQueuedMessageForEditing(entry.id);
778
+ if (!queuedText) {
779
+ this.ctx.updatePendingMessagesDisplay();
780
+ return 0;
781
+ }
782
+
783
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
784
+ this.ctx.editor.setText(queuedText);
785
+ this.ctx.updatePendingMessagesDisplay();
786
+ return 1;
623
787
  }
624
788
 
625
789
  /**
@@ -644,19 +808,18 @@ export class InputController {
644
808
 
645
809
  /**
646
810
  * Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
647
- * using the supplied `streamingBehavior`. Returns true if the text was a
648
- * recognised skill command chain and was dispatched. A failure to load a skill
649
- * file is surfaced via `showError` but still returns true — the editor was
650
- * already cleared on the success path, so falling through to plain-text
651
- * handling at that point would double-submit. Returns false when the text
652
- * isn't a `/skill:` prefix or the command name isn't a registered skill,
653
- * so the caller can fall through to plain-text handling (this branch
811
+ * using the supplied `streamingBehavior`. Returns true if the text contains a
812
+ * recognised canonical skill command or command chain and was dispatched. A
813
+ * failure to load a skill file is surfaced via `showError` but still returns
814
+ * true — the editor was already cleared on the success path, so falling
815
+ * through to plain-text handling at that point would double-submit. Returns
816
+ * false when the text has no registered canonical skill invocation, so the
817
+ * caller can fall through to plain-text handling (this branch
654
818
  * leaves the editor state untouched). `streamingBehavior` is only consulted
655
819
  * while the agent is streaming; the idle path of `promptCustomMessage`
656
820
  * ignores it.
657
821
  */
658
822
  async #invokeSkillCommand(text: string, streamingBehavior: "steer" | "followUp"): Promise<boolean> {
659
- if (!text.startsWith("/")) return false;
660
823
  const invocations = parseSkillInvocations(text, this.ctx.skillCommands ?? new Map());
661
824
  if (invocations.length === 0) return false;
662
825
  this.ctx.editor.addToHistory(text);
@@ -767,7 +930,7 @@ export class InputController {
767
930
  return this.handleFollowUp();
768
931
  }
769
932
 
770
- restoreLatestQueuedMessageToEditor(options?: { currentText?: string }): number {
933
+ restoreLatestQueuedMessageToEditor(): number {
771
934
  const compactionQueued = this.ctx.compactionQueuedMessages.pop();
772
935
  const queuedText = compactionQueued?.text ?? this.ctx.session.popLastQueuedMessage();
773
936
  if (!queuedText) {
@@ -776,9 +939,7 @@ export class InputController {
776
939
  }
777
940
 
778
941
  this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
779
- const currentText = options?.currentText ?? this.ctx.editor.getText();
780
- const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
781
- this.ctx.editor.setText(combinedText);
942
+ this.ctx.editor.setText(queuedText);
782
943
  this.ctx.updatePendingMessagesDisplay();
783
944
  return 1;
784
945
  }