lsd-pi 1.1.4 → 1.1.6

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 (175) hide show
  1. package/README.md +2 -1
  2. package/dist/headless-ui.js +2 -0
  3. package/dist/onboarding.js +11 -8
  4. package/dist/resources/extensions/async-jobs/async-bash-tool.js +14 -0
  5. package/dist/resources/extensions/async-jobs/await-tool.js +14 -0
  6. package/dist/resources/extensions/async-jobs/cancel-job-tool.js +7 -0
  7. package/dist/resources/extensions/cache-timer/index.js +5 -0
  8. package/dist/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  9. package/dist/resources/extensions/codex-rotate/README.md +9 -3
  10. package/dist/resources/extensions/codex-rotate/commands.js +15 -8
  11. package/dist/resources/extensions/codex-rotate/index.js +17 -8
  12. package/dist/resources/extensions/memory/auto-extract.js +196 -80
  13. package/dist/resources/extensions/memory/dream.js +86 -19
  14. package/dist/resources/extensions/shared/rtk.js +89 -87
  15. package/dist/resources/extensions/subagent/index.js +33 -7
  16. package/dist/startup-model-validation.js +12 -2
  17. package/dist/update-check.js +2 -2
  18. package/dist/update-cmd.js +3 -3
  19. package/dist/welcome-screen.js +43 -14
  20. package/package.json +3 -2
  21. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts +2 -0
  22. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts.map +1 -0
  23. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js +46 -0
  24. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js.map +1 -0
  25. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +8 -0
  26. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/core/agent-session.js +43 -4
  28. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +3 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  33. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  35. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts +48 -0
  37. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts.map +1 -0
  38. package/packages/pi-coding-agent/dist/core/pty-executor.js +173 -0
  39. package/packages/pi-coding-agent/dist/core/pty-executor.js.map +1 -0
  40. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/sdk.js +16 -3
  42. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  44. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/settings-manager.js +18 -0
  46. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/tool-approval.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/tool-approval.js +2 -2
  49. package/packages/pi-coding-agent/dist/core/tool-approval.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +7 -0
  51. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/tools/index.js +23 -2
  53. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +50 -0
  55. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/core/tools/pty.js +289 -0
  57. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +36 -22
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +3 -5
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +23 -62
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -4
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -4
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts +39 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +182 -0
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +6 -0
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +36 -0
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -4
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -2
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +106 -77
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +2 -5
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +4 -13
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +11 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +49 -13
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +2 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +27 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +251 -39
  107. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +2 -2
  109. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts +10 -0
  111. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/utils/terminal-screen.js +67 -0
  113. package/packages/pi-coding-agent/dist/utils/terminal-screen.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts +7 -0
  115. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js +67 -0
  117. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js.map +1 -0
  118. package/packages/pi-coding-agent/package.json +9 -4
  119. package/packages/pi-coding-agent/src/core/agent-session.clear-queue.test.ts +50 -0
  120. package/packages/pi-coding-agent/src/core/agent-session.ts +50 -4
  121. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  122. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  123. package/packages/pi-coding-agent/src/core/pty-executor.ts +229 -0
  124. package/packages/pi-coding-agent/src/core/sdk.ts +16 -3
  125. package/packages/pi-coding-agent/src/core/settings-manager.ts +27 -0
  126. package/packages/pi-coding-agent/src/core/tool-approval.ts +2 -2
  127. package/packages/pi-coding-agent/src/core/tools/index.ts +35 -2
  128. package/packages/pi-coding-agent/src/core/tools/pty.ts +354 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +37 -24
  130. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +22 -70
  131. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -3
  132. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -3
  133. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +224 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +45 -0
  135. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -3
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +104 -81
  137. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +5 -19
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +55 -13
  139. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  140. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +2 -0
  141. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +296 -48
  143. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +2 -2
  144. package/packages/pi-coding-agent/src/utils/terminal-screen.ts +77 -0
  145. package/packages/pi-coding-agent/src/utils/terminal-serializer.ts +72 -0
  146. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts +2 -0
  147. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts.map +1 -0
  148. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js +105 -0
  149. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js.map +1 -0
  150. package/packages/pi-tui/dist/components/editor.d.ts +4 -0
  151. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  152. package/packages/pi-tui/dist/components/editor.js +57 -3
  153. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  154. package/packages/pi-tui/dist/components/loader.d.ts +26 -6
  155. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  156. package/packages/pi-tui/dist/components/loader.js +178 -18
  157. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  158. package/packages/pi-tui/src/components/editor.ts +65 -3
  159. package/packages/pi-tui/src/components/loader.ts +196 -19
  160. package/pkg/dist/modes/interactive/theme/themes.js +2 -2
  161. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/async-jobs/async-bash-tool.ts +13 -0
  164. package/src/resources/extensions/async-jobs/await-tool.ts +13 -0
  165. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +8 -0
  166. package/src/resources/extensions/cache-timer/index.ts +102 -96
  167. package/src/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  168. package/src/resources/extensions/codex-rotate/README.md +9 -3
  169. package/src/resources/extensions/codex-rotate/commands.ts +335 -329
  170. package/src/resources/extensions/codex-rotate/index.ts +85 -75
  171. package/src/resources/extensions/memory/auto-extract.ts +330 -204
  172. package/src/resources/extensions/memory/dream.ts +88 -21
  173. package/src/resources/extensions/memory/tests/auto-extract.test.ts +200 -144
  174. package/src/resources/extensions/shared/rtk.js +112 -0
  175. package/src/resources/extensions/subagent/index.ts +35 -6
@@ -69,12 +69,14 @@ import {
69
69
  type PermissionMode,
70
70
  } from "../../core/tool-approval.js";
71
71
  import type { TruncationResult } from "../../core/tools/truncate.js";
72
+ import { isPtyAvailable } from "../../core/pty-executor.js";
72
73
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
73
74
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
74
75
  import { resetStdinForTui } from "../../utils/reset-stdin.js";
75
76
  import { ensureTool } from "../../utils/tools-manager.js";
76
77
  import { AssistantMessageComponent } from "./components/assistant-message.js";
77
78
  import { BashExecutionComponent } from "./components/bash-execution.js";
79
+ import { EmbeddedTerminalComponent } from "./components/embedded-terminal.js";
78
80
  import { BorderedLoader } from "./components/bordered-loader.js";
79
81
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
80
82
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
@@ -137,6 +139,8 @@ interface Expandable {
137
139
  setExpanded(expanded: boolean): void;
138
140
  }
139
141
 
142
+ type InteractiveBashComponent = BashExecutionComponent | EmbeddedTerminalComponent;
143
+
140
144
  function isExpandable(obj: unknown): obj is Expandable {
141
145
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
142
146
  }
@@ -192,7 +196,24 @@ export class InteractiveMode {
192
196
  private onInputCallback?: (text: string) => void;
193
197
  private loadingAnimation: Loader | undefined = undefined;
194
198
  private pendingWorkingMessage: string | undefined = undefined;
195
- private readonly defaultWorkingMessage = "Working...";
199
+ private readonly defaultWorkingMessage = "Cooking…";
200
+ private readonly workingMessages = [
201
+ "Cooking…",
202
+ "Vibing…",
203
+ "Brainworming…",
204
+ "Scheming…",
205
+ "Spelunking…",
206
+ "Manifesting…",
207
+ "Yak shaving…",
208
+ "Grokking…",
209
+ "Pondering…",
210
+ "Going deep…",
211
+ "Caffeinating…",
212
+ "On a mission…",
213
+ "Consulting the void…",
214
+ "Doing the thing…",
215
+ "Almost maybe…",
216
+ ];
196
217
 
197
218
  private lastSigintTime = 0;
198
219
  private lastEscapeTime = 0;
@@ -208,6 +229,7 @@ export class InteractiveMode {
208
229
 
209
230
  // Tool execution tracking: toolCallId -> component
210
231
  private pendingTools = new Map<string, ToolExecutionComponent>();
232
+ private agentPtyComponents = new Map<string, EmbeddedTerminalComponent>();
211
233
 
212
234
  // Tool output expansion state
213
235
  private toolOutputExpanded = false;
@@ -215,6 +237,10 @@ export class InteractiveMode {
215
237
  // Thinking block visibility state
216
238
  private hideThinkingBlock = false;
217
239
 
240
+ // Pin last prompt feature
241
+ private pinLastPromptEnabled = false;
242
+ private lastSentPromptText: string | undefined = undefined;
243
+
218
244
  // Skill commands: command name -> skill file path
219
245
  private skillCommands = new Map<string, string>();
220
246
 
@@ -225,10 +251,12 @@ export class InteractiveMode {
225
251
  private isBashMode = false;
226
252
 
227
253
  // Track current bash execution component
228
- private bashComponent: BashExecutionComponent | undefined = undefined;
254
+ private bashComponent: InteractiveBashComponent | undefined = undefined;
229
255
 
230
256
  // Track pending bash components (shown in pending area, moved to chat on submit)
231
- private pendingBashComponents: BashExecutionComponent[] = [];
257
+ private pendingBashComponents: InteractiveBashComponent[] = [];
258
+ private focusedEmbeddedTerminal?: EmbeddedTerminalComponent;
259
+ private lastEmbeddedTerminalSize?: { cols: number; rows: number };
232
260
 
233
261
  // Auto-compaction state
234
262
  private autoCompactionLoader: Loader | undefined = undefined;
@@ -312,6 +340,9 @@ export class InteractiveMode {
312
340
  // Load hide thinking block setting
313
341
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
314
342
 
343
+ // Load pin last prompt setting
344
+ this.pinLastPromptEnabled = this.settingsManager.getPinLastPrompt();
345
+
315
346
  // Register themes from resource loader and initialize
316
347
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
317
348
  initTheme(this.settingsManager.getTheme(), true, this.settingsManager.getThemeAccent());
@@ -618,6 +649,7 @@ export class InteractiveMode {
618
649
  // Main interactive loop
619
650
  while (true) {
620
651
  const userInput = await this.getUserInput();
652
+ this.recordLastSentPrompt(userInput);
621
653
  try {
622
654
  await this.session.prompt(userInput);
623
655
  } catch (error: unknown) {
@@ -1403,9 +1435,7 @@ export class InteractiveMode {
1403
1435
  this.defaultEditor.onExtensionShortcut = undefined;
1404
1436
  this.updateTerminalTitle();
1405
1437
  if (this.loadingAnimation) {
1406
- this.loadingAnimation.setMessage(
1407
- `${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`,
1408
- );
1438
+ this.loadingAnimation.setCycleMessages(this.workingMessages);
1409
1439
  }
1410
1440
  }
1411
1441
 
@@ -1415,6 +1445,44 @@ export class InteractiveMode {
1415
1445
  /**
1416
1446
  * Render all extension widgets to the widget container.
1417
1447
  */
1448
+ /**
1449
+ * Record the last user-sent prompt text and refresh the widget.
1450
+ * Called from the main loop and input-controller paths.
1451
+ */
1452
+ recordLastSentPrompt(text: string): void {
1453
+ // Ignore slash commands and bash commands — they're not "topic" prompts
1454
+ const trimmed = text.trim();
1455
+ if (trimmed.startsWith("/") || trimmed.startsWith("!")) return;
1456
+ this.lastSentPromptText = trimmed;
1457
+ this.updatePinLastPromptWidget();
1458
+ }
1459
+
1460
+ /**
1461
+ * Update (or remove) the "pin last prompt" widget above the editor.
1462
+ * Uses the built-in extension widget slot with key "__pin-last-prompt__".
1463
+ */
1464
+ private updatePinLastPromptWidget(): void {
1465
+ const key = "__pin-last-prompt__";
1466
+ if (!this.pinLastPromptEnabled || !this.lastSentPromptText) {
1467
+ // Remove widget
1468
+ const existing = this.extensionWidgetsAbove.get(key);
1469
+ if (existing?.dispose) existing.dispose();
1470
+ this.extensionWidgetsAbove.delete(key);
1471
+ } else {
1472
+ // Truncate to one line (max 200 chars) to keep it compact
1473
+ const raw = this.lastSentPromptText.replace(/[\r\n]+/g, " ").trim();
1474
+ const label = raw.length > 200 ? raw.slice(0, 197) + "…" : raw;
1475
+ const container = new Container();
1476
+ container.addChild(new Text(
1477
+ theme.fg("dim", ` last prompt: ${label}`),
1478
+ 0,
1479
+ 0,
1480
+ ));
1481
+ this.extensionWidgetsAbove.set(key, container);
1482
+ }
1483
+ this.renderWidgets();
1484
+ }
1485
+
1418
1486
  private renderWidgets(): void {
1419
1487
  if (!this.widgetContainerAbove || !this.widgetContainerBelow) return;
1420
1488
  this.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);
@@ -1619,6 +1687,13 @@ export class InteractiveMode {
1619
1687
  return result === "Yes";
1620
1688
  }
1621
1689
 
1690
+ /** Reset the loading animation back to cycling working messages. */
1691
+ private resetLoadingMessage(): void {
1692
+ if (this.loadingAnimation) {
1693
+ this.loadingAnimation.setCycleMessages(this.workingMessages);
1694
+ }
1695
+ }
1696
+
1622
1697
  private _registerApprovalHandlers(): void {
1623
1698
  const classifierService = new ClassifierService(this.session.modelRegistry.authStorage);
1624
1699
 
@@ -1629,9 +1704,7 @@ export class InteractiveMode {
1629
1704
  const title = `Allow ${actionLabel}: ${request.path}\n${request.message}`;
1630
1705
  const approved = await this._showApprovalConfirm(title);
1631
1706
 
1632
- if (this.loadingAnimation) {
1633
- this.loadingAnimation.setMessage(this.defaultWorkingMessage);
1634
- }
1707
+ this.resetLoadingMessage();
1635
1708
  return approved;
1636
1709
  });
1637
1710
 
@@ -1671,9 +1744,7 @@ export class InteractiveMode {
1671
1744
  });
1672
1745
 
1673
1746
  if (decision.approved) {
1674
- if (this.loadingAnimation) {
1675
- this.loadingAnimation.setMessage(this.defaultWorkingMessage);
1676
- }
1747
+ this.resetLoadingMessage();
1677
1748
  return true;
1678
1749
  }
1679
1750
 
@@ -1681,9 +1752,7 @@ export class InteractiveMode {
1681
1752
  const title = `Classifier ${decision.source === "rule" ? "blocked" : "did not approve"} ${request.toolName}\n${decision.reason}\n${argsPreview}`;
1682
1753
  const approved = await this._showApprovalConfirm(title);
1683
1754
 
1684
- if (this.loadingAnimation) {
1685
- this.loadingAnimation.setMessage(this.defaultWorkingMessage);
1686
- }
1755
+ this.resetLoadingMessage();
1687
1756
  return approved;
1688
1757
  });
1689
1758
 
@@ -1693,9 +1762,7 @@ export class InteractiveMode {
1693
1762
  `${request.message}\n${request.command}`,
1694
1763
  ["Allow once", "Allow for session", "Deny"],
1695
1764
  );
1696
- if (this.loadingAnimation) {
1697
- this.loadingAnimation.setMessage(this.defaultWorkingMessage);
1698
- }
1765
+ this.resetLoadingMessage();
1699
1766
  if (result === "Allow once") return "allow-once";
1700
1767
  if (result === "Allow for session") return "allow-session";
1701
1768
  return "deny";
@@ -2027,6 +2094,7 @@ export class InteractiveMode {
2027
2094
  this.defaultEditor.onAction("cycleModelBackward", () => this.cycleModel("backward"));
2028
2095
  this.defaultEditor.onAction("cyclePermissionMode", () => this.cyclePermissionMode());
2029
2096
  this.defaultEditor.onAction("showHotkeys", () => showHotkeys(this.getSlashCommandContext()));
2097
+ this.defaultEditor.onAction("terminalFocus", () => this.toggleEmbeddedTerminalFocus());
2030
2098
 
2031
2099
  // Global debug handler on TUI (works regardless of focus)
2032
2100
  this.ui.onDebug = () => this.handleDebugCommand();
@@ -2382,6 +2450,7 @@ export class InteractiveMode {
2382
2450
  this.hideThinkingBlock,
2383
2451
  this.getMarkdownThemeWithSettings(),
2384
2452
  this.settingsManager.getTimestampFormat(),
2453
+ this.session.thinkingLevel || "off",
2385
2454
  );
2386
2455
  this.chatContainer.addChild(assistantComponent);
2387
2456
  break;
@@ -2727,9 +2796,42 @@ export class InteractiveMode {
2727
2796
  for (const component of this.pendingBashComponents) {
2728
2797
  component.setExpanded(expanded);
2729
2798
  }
2799
+ this.updateEditorExpandHint();
2730
2800
  this.ui.requestRender();
2731
2801
  }
2732
2802
 
2803
+ /** Append/remove the "ctrl+o to expand" hint in the editor bottom border. */
2804
+ updateEditorExpandHint(): void {
2805
+ const expandKey = appKey(this.keybindings, "expandTools");
2806
+ const collapseHint = `${theme.fg("dim", expandKey)}${theme.fg("muted", " collapse")}`;
2807
+ const expandHint = `${theme.fg("dim", expandKey)}${theme.fg("muted", " : verbose")}`;
2808
+ // The base hint set during agent_start
2809
+ const enterKey = theme.fg("dim", "↵");
2810
+ const followUpKey = theme.fg("dim", appKey(this.keybindings, "followUp"));
2811
+ const steerLabel = theme.fg("muted", " steer");
2812
+ const queueLabel = theme.fg("muted", " queue");
2813
+ const baseHint = `${enterKey}${steerLabel} ${followUpKey}${queueLabel}`;
2814
+
2815
+ // Check if there are any expandable tool outputs in the chat
2816
+ const hasToolOutputs =
2817
+ this.chatContainer.children.some(isExpandable) ||
2818
+ !!this.bashComponent ||
2819
+ this.pendingBashComponents.length > 0;
2820
+
2821
+ const activeHint = this.toolOutputExpanded ? collapseHint : expandHint;
2822
+
2823
+ if (this.loadingAnimation) {
2824
+ // Agent is running — always show expand/collapse hint when there are tool outputs
2825
+ this.defaultEditor.bottomHint = hasToolOutputs
2826
+ ? `${baseHint} ${activeHint}`
2827
+ : baseHint;
2828
+ } else if (hasToolOutputs) {
2829
+ // Idle — show expand/collapse hint so user knows ctrl+o works
2830
+ this.defaultEditor.bottomHint = activeHint;
2831
+ }
2832
+ // If no tool outputs and idle, leave bottomHint as-is (cleared by agent_end)
2833
+ }
2834
+
2733
2835
  private toggleThinkingBlockVisibility(): void {
2734
2836
  this.hideThinkingBlock = !this.hideThinkingBlock;
2735
2837
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
@@ -3063,6 +3165,7 @@ export class InteractiveMode {
3063
3165
  enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
3064
3166
  codexRotate: this.settingsManager.getCodexRotate(),
3065
3167
  cacheTimer: this.settingsManager.getCacheTimer(),
3168
+ pinLastPrompt: this.settingsManager.getPinLastPrompt(),
3066
3169
  steeringMode: this.session.steeringMode,
3067
3170
  followUpMode: this.session.followUpMode,
3068
3171
  transport: this.settingsManager.getTransport(),
@@ -3086,6 +3189,8 @@ export class InteractiveMode {
3086
3189
  toolOutputMode: this.settingsManager.getToolOutputMode(),
3087
3190
  rtk: this.settingsManager.getRtk(),
3088
3191
  editorScheme: this.settingsManager.getEditorScheme(),
3192
+ autoDream: this.settingsManager.getAutoDream(),
3193
+ autoMemory: this.settingsManager.getAutoMemory(),
3089
3194
  sandboxEnabled: this.settingsManager.getSandboxSettings().enabled ?? true,
3090
3195
  sandboxNetworkMode: this.settingsManager.getSandboxSettings().networkMode
3091
3196
  ?? (this.settingsManager.getSandboxSettings().networkEnabled === true ? "allow" : this.settingsManager.getSandboxSettings().networkEnabled === false ? "deny" : "ask"),
@@ -3171,10 +3276,24 @@ export class InteractiveMode {
3171
3276
  this.settingsManager.setCacheTimer(enabled);
3172
3277
  this.showStatus(`Cache timer: ${enabled ? "enabled" : "disabled"}`);
3173
3278
  },
3279
+ onPinLastPromptChange: (enabled) => {
3280
+ this.settingsManager.setPinLastPrompt(enabled);
3281
+ this.pinLastPromptEnabled = enabled;
3282
+ this.updatePinLastPromptWidget();
3283
+ this.showStatus(`Pin last prompt: ${enabled ? "enabled" : "disabled"}`);
3284
+ },
3174
3285
  onRtkChange: (enabled) => {
3175
3286
  this.settingsManager.setRtk(enabled);
3176
3287
  this.showStatus(`RTK: ${enabled ? "enabled" : "disabled"} (restart required)`);
3177
3288
  },
3289
+ onAutoDreamChange: (enabled) => {
3290
+ this.settingsManager.setAutoDream(enabled);
3291
+ this.showStatus(`Auto dream: ${enabled ? "enabled" : "disabled"}`);
3292
+ },
3293
+ onAutoMemoryChange: (enabled) => {
3294
+ this.settingsManager.setAutoMemory(enabled);
3295
+ this.showStatus(`Auto memory: ${enabled ? "enabled" : "disabled"}`);
3296
+ },
3178
3297
  onSteeringModeChange: (mode) => {
3179
3298
  this.session.setSteeringMode(mode);
3180
3299
  },
@@ -3579,7 +3698,7 @@ export class InteractiveMode {
3579
3698
  this.chatContainer.addChild(new Spacer(1));
3580
3699
  summaryLoader = new Loader(
3581
3700
  this.ui,
3582
- (spinner) => theme.fg("accent", spinner),
3701
+ (spinner) => theme.fg("text", spinner),
3583
3702
  (text) => theme.fg("muted", text),
3584
3703
  `Summarizing branch... (${appKey(this.keybindings, "interrupt")} to cancel)`,
3585
3704
  );
@@ -4042,6 +4161,78 @@ export class InteractiveMode {
4042
4161
  }
4043
4162
  }
4044
4163
 
4164
+ private getAgentPtyComponent(sessionId: string): EmbeddedTerminalComponent | undefined {
4165
+ return this.agentPtyComponents.get(sessionId);
4166
+ }
4167
+
4168
+ private ensureAgentPtyComponent(sessionId: string, command?: string): EmbeddedTerminalComponent {
4169
+ let component = this.agentPtyComponents.get(sessionId);
4170
+ if (component) return component;
4171
+
4172
+ component = new EmbeddedTerminalComponent(
4173
+ command || `PTY ${sessionId}`,
4174
+ this.ui,
4175
+ this.settingsManager.getToolOutputMode(),
4176
+ appKey(this.keybindings, "terminalFocus"),
4177
+ false,
4178
+ );
4179
+ component.setStatusOverride(theme.fg("muted", "Agent-controlled terminal session"));
4180
+ component.setExpanded(true);
4181
+ this.chatContainer.addChild(component);
4182
+ this.agentPtyComponents.set(sessionId, component);
4183
+ return component;
4184
+ }
4185
+
4186
+ private updateAgentPtyComponent(
4187
+ sessionId: string,
4188
+ options?: { command?: string; screenText?: string; completed?: boolean; cancelled?: boolean; exitCode?: number },
4189
+ ): void {
4190
+ const component = this.ensureAgentPtyComponent(sessionId, options?.command);
4191
+ if (options?.screenText !== undefined) {
4192
+ component.setScreenText(options.screenText);
4193
+ }
4194
+ if (options?.completed) {
4195
+ component.setStatusOverride(undefined);
4196
+ component.setComplete(options.exitCode, options.cancelled ?? false);
4197
+ } else {
4198
+ component.setStatusOverride(theme.fg("muted", "Agent-controlled terminal session"));
4199
+ }
4200
+ }
4201
+
4202
+ private clearAgentPtyComponents(): void {
4203
+ for (const [, component] of this.agentPtyComponents) {
4204
+ this.chatContainer.removeChild(component);
4205
+ }
4206
+ this.agentPtyComponents.clear();
4207
+ }
4208
+
4209
+ private focusEmbeddedTerminal(component: EmbeddedTerminalComponent): void {
4210
+ this.focusedEmbeddedTerminal = component;
4211
+ this.lastEmbeddedTerminalSize = undefined;
4212
+ this.ui.setFocus(component);
4213
+ component.invalidate();
4214
+ this.ui.requestRender();
4215
+ }
4216
+
4217
+ private releaseEmbeddedTerminalFocus(): void {
4218
+ const focused = this.focusedEmbeddedTerminal;
4219
+ this.focusedEmbeddedTerminal = undefined;
4220
+ this.lastEmbeddedTerminalSize = undefined;
4221
+ this.ui.setFocus(this.editor);
4222
+ focused?.invalidate();
4223
+ this.ui.requestRender();
4224
+ }
4225
+
4226
+ private toggleEmbeddedTerminalFocus(): void {
4227
+ if (this.focusedEmbeddedTerminal) {
4228
+ this.releaseEmbeddedTerminalFocus();
4229
+ return;
4230
+ }
4231
+ if (this.bashComponent instanceof EmbeddedTerminalComponent) {
4232
+ this.focusEmbeddedTerminal(this.bashComponent);
4233
+ }
4234
+ }
4235
+
4045
4236
  private async handleBashCommand(command: string, excludeFromContext = false, displayCommand?: string, loginShell?: boolean): Promise<void> {
4046
4237
  const extensionRunner = this.session.extensionRunner;
4047
4238
  const label = displayCommand || command;
@@ -4061,7 +4252,7 @@ export class InteractiveMode {
4061
4252
  const result = eventResult.result;
4062
4253
 
4063
4254
  // Create UI component for display
4064
- this.bashComponent = new BashExecutionComponent(
4255
+ const component = new BashExecutionComponent(
4065
4256
  label,
4066
4257
  this.ui,
4067
4258
  excludeFromContext,
@@ -4069,19 +4260,20 @@ export class InteractiveMode {
4069
4260
  isRtkEnabled(),
4070
4261
  result.sandboxed ?? false,
4071
4262
  );
4072
- this.bashComponent.setExpanded(this.toolOutputExpanded);
4263
+ this.bashComponent = component;
4264
+ component.setExpanded(this.toolOutputExpanded);
4073
4265
  if (this.session.isStreaming) {
4074
- this.pendingMessagesContainer.addChild(this.bashComponent);
4075
- this.pendingBashComponents.push(this.bashComponent);
4266
+ this.pendingMessagesContainer.addChild(component);
4267
+ this.pendingBashComponents.push(component);
4076
4268
  } else {
4077
- this.chatContainer.addChild(this.bashComponent);
4269
+ this.chatContainer.addChild(component);
4078
4270
  }
4079
4271
 
4080
4272
  // Show output and complete
4081
4273
  if (result.output) {
4082
- this.bashComponent.appendOutput(result.output);
4274
+ component.appendOutput(result.output);
4083
4275
  }
4084
- this.bashComponent.setComplete(
4276
+ component.setComplete(
4085
4277
  result.exitCode,
4086
4278
  result.cancelled,
4087
4279
  result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
@@ -4098,7 +4290,60 @@ export class InteractiveMode {
4098
4290
 
4099
4291
  // Normal execution path (possibly with custom operations)
4100
4292
  const isDeferred = this.session.isStreaming;
4101
- this.bashComponent = new BashExecutionComponent(
4293
+ const canUsePty = !eventResult?.operations && await isPtyAvailable();
4294
+
4295
+ if (canUsePty) {
4296
+ const component = new EmbeddedTerminalComponent(
4297
+ label,
4298
+ this.ui,
4299
+ this.settingsManager.getToolOutputMode(),
4300
+ appKey(this.keybindings, "terminalFocus"),
4301
+ excludeFromContext,
4302
+ );
4303
+ this.bashComponent = component;
4304
+ component.setExpanded(this.toolOutputExpanded);
4305
+
4306
+ if (isDeferred) {
4307
+ this.pendingMessagesContainer.addChild(component);
4308
+ this.pendingBashComponents.push(component);
4309
+ } else {
4310
+ this.chatContainer.addChild(component);
4311
+ }
4312
+ this.ui.requestRender();
4313
+
4314
+ try {
4315
+ const session = await this.session.executeBashInteractive(command, {
4316
+ onChunk: (chunk) => {
4317
+ component.appendOutput(chunk);
4318
+ this.ui.requestRender();
4319
+ },
4320
+ cols: Math.max(40, this.ui.terminal.columns - 2),
4321
+ rows: Math.max(10, this.ui.terminal.rows - 8),
4322
+ loginShell,
4323
+ });
4324
+ component.setHandle(session.handle, () => this.releaseEmbeddedTerminalFocus());
4325
+ if (!isDeferred) {
4326
+ this.focusEmbeddedTerminal(component);
4327
+ }
4328
+
4329
+ const result = await session.result;
4330
+ component.setComplete(result.exitCode, result.cancelled);
4331
+ this.session.recordBashResult(command, result, { excludeFromContext });
4332
+ } catch (error) {
4333
+ component.setComplete(undefined, false);
4334
+ this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
4335
+ } finally {
4336
+ if (this.focusedEmbeddedTerminal === component) {
4337
+ this.releaseEmbeddedTerminalFocus();
4338
+ }
4339
+ this.session.clearBashAbortController();
4340
+ this.bashComponent = undefined;
4341
+ this.ui.requestRender();
4342
+ }
4343
+ return;
4344
+ }
4345
+
4346
+ const fallbackComponent = new BashExecutionComponent(
4102
4347
  label,
4103
4348
  this.ui,
4104
4349
  excludeFromContext,
@@ -4106,15 +4351,16 @@ export class InteractiveMode {
4106
4351
  isRtkEnabled(),
4107
4352
  false,
4108
4353
  );
4109
- this.bashComponent.setExpanded(this.toolOutputExpanded);
4354
+ this.bashComponent = fallbackComponent;
4355
+ fallbackComponent.setExpanded(this.toolOutputExpanded);
4110
4356
 
4111
4357
  if (isDeferred) {
4112
4358
  // Show in pending area when agent is streaming
4113
- this.pendingMessagesContainer.addChild(this.bashComponent);
4114
- this.pendingBashComponents.push(this.bashComponent);
4359
+ this.pendingMessagesContainer.addChild(fallbackComponent);
4360
+ this.pendingBashComponents.push(fallbackComponent);
4115
4361
  } else {
4116
4362
  // Show in chat immediately when agent is idle
4117
- this.chatContainer.addChild(this.bashComponent);
4363
+ this.chatContainer.addChild(fallbackComponent);
4118
4364
  }
4119
4365
  this.ui.requestRender();
4120
4366
 
@@ -4122,27 +4368,21 @@ export class InteractiveMode {
4122
4368
  const result = await this.session.executeBash(
4123
4369
  command,
4124
4370
  (chunk) => {
4125
- if (this.bashComponent) {
4126
- this.bashComponent.appendOutput(chunk);
4127
- this.ui.requestRender();
4128
- }
4371
+ fallbackComponent.appendOutput(chunk);
4372
+ this.ui.requestRender();
4129
4373
  },
4130
4374
  { excludeFromContext, operations: eventResult?.operations, loginShell },
4131
4375
  );
4132
4376
 
4133
- if (this.bashComponent) {
4134
- this.bashComponent.setComplete(
4135
- result.exitCode,
4136
- result.cancelled,
4137
- result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
4138
- result.fullOutputPath,
4139
- result.sandboxed,
4140
- );
4141
- }
4377
+ fallbackComponent.setComplete(
4378
+ result.exitCode,
4379
+ result.cancelled,
4380
+ result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
4381
+ result.fullOutputPath,
4382
+ result.sandboxed,
4383
+ );
4142
4384
  } catch (error) {
4143
- if (this.bashComponent) {
4144
- this.bashComponent.setComplete(undefined, false);
4145
- }
4385
+ fallbackComponent.setComplete(undefined, false);
4146
4386
  this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
4147
4387
  }
4148
4388
 
@@ -4170,7 +4410,7 @@ export class InteractiveMode {
4170
4410
  const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
4171
4411
  const compactingLoader = new Loader(
4172
4412
  this.ui,
4173
- (spinner) => theme.fg("accent", spinner),
4413
+ (spinner) => theme.fg("text", spinner),
4174
4414
  (text) => theme.fg("muted", text),
4175
4415
  label,
4176
4416
  );
@@ -4208,6 +4448,14 @@ export class InteractiveMode {
4208
4448
 
4209
4449
  requestRender(force = false): void {
4210
4450
  if (!this.isInitialized) return;
4451
+ if (this.focusedEmbeddedTerminal) {
4452
+ const cols = Math.max(40, this.ui.terminal.columns - 2);
4453
+ const rows = Math.max(10, this.ui.terminal.rows - 8);
4454
+ if (!this.lastEmbeddedTerminalSize || this.lastEmbeddedTerminalSize.cols !== cols || this.lastEmbeddedTerminalSize.rows !== rows) {
4455
+ this.focusedEmbeddedTerminal.resize(cols, rows);
4456
+ this.lastEmbeddedTerminalSize = { cols, rows };
4457
+ }
4458
+ }
4211
4459
  this.ui.requestRender(force);
4212
4460
  }
4213
4461
 
@@ -99,7 +99,7 @@ const dark: ThemeJson = {
99
99
  thinkingHigh: "blueXhigh",
100
100
  thinkingXhigh: "cyan",
101
101
 
102
- bashMode: "green",
102
+ bashMode: "accent",
103
103
  },
104
104
  export: {
105
105
  pageBg: "#18181e",
@@ -195,7 +195,7 @@ const light: ThemeJson = {
195
195
  thinkingHigh: "blueHigh",
196
196
  thinkingXhigh: "blueXhigh",
197
197
 
198
- bashMode: "green",
198
+ bashMode: "accent",
199
199
  },
200
200
  export: {
201
201
  pageBg: "#f8f8f8",
@@ -0,0 +1,77 @@
1
+ import xtermPkg from "@xterm/headless";
2
+
3
+ const { Terminal } = xtermPkg;
4
+
5
+ export type HeadlessTerminal = InstanceType<typeof Terminal>;
6
+
7
+ export function createHeadlessTerminal(cols = 80, rows = 24, scrollback = 5000): HeadlessTerminal {
8
+ return new Terminal({
9
+ cols: Math.max(20, cols),
10
+ rows: Math.max(5, rows),
11
+ scrollback,
12
+ allowProposedApi: true,
13
+ });
14
+ }
15
+
16
+ function findLastContentLine(terminal: HeadlessTerminal, startLine = 0): number {
17
+ const buffer = terminal.buffer.active;
18
+ for (let i = buffer.length - 1; i >= startLine; i--) {
19
+ const line = buffer.getLine(i);
20
+ if (line && line.translateToString(true).length > 0) {
21
+ return i;
22
+ }
23
+ }
24
+ return -1;
25
+ }
26
+
27
+ function collectTerminalLines(terminal: HeadlessTerminal, startLine: number, endLineInclusive: number): string[] {
28
+ const buffer = terminal.buffer.active;
29
+ if (endLineInclusive < startLine) return [];
30
+
31
+ const lines: string[] = [];
32
+ for (let i = startLine; i <= endLineInclusive; i++) {
33
+ const line = buffer.getLine(i);
34
+ if (!line) {
35
+ lines.push("");
36
+ continue;
37
+ }
38
+
39
+ let trimRight = true;
40
+ if (i + 1 <= endLineInclusive) {
41
+ const nextLine = buffer.getLine(i + 1);
42
+ if (nextLine?.isWrapped) {
43
+ trimRight = false;
44
+ }
45
+ }
46
+
47
+ const lineContent = line.translateToString(trimRight);
48
+ if (line.isWrapped && lines.length > 0) {
49
+ lines[lines.length - 1] += lineContent;
50
+ } else {
51
+ lines.push(lineContent);
52
+ }
53
+ }
54
+
55
+ return lines;
56
+ }
57
+
58
+ export function snapshotTerminalLines(terminal: HeadlessTerminal, startLine = 0): string[] {
59
+ const lastContentLine = findLastContentLine(terminal, startLine);
60
+ if (lastContentLine < startLine) return [];
61
+ return collectTerminalLines(terminal, startLine, lastContentLine);
62
+ }
63
+
64
+ export function snapshotTerminalViewport(terminal: HeadlessTerminal): string[] {
65
+ const buffer = terminal.buffer.active;
66
+ const start = buffer.viewportY;
67
+ const end = Math.max(start, start + terminal.rows - 1);
68
+ return collectTerminalLines(terminal, start, end);
69
+ }
70
+
71
+ export function snapshotTerminalViewportText(terminal: HeadlessTerminal): string {
72
+ return snapshotTerminalViewport(terminal).join("\n").replace(/[ \t]+$/gmu, "");
73
+ }
74
+
75
+ export function snapshotTerminalBufferText(terminal: HeadlessTerminal): string {
76
+ return snapshotTerminalLines(terminal).join("\n");
77
+ }