@gajae-code/coding-agent 0.11.10 → 0.11.11

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 (111) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/settings-schema.d.ts +57 -2
  6. package/dist/types/config/settings.d.ts +1 -0
  7. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  8. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  9. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  10. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  11. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  12. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  13. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  14. package/dist/types/modes/components/welcome.d.ts +1 -0
  15. package/dist/types/modes/interactive-mode.d.ts +5 -1
  16. package/dist/types/runtime/memory-domain.d.ts +9 -0
  17. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  18. package/dist/types/runtime/memory-guard.d.ts +39 -0
  19. package/dist/types/runtime/memory-limit.d.ts +11 -0
  20. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  21. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  22. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  23. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  24. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  25. package/dist/types/sdk/bus/index.d.ts +3 -1
  26. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  29. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  30. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  31. package/dist/types/sdk/startup-capability.d.ts +6 -0
  32. package/dist/types/session/agent-session.d.ts +9 -11
  33. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  34. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  35. package/dist/types/session/session-manager.d.ts +34 -0
  36. package/dist/types/task/executor.d.ts +6 -0
  37. package/dist/types/task/types.d.ts +36 -0
  38. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  39. package/dist/types/tools/resource-gc.d.ts +19 -0
  40. package/package.json +8 -8
  41. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  42. package/scripts/run-test-manifest.ts +6 -4
  43. package/src/cli/args.ts +14 -8
  44. package/src/cli/fast-help.ts +1 -1
  45. package/src/cli.ts +48 -2
  46. package/src/commands/sdk.ts +91 -7
  47. package/src/config/model-profile-activation.ts +1 -0
  48. package/src/config/model-profile-contract.ts +13 -0
  49. package/src/config/model-profiles.ts +24 -0
  50. package/src/config/settings-schema.ts +61 -2
  51. package/src/config/settings.ts +8 -0
  52. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  53. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  54. package/src/extensibility/extensions/runner.ts +4 -0
  55. package/src/extensibility/extensions/types.ts +14 -0
  56. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  57. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  58. package/src/gjc-runtime/linux-proc.ts +70 -36
  59. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  60. package/src/gjc-runtime/state-runtime.ts +48 -11
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +23 -4
  63. package/src/modes/acp/acp-agent.ts +554 -60
  64. package/src/modes/acp/acp-event-mapper.ts +93 -6
  65. package/src/modes/components/model-selector.ts +15 -1
  66. package/src/modes/components/notifications-settings-editor.ts +1 -1
  67. package/src/modes/components/queue-pane.ts +55 -9
  68. package/src/modes/components/queued-message-selector.ts +53 -10
  69. package/src/modes/components/tool-status-header.ts +1 -4
  70. package/src/modes/components/welcome.ts +18 -3
  71. package/src/modes/controllers/command-controller.ts +4 -0
  72. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  73. package/src/modes/controllers/input-controller.ts +44 -3
  74. package/src/modes/interactive-mode.ts +65 -26
  75. package/src/modes/runtime-init.ts +1 -0
  76. package/src/prompts/agents/executor.md +1 -1
  77. package/src/runtime/memory-domain.ts +66 -0
  78. package/src/runtime/memory-guard-contract.ts +73 -0
  79. package/src/runtime/memory-guard.ts +292 -0
  80. package/src/runtime/memory-limit.ts +50 -0
  81. package/src/runtime-mcp/manager.ts +26 -6
  82. package/src/sdk/acp/adapter.ts +8 -1
  83. package/src/sdk/acp/final-text.ts +40 -0
  84. package/src/sdk/acp/mcp.ts +26 -0
  85. package/src/sdk/broker/lifecycle.ts +82 -1
  86. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  87. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  88. package/src/sdk/bus/config.ts +1 -1
  89. package/src/sdk/bus/index.ts +162 -22
  90. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  91. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  92. package/src/sdk/bus/telegram-daemon.ts +531 -53
  93. package/src/sdk/host/control/dispatch.ts +2 -0
  94. package/src/sdk/host/control/operations.ts +1 -0
  95. package/src/sdk/host/host.ts +2 -2
  96. package/src/sdk/lifecycle-session.ts +19 -3
  97. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  98. package/src/sdk/protocol/operation-registry.ts +1 -0
  99. package/src/sdk/session.ts +12 -1
  100. package/src/sdk/startup-capability.ts +6 -0
  101. package/src/session/agent-session.ts +256 -142
  102. package/src/session/fallback-chain-controller.ts +47 -8
  103. package/src/session/internal/managed-session-scope.ts +40 -10
  104. package/src/session/internal/native-publish-outcome.ts +8 -3
  105. package/src/session/session-manager.ts +608 -25
  106. package/src/task/executor.ts +11 -3
  107. package/src/task/index.ts +2 -0
  108. package/src/task/types.ts +22 -11
  109. package/src/task/ultragoal-redteam-activation.ts +75 -0
  110. package/src/tools/resource-gc.ts +496 -120
  111. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -197,21 +197,26 @@ export function mapAgentSessionEventToAcpSessionUpdates(
197
197
  return [toSessionNotification(sessionId, update)];
198
198
  }
199
199
  case "tool_execution_end": {
200
+ const args = getToolExecutionEndArgs(event, options);
200
201
  const resultContent = [...extractDiffToolCallContent(event.result), ...extractToolCallContent(event.result)];
201
- const content = mergeToolUpdateContent(
202
- buildToolStartContent(event.toolName, getToolExecutionEndArgs(event, options)),
203
- resultContent,
204
- );
202
+ const content = mergeToolUpdateContent(buildToolStartContent(event.toolName, args), resultContent);
205
203
  const update: SessionUpdate = {
206
204
  sessionUpdate: "tool_call_update",
207
205
  toolCallId: event.toolCallId,
208
206
  status: event.isError ? "failed" : "completed",
209
207
  rawOutput: event.result,
210
208
  };
209
+ if (event.isError) {
210
+ update.title = `Failed: ${buildToolTitle(event.toolName, args, undefined)}`;
211
+ update.kind = "other";
212
+ }
211
213
  if (content.length > 0) {
212
214
  update.content = content;
213
215
  }
214
- const locations = extractToolLocationsFromResult(event.result, options.cwd);
216
+ const locations = mergeToolLocations(
217
+ extractToolLocations(args, options.cwd),
218
+ extractToolLocationsFromResult(event.result, options.cwd),
219
+ );
215
220
  if (locations.length > 0) {
216
221
  update.locations = locations;
217
222
  }
@@ -295,14 +300,83 @@ export function mapAgentSessionEventToAcpSessionUpdates(
295
300
  return [toSessionNotification(sessionId, { sessionUpdate: "session_info_update", _meta: meta })];
296
301
  }
297
302
  case "auto_retry_start":
303
+ return [
304
+ toSessionNotification(sessionId, {
305
+ sessionUpdate: "session_info_update",
306
+ _meta: {
307
+ gjcPhase: "retrying",
308
+ gjcRetryState: "waiting",
309
+ gjcRetryAttempt: event.attempt,
310
+ gjcRetryMaxAttempts: event.maxAttempts,
311
+ gjcRetryDelayMs: event.delayMs,
312
+ gjcRetryErrorMessage: event.errorMessage,
313
+ gjcRetryUnbounded: event.unbounded ?? false,
314
+ running: true,
315
+ gjcRunning: true,
316
+ },
317
+ }),
318
+ ];
298
319
  case "auto_retry_end":
320
+ return [
321
+ toSessionNotification(sessionId, {
322
+ sessionUpdate: "session_info_update",
323
+ _meta: {
324
+ gjcPhase: event.success ? "responding" : "retry_failed",
325
+ gjcRetryState: event.success ? "succeeded" : "failed",
326
+ gjcRetryAttempt: event.attempt,
327
+ ...(event.finalError ? { gjcRetryFinalError: event.finalError } : {}),
328
+ running: true,
329
+ gjcRunning: true,
330
+ },
331
+ }),
332
+ ];
299
333
  case "ttsr_triggered":
334
+ return [
335
+ toSessionNotification(sessionId, {
336
+ sessionUpdate: "session_info_update",
337
+ _meta: {
338
+ gjcTtsrTriggered: true,
339
+ gjcTtsrRuleCount: event.rules.length,
340
+ },
341
+ }),
342
+ ];
300
343
  case "irc_message":
301
344
  case "subagent_steer_message":
345
+ return [];
302
346
  case "notice":
347
+ return [
348
+ toSessionNotification(sessionId, {
349
+ sessionUpdate: "agent_thought_chunk",
350
+ content: {
351
+ type: "text",
352
+ text: `[${event.level}${event.source ? `:${event.source}` : ""}] ${event.message}\n`,
353
+ },
354
+ }),
355
+ ];
303
356
  case "thinking_level_changed":
357
+ return [
358
+ toSessionNotification(sessionId, {
359
+ sessionUpdate: "session_info_update",
360
+ _meta: { gjcThinkingLevel: event.thinkingLevel ?? "off" },
361
+ }),
362
+ ];
304
363
  case "goal_updated":
305
- return [];
364
+ return [
365
+ toSessionNotification(sessionId, {
366
+ sessionUpdate: "session_info_update",
367
+ _meta: {
368
+ gjcGoalActive: event.goal !== null,
369
+ ...(event.goal
370
+ ? {
371
+ gjcGoalId: event.goal.id,
372
+ gjcGoalStatus: event.goal.status,
373
+ gjcGoalObjective: event.goal.objective,
374
+ }
375
+ : {}),
376
+ ...(event.state ? { gjcGoalModeState: event.state } : {}),
377
+ },
378
+ }),
379
+ ];
306
380
  default:
307
381
  return assertNeverAcp(event);
308
382
  }
@@ -547,6 +621,19 @@ function mergeToolUpdateContent(startContent: ToolCallContent[], resultContent:
547
621
  return merged;
548
622
  }
549
623
 
624
+ function mergeToolLocations(...groups: ToolCallLocation[][]): ToolCallLocation[] {
625
+ const locations: ToolCallLocation[] = [];
626
+ const seen = new Set<string>();
627
+ for (const group of groups) {
628
+ for (const location of group) {
629
+ if (seen.has(location.path)) continue;
630
+ seen.add(location.path);
631
+ locations.push(location);
632
+ }
633
+ }
634
+ return locations;
635
+ }
636
+
550
637
  function isCommandToolName(toolName: string): boolean {
551
638
  return toolName === "bash" || toolName === "shell" || toolName === "exec";
552
639
  }
@@ -1245,8 +1245,14 @@ export class ModelSelectorComponent extends Container {
1245
1245
  new Text(`${prefix}${i === this.#presetScopeIndex ? theme.fg("accent", label) : label}`, 0, 0),
1246
1246
  );
1247
1247
  }
1248
+ this.#listContainer.addChild(new Spacer(1));
1249
+ this.#listContainer.addChild(
1250
+ new Text(theme.fg("muted", " Enter: apply | d: set as default | Esc: back"), 0, 0),
1251
+ );
1248
1252
  } else {
1249
- this.#listContainer.addChild(new Text(theme.fg("muted", " Press Enter to apply this preset"), 0, 0));
1253
+ this.#listContainer.addChild(
1254
+ new Text(theme.fg("muted", " Press Enter to apply or d to set as default"), 0, 0),
1255
+ );
1250
1256
  }
1251
1257
  }
1252
1258
 
@@ -1558,6 +1564,14 @@ export class ModelSelectorComponent extends Container {
1558
1564
  }
1559
1565
 
1560
1566
  #handlePresetLandingInput(keyData: string): void {
1567
+ if (keyData === "d" || keyData === "D") {
1568
+ if (this.#previewProfileName) {
1569
+ this.#presetScopeMenuOpen = true;
1570
+ this.#presetScopeIndex = 1;
1571
+ this.#handlePresetEnter();
1572
+ return;
1573
+ }
1574
+ }
1561
1575
  if (isPrintableCharacter(keyData)) {
1562
1576
  this.#switchToModelMode(keyData);
1563
1577
  return;
@@ -195,7 +195,7 @@ function emptyState(): NotificationsEditorState {
195
195
  sessionScope: "all",
196
196
  richEnabled: true,
197
197
  richDraftEnabled: false,
198
- toolActivityEnabled: true,
198
+ toolActivityEnabled: false,
199
199
  streamingEnabled: true,
200
200
  },
201
201
  };
@@ -1,11 +1,12 @@
1
1
  import { Container, matchesKey, type SelectItem, SelectList, Spacer, Text } from "@gajae-code/tui";
2
+ import { formatKeyHint } from "../../config/keybindings";
2
3
  import type { QueuedMessageEditEntry } from "../../session/agent-session";
3
4
  import { getSelectListTheme, theme } from "../theme/theme";
4
5
  import { DynamicBorder } from "./dynamic-border";
5
6
 
6
7
  const MAX_VISIBLE_QUEUE_MESSAGES = 8;
7
- const RAW_UP = "\x1b[A";
8
- const RAW_DOWN = "\x1b[B";
8
+ type QueueSelectorAction = "tui.select.confirm" | "tui.select.cancel";
9
+ type QueueSelectorNavigationAction = "tui.select.up" | "tui.select.down" | "tui.select.pageUp" | "tui.select.pageDown";
9
10
 
10
11
  export class QueuePaneComponent extends Container {
11
12
  #selectList: SelectList;
@@ -13,11 +14,20 @@ export class QueuePaneComponent extends Container {
13
14
  #selectedIndex: number;
14
15
  #onDelete: (entry: QueuedMessageEditEntry, index: number) => void;
15
16
  #onMove: (entry: QueuedMessageEditEntry, index: number, direction: "up" | "down") => void;
16
-
17
+ #onSelect: (entry: QueuedMessageEditEntry) => void;
18
+ #onClose: () => void;
19
+ #entries: QueuedMessageEditEntry[];
20
+ #matchesSelectAction: (keyData: string, action: QueueSelectorAction) => boolean;
21
+ #resolveSelectNavigation: (keyData: string) => QueueSelectorNavigationAction | undefined;
17
22
  constructor(
18
23
  entries: QueuedMessageEditEntry[],
19
24
  options: {
20
25
  selectedIndex?: number;
26
+ formatKeyHint?: (key: string) => string;
27
+ formatSelectAction?: (action: QueueSelectorAction) => string;
28
+ matchesSelectAction?: (keyData: string, action: QueueSelectorAction) => boolean;
29
+ resolveSelectNavigation?: (keyData: string) => QueueSelectorNavigationAction | undefined;
30
+ onSelect: (entry: QueuedMessageEditEntry) => void;
21
31
  onDelete: (entry: QueuedMessageEditEntry, index: number) => void;
22
32
  onMove: (entry: QueuedMessageEditEntry, index: number, direction: "up" | "down") => void;
23
33
  onClose: () => void;
@@ -25,23 +35,46 @@ export class QueuePaneComponent extends Container {
25
35
  ) {
26
36
  super();
27
37
  this.#onDelete = options.onDelete;
38
+ this.#onSelect = options.onSelect;
28
39
  this.#onMove = options.onMove;
40
+ this.#onClose = options.onClose;
41
+ this.#entries = entries;
42
+ this.#matchesSelectAction =
43
+ options.matchesSelectAction ??
44
+ ((keyData, action) => matchesKey(keyData, action === "tui.select.confirm" ? "enter" : "escape"));
45
+ this.#resolveSelectNavigation = options.resolveSelectNavigation ?? (() => undefined);
29
46
  this.#selectedIndex = Math.max(0, Math.min(options.selectedIndex ?? 0, entries.length - 1));
30
47
  this.#selectedEntry = entries[this.#selectedIndex];
31
48
  const byId = new Map(entries.map(entry => [entry.id, entry]));
49
+ const displayKey = options.formatKeyHint ?? formatKeyHint;
50
+ const selectKeys = `${displayKey("alt+up")}/${displayKey("alt+down")}`;
51
+ const editKey = options.formatSelectAction
52
+ ? options.formatSelectAction("tui.select.confirm") || "Disabled"
53
+ : displayKey("enter");
54
+ const deleteKey = displayKey("delete");
55
+ const moveKeys = `${displayKey("ctrl+up")}/${displayKey("ctrl+down")}`;
56
+ const closeKey = options.formatSelectAction
57
+ ? options.formatSelectAction("tui.select.cancel") || "Disabled"
58
+ : displayKey("escape");
59
+ const itemHint = `${editKey} edit · ${deleteKey} remove · ${moveKeys} move`;
60
+ const controlsHint = `${selectKeys} select · ${itemHint} · ${closeKey} close`;
32
61
  const items: SelectItem[] = entries.map((entry, index) => ({
33
62
  value: entry.id,
34
63
  label: `${entry.label} ${index + 1}`,
35
64
  description: entry.text,
36
- hint: "Del remove · Ctrl+↑/↓ move",
65
+ hint: itemHint,
37
66
  }));
38
67
 
39
68
  this.addChild(new Spacer(1));
40
69
  this.addChild(new Text(theme.bold("Message queue"), 1, 0));
41
- this.addChild(new Text(theme.fg("muted", "Del remove · Ctrl+↑/↓ move · Esc close"), 1, 0));
70
+ this.addChild(new Text(theme.fg("muted", controlsHint), 1, 0));
42
71
  this.addChild(new Spacer(1));
43
72
  this.addChild(new DynamicBorder());
44
73
  this.#selectList = new SelectList(items, MAX_VISIBLE_QUEUE_MESSAGES, getSelectListTheme());
74
+ this.#selectList.onSelect = item => {
75
+ const entry = byId.get(item.value);
76
+ if (entry) this.#onSelect(entry);
77
+ };
45
78
  this.#selectList.onSelectionChange = item => {
46
79
  const index = entries.findIndex(entry => entry.id === item.value);
47
80
  this.#selectedIndex = index === -1 ? this.#selectedIndex : index;
@@ -54,12 +87,25 @@ export class QueuePaneComponent extends Container {
54
87
  }
55
88
 
56
89
  handleInput(keyData: string): void {
57
- if (matchesKey(keyData, "alt+up")) {
58
- this.#selectList.handleInput(RAW_UP);
90
+ if (this.#matchesSelectAction(keyData, "tui.select.confirm")) {
91
+ if (this.#selectedEntry) this.#onSelect(this.#selectedEntry);
92
+ return;
93
+ }
94
+ if (this.#matchesSelectAction(keyData, "tui.select.cancel")) {
95
+ this.#onClose();
96
+ return;
97
+ }
98
+ const navigation = this.#resolveSelectNavigation(keyData);
99
+ if (navigation) {
100
+ this.#selectList.handleNavigation(navigation);
59
101
  return;
60
102
  }
61
- if (matchesKey(keyData, "alt+down")) {
62
- this.#selectList.handleInput(RAW_DOWN);
103
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "escape")) return;
104
+ if (matchesKey(keyData, "alt+up") || matchesKey(keyData, "alt+down")) {
105
+ const direction = matchesKey(keyData, "alt+up") ? -1 : 1;
106
+ this.#selectedIndex = (this.#selectedIndex + direction + this.#entries.length) % this.#entries.length;
107
+ this.#selectedEntry = this.#entries[this.#selectedIndex];
108
+ this.#selectList.setSelectedIndex(this.#selectedIndex);
63
109
  return;
64
110
  }
65
111
  if (matchesKey(keyData, "ctrl+up") || matchesKey(keyData, "ctrl+shift+up")) {
@@ -1,12 +1,13 @@
1
1
  import { Container, matchesKey, type SelectItem, SelectList, Spacer, Text } from "@gajae-code/tui";
2
+ import { formatKeyHint } from "../../config/keybindings";
2
3
  import type { QueuedMessageEditEntry } from "../../session/agent-session";
3
4
  import { getSelectListTheme, theme } from "../theme/theme";
4
5
  import { DynamicBorder } from "./dynamic-border";
5
6
 
6
7
  const MAX_VISIBLE_QUEUED_MESSAGES = 8;
7
- const RAW_UP = "\x1b[A";
8
- const RAW_DOWN = "\x1b[B";
9
8
  type QueuedMessageMoveDirection = "up" | "down";
9
+ type QueueSelectorAction = "tui.select.confirm" | "tui.select.cancel";
10
+ type QueueSelectorNavigationAction = "tui.select.up" | "tui.select.down" | "tui.select.pageUp" | "tui.select.pageDown";
10
11
 
11
12
  export type { QueuedMessageMoveDirection };
12
13
 
@@ -16,32 +17,61 @@ export class QueuedMessageSelectorComponent extends Container {
16
17
  #selectedIndex = 0;
17
18
  #onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void;
18
19
  #onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void;
19
-
20
+ #onSelect: (entry: QueuedMessageEditEntry) => void;
21
+ #onCancel: () => void;
22
+ #entries: QueuedMessageEditEntry[];
23
+ #matchesSelectAction: (keyData: string, action: QueueSelectorAction) => boolean;
24
+ #resolveSelectNavigation: (keyData: string) => QueueSelectorNavigationAction | undefined;
20
25
  constructor(
21
26
  entries: QueuedMessageEditEntry[],
22
27
  onSelect: (entry: QueuedMessageEditEntry) => void,
23
28
  onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void,
24
29
  onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void,
25
30
  onCancel: () => void,
26
- options?: { selectedIndex?: number },
31
+ options?: {
32
+ selectedIndex?: number;
33
+ formatKeyHint?: (key: string) => string;
34
+ formatSelectAction?: (action: QueueSelectorAction) => string;
35
+ matchesSelectAction?: (keyData: string, action: QueueSelectorAction) => boolean;
36
+ resolveSelectNavigation?: (keyData: string) => QueueSelectorNavigationAction | undefined;
37
+ },
27
38
  ) {
28
39
  super();
29
40
 
30
41
  this.#onDelete = onDelete;
31
42
  this.#onMove = onMove;
43
+ this.#onSelect = onSelect;
44
+ this.#onCancel = onCancel;
45
+ this.#entries = entries;
46
+ this.#matchesSelectAction =
47
+ options?.matchesSelectAction ??
48
+ ((keyData, action) => matchesKey(keyData, action === "tui.select.confirm" ? "enter" : "escape"));
49
+ this.#resolveSelectNavigation = options?.resolveSelectNavigation ?? (() => undefined);
32
50
  const byId = new Map(entries.map(entry => [entry.id, entry]));
33
51
  this.#selectedIndex = Math.max(0, Math.min(options?.selectedIndex ?? 0, entries.length - 1));
34
52
  this.#selectedEntry = entries[this.#selectedIndex];
53
+ const displayKey = options?.formatKeyHint ?? formatKeyHint;
54
+ const selectKeys = `${displayKey("alt+up")}/${displayKey("alt+down")}`;
55
+ const editKey = options?.formatSelectAction
56
+ ? options.formatSelectAction("tui.select.confirm") || "Disabled"
57
+ : displayKey("enter");
58
+ const deleteKey = displayKey("delete");
59
+ const moveKeys = `${displayKey("ctrl+up")}/${displayKey("ctrl+down")}`;
60
+ const cancelKey = options?.formatSelectAction
61
+ ? options.formatSelectAction("tui.select.cancel") || "Disabled"
62
+ : displayKey("escape");
63
+ const itemHint = `${editKey} edit · ${deleteKey} remove · ${moveKeys} move`;
64
+ const controlsHint = `${selectKeys} select · ${itemHint} · ${cancelKey} cancel`;
35
65
  const items: SelectItem[] = entries.map((entry, index) => ({
36
66
  value: entry.id,
37
67
  label: `${entry.label} ${index + 1}`,
38
68
  description: entry.text,
39
- hint: "Enter edit · Del remove · Ctrl+↑/↓ move",
69
+ hint: itemHint,
40
70
  }));
41
71
 
42
72
  this.addChild(new Spacer(1));
43
73
  this.addChild(new Text(theme.bold("Queued messages"), 1, 0));
44
- this.addChild(new Text(theme.fg("muted", "Enter edit · Del remove · Ctrl+↑/↓ move · Esc cancel"), 1, 0));
74
+ this.addChild(new Text(theme.fg("muted", controlsHint), 1, 0));
45
75
  this.addChild(new Spacer(1));
46
76
  this.addChild(new DynamicBorder());
47
77
 
@@ -63,12 +93,25 @@ export class QueuedMessageSelectorComponent extends Container {
63
93
  }
64
94
 
65
95
  handleInput(keyData: string): void {
66
- if (matchesKey(keyData, "alt+up")) {
67
- this.#selectList.handleInput(RAW_UP);
96
+ if (this.#matchesSelectAction(keyData, "tui.select.confirm")) {
97
+ if (this.#selectedEntry) this.#onSelect(this.#selectedEntry);
98
+ return;
99
+ }
100
+ if (this.#matchesSelectAction(keyData, "tui.select.cancel")) {
101
+ this.#onCancel();
102
+ return;
103
+ }
104
+ const navigation = this.#resolveSelectNavigation(keyData);
105
+ if (navigation) {
106
+ this.#selectList.handleNavigation(navigation);
68
107
  return;
69
108
  }
70
- if (matchesKey(keyData, "alt+down")) {
71
- this.#selectList.handleInput(RAW_DOWN);
109
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "escape")) return;
110
+ if (matchesKey(keyData, "alt+up") || matchesKey(keyData, "alt+down")) {
111
+ const direction = matchesKey(keyData, "alt+up") ? -1 : 1;
112
+ this.#selectedIndex = (this.#selectedIndex + direction + this.#entries.length) % this.#entries.length;
113
+ this.#selectedEntry = this.#entries[this.#selectedIndex];
114
+ this.#selectList.setSelectedIndex(this.#selectedIndex);
72
115
  return;
73
116
  }
74
117
  if (matchesKey(keyData, "ctrl+up") || matchesKey(keyData, "ctrl+shift+up")) {
@@ -70,16 +70,13 @@ export interface StatusLineActionHint {
70
70
 
71
71
  const ACTION_HINT_PRIORITY: readonly AppKeybinding[] = [
72
72
  "app.message.sendNow",
73
- "app.message.queue",
74
73
  "app.message.followUp",
74
+ "app.message.queue",
75
75
  "app.message.dequeue",
76
76
  "app.commandPalette.open",
77
77
  "app.plan.toggle",
78
78
  "app.mode.cycle",
79
- "app.thinking.cycle",
80
- "app.model.select",
81
79
  "app.model.cycleForward",
82
- "app.history.search",
83
80
  "app.session.togglePath",
84
81
  "app.session.toggleSort",
85
82
  "app.session.rename",
@@ -20,6 +20,7 @@ export interface WelcomeComponentOptions {
20
20
  getViewportRows?: () => number | undefined;
21
21
  getReservedBottomRows?: (termWidth: number) => number;
22
22
  changelogMarkdown?: string;
23
+ rightGutterWidth?: number;
23
24
  collapseChangelog?: boolean;
24
25
  buildLabel?: string;
25
26
  keyDisplayContext?: KeyDisplayContext;
@@ -110,7 +111,8 @@ export class WelcomeComponent implements Component {
110
111
  }
111
112
 
112
113
  render(termWidth: number): string[] {
113
- const boxWidth = Math.max(0, termWidth);
114
+ const rightGutterWidth = this.#rightGutterWidth(termWidth);
115
+ const boxWidth = Math.max(0, termWidth - rightGutterWidth);
114
116
  if (boxWidth < 4) {
115
117
  return [];
116
118
  }
@@ -245,7 +247,7 @@ export class WelcomeComponent implements Component {
245
247
  lines.push(tl + titleStyled + theme.fg("dim", hChar.repeat(afterTitle)) + tr);
246
248
  }
247
249
  if (outputRows === 1) {
248
- return lines;
250
+ return this.#withRightGutter(lines, rightGutterWidth);
249
251
  }
250
252
 
251
253
  if (showRightColumn) {
@@ -273,7 +275,7 @@ export class WelcomeComponent implements Component {
273
275
  lines.push(bl + h.repeat(leftCol) + br);
274
276
  }
275
277
 
276
- return lines;
278
+ return this.#withRightGutter(lines, rightGutterWidth);
277
279
  }
278
280
 
279
281
  /** Center text within a given width */
@@ -295,6 +297,19 @@ export class WelcomeComponent implements Component {
295
297
  }
296
298
  return str + padding(width - visLen);
297
299
  }
300
+ #rightGutterWidth(termWidth: number): number {
301
+ const configured = this.options.rightGutterWidth ?? 0;
302
+ if (!Number.isFinite(configured) || configured <= 0) return 0;
303
+ const gutterWidth = Math.floor(configured);
304
+ return Math.min(gutterWidth, Math.max(0, termWidth - 4));
305
+ }
306
+
307
+ #withRightGutter(lines: string[], rightGutterWidth: number): string[] {
308
+ if (rightGutterWidth <= 0) return lines;
309
+ const gutter = padding(rightGutterWidth);
310
+ return lines.map(line => line + gutter);
311
+ }
312
+
298
313
  #targetRows(termWidth: number): number | undefined {
299
314
  const viewportRows = this.options.getViewportRows?.();
300
315
  if (typeof viewportRows !== "number" || !Number.isFinite(viewportRows) || viewportRows <= 0) {
@@ -65,6 +65,8 @@ export function buildHelpMarkdown(keybindings: Pick<KeybindingsManager, "getAcce
65
65
  const displayKey = (action: Keybinding): string => keybindings.getAccessibleDisplayString(action) || "Disabled";
66
66
  const sessionNewKey = formatHotkeyMarkdownCode(displayKey("app.session.new"));
67
67
  const selectModelKey = formatHotkeyMarkdownCode(displayKey("app.model.select"));
68
+ const queueMessageKey = formatHotkeyMarkdownCode(displayKey("app.message.queue"));
69
+ const dequeueMessageKey = formatHotkeyMarkdownCode(displayKey("app.message.dequeue"));
68
70
  const autocompleteNavigationKeys = formatHotkeyMarkdownCode(
69
71
  `${displayKey("tui.select.up")}/${displayKey("tui.select.down")}`,
70
72
  false,
@@ -84,6 +86,8 @@ export function buildHelpMarkdown(keybindings: Pick<KeybindingsManager, "getAcce
84
86
  "| Show session details | `/session info` |",
85
87
  "| Delete current session transcript/artifacts | `/session delete` |",
86
88
  `| Select a model | \`/model\` or ${selectModelKey} |`,
89
+ `| Queue a message for the next turn | ${queueMessageKey} |`,
90
+ `| Select or edit a queued message | ${dequeueMessageKey} |`,
87
91
  "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
88
92
  "",
89
93
  "**Finding commands**",
@@ -425,6 +425,7 @@ export class ExtensionUiController {
425
425
  getSystemPrompt: () => this.ctx.session.systemPrompt,
426
426
  clearContext: () => this.ctx.session.clearContext(),
427
427
  cycleModel: () => this.ctx.session.cycleModel(),
428
+ setModelProfile: name => this.ctx.session.activateModelProfileForControl(name),
428
429
  cycleThinkingLevel: () => this.ctx.session.cycleThinkingLevel(),
429
430
  setQueueMode: (kind, mode) => {
430
431
  if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
@@ -446,11 +447,7 @@ export class ExtensionUiController {
446
447
  operateGoal: (op, objective) => this.ctx.session.operateGoal(op, objective),
447
448
  getSkillState: () =>
448
449
  this.ctx.session.skills.map(skill => ({ name: skill.name, description: skill.description })),
449
- getConfigItems: () => ({
450
- steeringMode: this.ctx.session.steeringMode,
451
- followUpMode: this.ctx.session.followUpMode,
452
- interruptMode: this.ctx.session.interruptMode,
453
- }),
450
+ getConfigItems: () => this.ctx.session.getSdkConfigItems(),
454
451
  getBranchCandidates: () => this.ctx.sessionManager.getTree(),
455
452
  getExtensions: () => this.ctx.session.extensionRunner?.getExtensionPaths() ?? [],
456
453
  setSdkPermissionProvider: provider => this.ctx.session.setSdkPermissionProvider(provider),
@@ -727,6 +724,7 @@ export class ExtensionUiController {
727
724
  getSystemPrompt: () => this.ctx.session.systemPrompt,
728
725
  clearContext: () => this.ctx.session.clearContext(),
729
726
  cycleModel: () => this.ctx.session.cycleModel(),
727
+ setModelProfile: name => this.ctx.session.activateModelProfileForControl(name),
730
728
  cycleThinkingLevel: () => this.ctx.session.cycleThinkingLevel(),
731
729
  setQueueMode: (kind, mode) => {
732
730
  if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
@@ -745,11 +743,7 @@ export class ExtensionUiController {
745
743
  },
746
744
  getSkillState: () =>
747
745
  this.ctx.session.skills.map(skill => ({ name: skill.name, description: skill.description })),
748
- getConfigItems: () => ({
749
- steeringMode: this.ctx.session.steeringMode,
750
- followUpMode: this.ctx.session.followUpMode,
751
- interruptMode: this.ctx.session.interruptMode,
752
- }),
746
+ getConfigItems: () => this.ctx.session.getSdkConfigItems(),
753
747
  getBranchCandidates: () => this.ctx.sessionManager.getTree(),
754
748
  getExtensions: () => this.ctx.session.extensionRunner?.getExtensionPaths() ?? [],
755
749
  setSdkPermissionProvider: provider => this.ctx.session.setSdkPermissionProvider(provider),
@@ -33,6 +33,13 @@ import type { PasteTextContext } from "../components/custom-editor";
33
33
  import { QueuePaneComponent } from "../components/queue-pane";
34
34
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
35
35
 
36
+ const QUEUE_SELECTOR_NAVIGATION_ACTIONS = [
37
+ "tui.select.up",
38
+ "tui.select.down",
39
+ "tui.select.pageUp",
40
+ "tui.select.pageDown",
41
+ ] as const;
42
+
36
43
  interface Expandable {
37
44
  setExpanded(expanded: boolean): void;
38
45
  setManuallyExpanded?(expanded: boolean): void;
@@ -151,15 +158,24 @@ export class InputController {
151
158
  case "app.editor.external":
152
159
  return Boolean(getEditorCommand());
153
160
  case "app.message.followUp":
154
- case "app.message.queue":
155
161
  return (
156
162
  this.ctx.session.isStreaming ||
157
163
  this.ctx.session.isCompacting ||
158
164
  this.ctx.session.isBashRunning ||
159
165
  this.ctx.session.isEvalRunning
160
166
  );
167
+ case "app.message.queue":
168
+ return (
169
+ this.ctx.editor.getText().trim().length > 0 &&
170
+ (this.ctx.session.isStreaming ||
171
+ this.ctx.session.isCompacting ||
172
+ this.ctx.session.isBashRunning ||
173
+ this.ctx.session.isEvalRunning)
174
+ );
161
175
  case "app.message.dequeue":
162
- return this.ctx.session.queuedMessageCount > 0;
176
+ return (
177
+ this.ctx.session.getQueuedMessageEntries().length > 0 || this.ctx.compactionQueuedMessages.length > 0
178
+ );
163
179
  case "app.clipboard.copyPrompt":
164
180
  return this.ctx.editor.getText().length > 0;
165
181
  case "app.session.tree":
@@ -1037,6 +1053,21 @@ export class InputController {
1037
1053
  };
1038
1054
  const pane = new QueuePaneComponent(entries, {
1039
1055
  selectedIndex,
1056
+ formatKeyHint: key => this.ctx.keybindings.formatKeyHint(key),
1057
+ formatSelectAction: action => this.ctx.keybindings.getDisplayString(action),
1058
+ matchesSelectAction: (keyData, action) =>
1059
+ this.ctx.keybindings.getKeys(action).some(key => matchesKey(keyData, key)),
1060
+ resolveSelectNavigation: keyData =>
1061
+ QUEUE_SELECTOR_NAVIGATION_ACTIONS.find(action =>
1062
+ this.ctx.keybindings.getKeys(action).some(key => matchesKey(keyData, key)),
1063
+ ),
1064
+ onSelect: entry => {
1065
+ const restored = this.#restoreQueuedMessageToEditor(entry);
1066
+ close();
1067
+ this.ctx.showStatus(
1068
+ restored === 0 ? "Queued message is no longer available" : "Restored queued message to editor",
1069
+ );
1070
+ },
1040
1071
  onDelete: (entry, index) => {
1041
1072
  const deleted = this.ctx.session.removeQueuedMessageForEditing(entry.id) !== undefined;
1042
1073
  const remaining = this.ctx.session.getQueuedMessageEntries();
@@ -1209,7 +1240,17 @@ export class InputController {
1209
1240
  this.#restoreEditorFocus();
1210
1241
  this.ctx.ui.requestRender();
1211
1242
  },
1212
- { selectedIndex },
1243
+ {
1244
+ selectedIndex,
1245
+ formatKeyHint: key => this.ctx.keybindings.formatKeyHint(key),
1246
+ formatSelectAction: action => this.ctx.keybindings.getDisplayString(action),
1247
+ matchesSelectAction: (keyData, action) =>
1248
+ this.ctx.keybindings.getKeys(action).some(key => matchesKey(keyData, key)),
1249
+ resolveSelectNavigation: keyData =>
1250
+ QUEUE_SELECTOR_NAVIGATION_ACTIONS.find(action =>
1251
+ this.ctx.keybindings.getKeys(action).some(key => matchesKey(keyData, key)),
1252
+ ),
1253
+ },
1213
1254
  );
1214
1255
  this.ctx.editorContainer.clear();
1215
1256
  this.ctx.editorContainer.addChild(selector);