@gajae-code/coding-agent 0.5.4 → 0.6.1

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 (155) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/web-search-cli.d.ts +12 -0
  3. package/dist/types/commands/rlm.d.ts +10 -0
  4. package/dist/types/commands/web-search.d.ts +54 -0
  5. package/dist/types/config/keybindings.d.ts +10 -0
  6. package/dist/types/config/model-profiles.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +3 -0
  8. package/dist/types/config/models-config-schema.d.ts +3 -0
  9. package/dist/types/config/settings-schema.d.ts +61 -3
  10. package/dist/types/edit/notebook.d.ts +3 -0
  11. package/dist/types/eval/py/executor.d.ts +3 -0
  12. package/dist/types/eval/py/kernel.d.ts +3 -1
  13. package/dist/types/eval/py/runtime.d.ts +9 -1
  14. package/dist/types/exec/bash-executor.d.ts +4 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  16. package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  18. package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
  19. package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
  20. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
  21. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  22. package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
  23. package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
  24. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
  25. package/dist/types/goals/state.d.ts +1 -1
  26. package/dist/types/goals/tools/goal-tool.d.ts +2 -0
  27. package/dist/types/main.d.ts +11 -0
  28. package/dist/types/modes/components/custom-editor.d.ts +4 -2
  29. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
  30. package/dist/types/modes/components/model-selector.d.ts +5 -2
  31. package/dist/types/modes/components/status-line.d.ts +4 -1
  32. package/dist/types/modes/controllers/input-controller.d.ts +3 -0
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  34. package/dist/types/modes/print-mode.d.ts +6 -0
  35. package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
  36. package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
  37. package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
  38. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
  39. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
  40. package/dist/types/rlm/artifacts.d.ts +9 -0
  41. package/dist/types/rlm/complete-research-tool.d.ts +35 -0
  42. package/dist/types/rlm/data-context.d.ts +6 -0
  43. package/dist/types/rlm/index.d.ts +35 -0
  44. package/dist/types/rlm/notebook.d.ts +12 -0
  45. package/dist/types/rlm/preset.d.ts +23 -0
  46. package/dist/types/rlm/python-tool.d.ts +16 -0
  47. package/dist/types/rlm/report.d.ts +14 -0
  48. package/dist/types/rlm/types.d.ts +37 -0
  49. package/dist/types/sdk.d.ts +7 -0
  50. package/dist/types/session/agent-session.d.ts +21 -0
  51. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
  52. package/dist/types/tools/browser/attach.d.ts +19 -3
  53. package/dist/types/tools/browser/registry.d.ts +15 -0
  54. package/dist/types/tools/browser/render.d.ts +3 -0
  55. package/dist/types/tools/browser.d.ts +18 -1
  56. package/dist/types/tools/computer/render.d.ts +17 -0
  57. package/dist/types/tools/computer.d.ts +465 -0
  58. package/dist/types/tools/index.d.ts +24 -1
  59. package/dist/types/tools/job.d.ts +13 -0
  60. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  61. package/dist/types/web/search/index.d.ts +32 -2
  62. package/dist/types/web/search/providers/base.d.ts +22 -0
  63. package/dist/types/web/search/providers/xai.d.ts +64 -0
  64. package/dist/types/web/search/types.d.ts +11 -3
  65. package/package.json +7 -7
  66. package/src/cli/web-search-cli.ts +123 -8
  67. package/src/cli.ts +2 -0
  68. package/src/commands/rlm.ts +19 -0
  69. package/src/commands/web-search.ts +66 -0
  70. package/src/config/keybindings.ts +11 -0
  71. package/src/config/model-profiles.ts +11 -3
  72. package/src/config/model-registry.ts +55 -1
  73. package/src/config/models-config-schema.ts +1 -0
  74. package/src/config/settings-schema.ts +67 -1
  75. package/src/edit/notebook.ts +6 -2
  76. package/src/eval/py/executor.ts +8 -1
  77. package/src/eval/py/kernel.ts +9 -4
  78. package/src/eval/py/runtime.ts +153 -32
  79. package/src/exec/bash-executor.ts +10 -4
  80. package/src/extensibility/custom-tools/types.ts +2 -0
  81. package/src/extensibility/custom-tools/wrapper.ts +2 -0
  82. package/src/extensibility/extensions/types.ts +2 -0
  83. package/src/extensibility/extensions/wrapper.ts +1 -0
  84. package/src/gjc-runtime/launch-tmux.ts +129 -1
  85. package/src/gjc-runtime/session-state-sidecar.ts +61 -1
  86. package/src/gjc-runtime/tmux-common.ts +26 -2
  87. package/src/gjc-runtime/tmux-gc.ts +40 -27
  88. package/src/gjc-runtime/tmux-sessions.ts +13 -1
  89. package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
  90. package/src/goals/runtime.ts +4 -3
  91. package/src/goals/state.ts +1 -1
  92. package/src/goals/tools/goal-tool.ts +16 -3
  93. package/src/internal-urls/docs-index.generated.ts +13 -9
  94. package/src/main.ts +28 -3
  95. package/src/modes/components/custom-editor.ts +13 -4
  96. package/src/modes/components/custom-model-preset-wizard.ts +293 -0
  97. package/src/modes/components/hook-selector.ts +1 -1
  98. package/src/modes/components/model-selector.ts +72 -29
  99. package/src/modes/components/skill-message.ts +62 -8
  100. package/src/modes/components/status-line.ts +13 -1
  101. package/src/modes/controllers/input-controller.ts +60 -11
  102. package/src/modes/controllers/selector-controller.ts +39 -0
  103. package/src/modes/interactive-mode.ts +1 -1
  104. package/src/modes/print-mode.ts +14 -4
  105. package/src/modes/rpc/rpc-client.ts +250 -80
  106. package/src/modes/rpc/rpc-mode.ts +6 -12
  107. package/src/modes/rpc/rpc-socket-security.ts +103 -0
  108. package/src/modes/rpc/rpc-types.ts +10 -0
  109. package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
  110. package/src/modes/shared/agent-wire/command-validation.ts +1 -0
  111. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  112. package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
  113. package/src/modes/utils/hotkeys-markdown.ts +4 -2
  114. package/src/modes/utils/ui-helpers.ts +2 -2
  115. package/src/prompts/goals/goal-continuation.md +1 -0
  116. package/src/prompts/goals/goal-mode-active.md +1 -0
  117. package/src/prompts/system/rlm-report-command.md +1 -0
  118. package/src/prompts/system/rlm-research.md +23 -0
  119. package/src/prompts/tools/bash.md +23 -2
  120. package/src/prompts/tools/browser.md +7 -3
  121. package/src/prompts/tools/computer.md +74 -0
  122. package/src/prompts/tools/goal.md +3 -0
  123. package/src/prompts/tools/job.md +9 -1
  124. package/src/prompts/tools/web-search.md +7 -0
  125. package/src/rlm/artifacts.ts +60 -0
  126. package/src/rlm/complete-research-tool.ts +163 -0
  127. package/src/rlm/data-context.ts +26 -0
  128. package/src/rlm/index.ts +339 -0
  129. package/src/rlm/notebook.ts +108 -0
  130. package/src/rlm/preset.ts +76 -0
  131. package/src/rlm/python-tool.ts +68 -0
  132. package/src/rlm/report.ts +70 -0
  133. package/src/rlm/types.ts +40 -0
  134. package/src/sdk.ts +12 -0
  135. package/src/session/agent-session.ts +48 -3
  136. package/src/slash-commands/builtin-registry.ts +17 -0
  137. package/src/tools/bash-allowed-prefixes.ts +84 -1
  138. package/src/tools/bash.ts +80 -13
  139. package/src/tools/browser/attach.ts +103 -3
  140. package/src/tools/browser/registry.ts +176 -2
  141. package/src/tools/browser/render.ts +9 -1
  142. package/src/tools/browser.ts +33 -0
  143. package/src/tools/computer/render.ts +78 -0
  144. package/src/tools/computer.ts +640 -0
  145. package/src/tools/index.ts +41 -1
  146. package/src/tools/job.ts +88 -5
  147. package/src/tools/json-tree.ts +42 -29
  148. package/src/tools/renderers.ts +2 -0
  149. package/src/tools/tool-timeouts.ts +1 -0
  150. package/src/web/search/index.ts +27 -2
  151. package/src/web/search/provider.ts +16 -1
  152. package/src/web/search/providers/base.ts +22 -0
  153. package/src/web/search/providers/xai.ts +511 -0
  154. package/src/web/search/render.ts +7 -0
  155. package/src/web/search/types.ts +11 -1
@@ -1,9 +1,20 @@
1
1
  import type { TextContent } from "@gajae-code/ai";
2
2
  import type { Component } from "@gajae-code/tui";
3
- import { Box, Container, Markdown, Spacer, Text, truncateToWidth } from "@gajae-code/tui";
3
+ import {
4
+ Box,
5
+ Container,
6
+ Markdown,
7
+ replaceTabs,
8
+ Spacer,
9
+ Text,
10
+ truncateToWidth,
11
+ wrapTextWithAnsi,
12
+ } from "@gajae-code/tui";
4
13
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
5
14
  import type { CustomMessage, SkillPromptDetails } from "../../session/messages";
6
15
 
16
+ const COLLAPSED_ARGS_PREVIEW_WIDTH = 96;
17
+ const COLLAPSED_ARGS_PREVIEW_LINES = 6;
7
18
  export class SkillMessageComponent extends Container {
8
19
  #box: Box;
9
20
  #contentComponent?: Component;
@@ -43,14 +54,16 @@ export class SkillMessageComponent extends Container {
43
54
  const name = details?.name ?? "unknown";
44
55
  const args = details?.args?.trim();
45
56
 
46
- // Single compact line: `[skill] <name>: <args>`. The summary is the
47
- // args the user typed; with none, just `[skill] <name>`. Collapsed to
48
- // one line path / line-count / full prompt body are debugging detail
49
- // and only render once expanded.
50
- const summary = args ? truncateToWidth(args.replace(/\s+/g, " "), 72) : undefined;
57
+ // Collapsed view keeps args readable without exposing the full prompt body.
58
+ // Each visual line is bounded, but multi-line arguments remain multi-line so
59
+ // the invocation context is not mistaken for a one-line truncated payload.
60
+ const argsPreview = args ? this.#formatArgsPreview(args) : [];
51
61
  const header = `${theme.fg("customMessageLabel", theme.bold("[skill]"))} ${theme.fg("customMessageText", name)}`;
52
- const headerText = summary ? `${header}${theme.fg("customMessageText", `: ${summary}`)}` : header;
53
- this.#box.addChild(new Text(headerText, 0, 0));
62
+ this.#box.addChild(new Text(header, 0, 0));
63
+ if (argsPreview.length > 0) {
64
+ this.#box.addChild(new Spacer(1));
65
+ this.#box.addChild(new Text(argsPreview.map(line => theme.fg("customMessageText", line)).join("\n"), 0, 0));
66
+ }
54
67
 
55
68
  if (!this.#expanded) {
56
69
  return;
@@ -70,6 +83,18 @@ export class SkillMessageComponent extends Container {
70
83
  );
71
84
  }
72
85
 
86
+ if (args) {
87
+ this.#box.addChild(new Spacer(1));
88
+ const argsHeader = theme.fg("customMessageLabel", theme.bold("Arguments"));
89
+ this.#box.addChild(new Text(argsHeader, 0, 0));
90
+ this.#box.addChild(new Spacer(1));
91
+ this.#box.addChild(
92
+ new Markdown(replaceTabs(args), 0, 0, getMarkdownTheme(), {
93
+ color: (value: string) => theme.fg("customMessageText", value),
94
+ }),
95
+ );
96
+ }
97
+
73
98
  const text = this.#extractText();
74
99
  if (!text) {
75
100
  return;
@@ -86,6 +111,35 @@ export class SkillMessageComponent extends Container {
86
111
  this.#box.addChild(this.#contentComponent);
87
112
  }
88
113
 
114
+ #formatArgsPreview(args: string): string[] {
115
+ const preview: string[] = [];
116
+ let omitted = false;
117
+ const sourceLines = replaceTabs(args).split(/\r?\n/);
118
+ for (let index = 0; index < sourceLines.length; index += 1) {
119
+ const sourceLine = sourceLines[index]?.trimEnd() ?? "";
120
+ const wrapped = wrapTextWithAnsi(sourceLine.length > 0 ? sourceLine : " ", COLLAPSED_ARGS_PREVIEW_WIDTH);
121
+ for (const line of wrapped.length > 0 ? wrapped : [""]) {
122
+ if (preview.length >= COLLAPSED_ARGS_PREVIEW_LINES) {
123
+ omitted = true;
124
+ break;
125
+ }
126
+ preview.push(truncateToWidth(line, COLLAPSED_ARGS_PREVIEW_WIDTH));
127
+ }
128
+ if (omitted) break;
129
+ }
130
+ if (sourceLines.length > 0 && preview.length === COLLAPSED_ARGS_PREVIEW_LINES) {
131
+ const visibleSource = preview.join("\n");
132
+ omitted ||= replaceTabs(args).trimEnd().length > visibleSource.trimEnd().length;
133
+ }
134
+ if (omitted && preview.length > 0) {
135
+ preview[preview.length - 1] = truncateToWidth(
136
+ `${preview[preview.length - 1]} …`,
137
+ COLLAPSED_ARGS_PREVIEW_WIDTH,
138
+ );
139
+ }
140
+ return preview;
141
+ }
142
+
89
143
  #extractText(): string {
90
144
  if (typeof this.message.content === "string") {
91
145
  return this.message.content;
@@ -44,6 +44,10 @@ export interface StatusLineSettings {
44
44
  sessionAccent?: boolean;
45
45
  }
46
46
 
47
+ export interface StatusLineComponentOptions {
48
+ version?: string;
49
+ }
50
+
47
51
  // ═══════════════════════════════════════════════════════════════════════════
48
52
  // Per-message token cache
49
53
  // ═══════════════════════════════════════════════════════════════════════════
@@ -161,6 +165,7 @@ export class StatusLineComponent implements Component {
161
165
  #skillHudEntries: SkillActiveEntry[] = [];
162
166
  #skillHudLastFetch = 0;
163
167
  #skillHudInFlight = false;
168
+ #version: string | undefined;
164
169
 
165
170
  // Git status caching (1s TTL)
166
171
  #cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
@@ -197,7 +202,10 @@ export class StatusLineComponent implements Component {
197
202
  #nonMessageTokensCache: number | undefined;
198
203
  #nonMessageInputsKey: string | undefined;
199
204
 
200
- constructor(private readonly session: AgentSession) {
205
+ constructor(
206
+ private readonly session: AgentSession,
207
+ options: StatusLineComponentOptions = {},
208
+ ) {
201
209
  this.#settings = {
202
210
  preset: settings.get("statusLine.preset"),
203
211
  leftSegments: settings.get("statusLine.leftSegments"),
@@ -208,6 +216,7 @@ export class StatusLineComponent implements Component {
208
216
  segmentOptions: settings.getGroup("statusLine").segmentOptions,
209
217
  sessionAccent: settings.get("statusLine.sessionAccent"),
210
218
  };
219
+ this.#version = options.version?.trim() || undefined;
211
220
  }
212
221
 
213
222
  updateSettings(settings: StatusLineSettings): void {
@@ -701,6 +710,9 @@ export class StatusLineComponent implements Component {
701
710
  const label = `${formatCount("job", runningBackgroundJobs)} running`;
702
711
  rightParts.push(theme.fg("statusLineSubagents", `${icon}${label}`));
703
712
  }
713
+ if (this.#version) {
714
+ rightParts.push(theme.fg("dim", `v${this.#version}`));
715
+ }
704
716
  const topFillWidth = Math.max(0, width);
705
717
  const left = [...leftParts];
706
718
  const right = [...rightParts];
@@ -34,6 +34,8 @@ function isExpandable(obj: unknown): obj is Expandable {
34
34
  export class InputController {
35
35
  constructor(private ctx: InteractiveModeContext) {}
36
36
 
37
+ #lastBackgroundFoldKeyTime = 0;
38
+
37
39
  /** Set after a first Esc silently consumes a queued steer. Kept until the
38
40
  * queued steer is either cancelled by a second Esc or drained by continuation,
39
41
  * so abort cleanup going idle cannot turn the second Esc into an idle action. */
@@ -195,6 +197,8 @@ export class InputController {
195
197
  this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
196
198
  this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
197
199
  this.ctx.editor.onDequeue = () => this.handleDequeue();
200
+ this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
201
+ this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
198
202
 
199
203
  this.ctx.editor.clearCustomKeyHandlers();
200
204
  // Wire up extension shortcuts
@@ -206,16 +210,22 @@ export class InputController {
206
210
  }
207
211
 
208
212
  for (const key of this.ctx.keybindings.getKeys("app.session.new")) {
209
- this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.handleClearCommand());
213
+ this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleClearCommand());
210
214
  }
211
215
  for (const key of this.ctx.keybindings.getKeys("app.session.tree")) {
212
- this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showTreeSelector());
216
+ this.ctx.editor.setCustomKeyHandler(key, () => {
217
+ this.ctx.showTreeSelector();
218
+ });
213
219
  }
214
220
  for (const key of this.ctx.keybindings.getKeys("app.session.fork")) {
215
- this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showUserMessageSelector());
221
+ this.ctx.editor.setCustomKeyHandler(key, () => {
222
+ this.ctx.showUserMessageSelector();
223
+ });
216
224
  }
217
225
  for (const key of this.ctx.keybindings.getKeys("app.session.resume")) {
218
- this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionSelector());
226
+ this.ctx.editor.setCustomKeyHandler(key, () => {
227
+ this.ctx.showSessionSelector();
228
+ });
219
229
  }
220
230
  for (const key of this.ctx.keybindings.getKeys("app.message.followUp")) {
221
231
  this.ctx.editor.setCustomKeyHandler(key, () => void this.handleFollowUp());
@@ -224,13 +234,22 @@ export class InputController {
224
234
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
225
235
  }
226
236
  for (const key of this.ctx.keybindings.getKeys("app.clipboard.copyLine")) {
227
- this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
237
+ this.ctx.editor.setCustomKeyHandler(key, () => {
238
+ this.handleCopyCurrentLine();
239
+ });
228
240
  }
229
241
  for (const key of this.ctx.keybindings.getKeys("app.session.observe")) {
230
- this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionObserver());
242
+ this.ctx.editor.setCustomKeyHandler(key, () => {
243
+ this.ctx.showSessionObserver();
244
+ });
231
245
  }
232
246
  for (const key of this.ctx.keybindings.getKeys("app.jobs.open")) {
233
- this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showJobsOverlay());
247
+ this.ctx.editor.setCustomKeyHandler(key, () => {
248
+ this.ctx.showJobsOverlay();
249
+ });
250
+ }
251
+ for (const key of this.ctx.keybindings.getKeys("app.tool.backgroundFold")) {
252
+ this.ctx.editor.setCustomKeyHandler(key, () => this.handleForegroundToolBackgroundFold());
234
253
  }
235
254
 
236
255
  this.ctx.editor.onChange = (text: string) => {
@@ -311,8 +330,8 @@ export class InputController {
311
330
  // Handle skill commands (/skill:name [args]). While streaming, Enter
312
331
  // honors `busyPromptMode`: "steer" interrupts the active turn, "queue"
313
332
  // runs after it completes (matches the free-text Enter semantics applied
314
- // a few lines below at the streaming branch). Ctrl+Enter always routes
315
- // through `handleFollowUp` and dispatches the same helper with `"followUp"`.
333
+ // a few lines below at the streaming branch). Explicit queue shortcuts
334
+ // route through `handleFollowUp` and dispatch as `followUp`.
316
335
  if (await this.#invokeSkillCommand(text, this.#busyStreamingBehavior())) {
317
336
  return;
318
337
  }
@@ -481,7 +500,7 @@ export class InputController {
481
500
  * completes (in submission order). Only consulted while streaming.
482
501
  */
483
502
  #busyStreamingBehavior(): "steer" | "followUp" {
484
- return this.ctx.settings.get("busyPromptMode") === "queue" ? "followUp" : "steer";
503
+ return this.ctx.settings.get("busyPromptMode") === "steer" ? "steer" : "followUp";
485
504
  }
486
505
 
487
506
  /**
@@ -580,7 +599,7 @@ export class InputController {
580
599
 
581
600
  // Skill commands invoke through the custom-message path regardless of
582
601
  // which keybinding submitted them. Enter routes them as `steer`;
583
- // Ctrl+Enter (this handler) routes them as `followUp`.
602
+ // explicit queue shortcuts route them as `followUp`.
584
603
  if (await this.#invokeSkillCommand(text, "followUp")) {
585
604
  return;
586
605
  }
@@ -602,6 +621,11 @@ export class InputController {
602
621
  await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text));
603
622
  }
604
623
 
624
+ /** Send editor text explicitly as a queued next-turn message. */
625
+ async handleQueueSubmit(): Promise<void> {
626
+ return this.handleFollowUp();
627
+ }
628
+
605
629
  restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
606
630
  this.ctx.locallySubmittedUserSignatures.clear();
607
631
  const { steering, followUp } = this.ctx.session.clearQueue();
@@ -690,6 +714,31 @@ export class InputController {
690
714
  process.kill(0, "SIGTSTP");
691
715
  }
692
716
 
717
+ handleForegroundToolBackgroundFold(): boolean {
718
+ if (!this.ctx.session.hasForegroundBashBackgroundRequestHandler?.()) {
719
+ this.#lastBackgroundFoldKeyTime = 0;
720
+ return false;
721
+ }
722
+
723
+ const now = Date.now();
724
+ if (now - this.#lastBackgroundFoldKeyTime > 750) {
725
+ this.#lastBackgroundFoldKeyTime = now;
726
+ this.ctx.showStatus("Press Ctrl+B again to fold supported foreground bash into a background job");
727
+ return true;
728
+ }
729
+ this.#lastBackgroundFoldKeyTime = 0;
730
+
731
+ if (!this.ctx.session.requestForegroundBashBackground?.()) {
732
+ this.ctx.showWarning(
733
+ "No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
734
+ );
735
+ return true;
736
+ }
737
+
738
+ this.ctx.showStatus("Folding foreground bash into a quiet background job…");
739
+ return true;
740
+ }
741
+
693
742
  handleTextPaste(text: string): boolean | Promise<boolean> {
694
743
  const imagePath = this.#getPastedImagePathCandidate(text);
695
744
  return imagePath ? this.#attachPastedImagePath(imagePath) : false;
@@ -49,6 +49,10 @@ import {
49
49
  import { setSessionTerminalTitle } from "../../utils/title-generator";
50
50
  import { AgentDashboard } from "../components/agent-dashboard";
51
51
  import { AssistantMessageComponent } from "../components/assistant-message";
52
+ import {
53
+ CustomModelPresetWizardComponent,
54
+ type CustomModelPresetWizardSubmit,
55
+ } from "../components/custom-model-preset-wizard";
52
56
  import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "../components/custom-provider-wizard";
53
57
  import { ExtensionDashboard } from "../components/extensions";
54
58
  import { HistorySearchComponent } from "../components/history-search";
@@ -213,6 +217,36 @@ export class SelectorController {
213
217
  this.ctx.ui.requestRender();
214
218
  }
215
219
 
220
+ showCustomModelPresetWizard(): void {
221
+ this.showSelector(done => {
222
+ let wizard: CustomModelPresetWizardComponent;
223
+ const submit = async (input: CustomModelPresetWizardSubmit): Promise<void> => {
224
+ try {
225
+ const profile = await this.ctx.session.modelRegistry.saveCustomModelProfile(input.name, input.profile);
226
+ await this.ctx.session.modelRegistry.refresh("offline");
227
+ await this.ctx.notifyConfigChanged?.();
228
+ this.ctx.showStatus(`Custom model preset created: ${profile.displayName ?? profile.name}`);
229
+ done();
230
+ this.ctx.ui.requestRender();
231
+ } catch (err) {
232
+ const message = err instanceof Error ? err.message : String(err);
233
+ wizard.setSubmitError(`Preset creation failed: ${message}`);
234
+ }
235
+ };
236
+ wizard = new CustomModelPresetWizardComponent(
237
+ input => {
238
+ void submit(input);
239
+ },
240
+ () => {
241
+ done();
242
+ this.ctx.ui.requestRender();
243
+ },
244
+ () => this.ctx.ui.requestRender(),
245
+ );
246
+ return { component: wizard, focus: wizard };
247
+ });
248
+ }
249
+
216
250
  showCustomProviderWizard(): void {
217
251
  this.showSelector(done => {
218
252
  let wizard: CustomProviderWizardComponent;
@@ -618,6 +652,11 @@ export class SelectorController {
618
652
  this.ctx.session.scopedModels,
619
653
  async selection => {
620
654
  try {
655
+ if (selection.kind === "createProfile") {
656
+ done();
657
+ this.showCustomModelPresetWizard();
658
+ return;
659
+ }
621
660
  if (selection.kind === "profile") {
622
661
  await activateModelProfile(
623
662
  {
@@ -406,7 +406,7 @@ export class InteractiveMode implements InteractiveModeContext {
406
406
  this.hookWidgetContainerBelow = new Container();
407
407
  this.editorContainer = new Container();
408
408
  this.editorContainer.addChild(this.editor);
409
- this.statusLine = new StatusLineComponent(session);
409
+ this.statusLine = new StatusLineComponent(session, { version: this.#version });
410
410
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
411
411
 
412
412
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
@@ -23,6 +23,12 @@ export interface PrintModeOptions {
23
23
  initialMessage?: string;
24
24
  /** Images to attach to the initial message */
25
25
  initialImages?: ImageContent[];
26
+ /**
27
+ * When true, an assistant error/abort does not call process.exit(); print mode
28
+ * returns instead so the caller (e.g. RLM autonomous mode) can run its own
29
+ * finalization/cleanup before the process exits.
30
+ */
31
+ suppressProcessExit?: boolean;
26
32
  }
27
33
 
28
34
  /**
@@ -84,10 +90,14 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
84
90
  ) {
85
91
  const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
86
92
  const flushed = process.stderr.write(`${errorLine}\n`);
87
- if (flushed) {
88
- process.exit(1);
89
- } else {
90
- process.stderr.once("drain", () => process.exit(1));
93
+ // When the caller owns finalization (RLM autonomous), return instead of
94
+ // exiting so its cleanup runs; the caller surfaces a non-zero exit itself.
95
+ if (!options.suppressProcessExit) {
96
+ if (flushed) {
97
+ process.exit(1);
98
+ } else {
99
+ process.stderr.once("drain", () => process.exit(1));
100
+ }
91
101
  }
92
102
  }
93
103