@llmist/cli 15.5.0 → 15.7.0

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.
package/dist/cli.js CHANGED
@@ -98,7 +98,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commande
98
98
  // package.json
99
99
  var package_default = {
100
100
  name: "@llmist/cli",
101
- version: "15.5.0",
101
+ version: "15.7.0",
102
102
  description: "CLI for llmist - run LLM agents from the command line",
103
103
  type: "module",
104
104
  main: "dist/cli.js",
@@ -154,7 +154,7 @@ var package_default = {
154
154
  node: ">=22.0.0"
155
155
  },
156
156
  dependencies: {
157
- llmist: "^15.5.0",
157
+ llmist: "^15.7.0",
158
158
  "@unblessed/node": "^1.0.0-alpha.23",
159
159
  chalk: "^5.6.2",
160
160
  commander: "^12.1.0",
@@ -168,7 +168,7 @@ var package_default = {
168
168
  zod: "^4.1.12"
169
169
  },
170
170
  devDependencies: {
171
- "@llmist/testing": "^15.5.0",
171
+ "@llmist/testing": "^15.7.0",
172
172
  "@types/diff": "^8.0.0",
173
173
  "@types/js-yaml": "^4.0.9",
174
174
  "@types/marked-terminal": "^6.1.1",
@@ -4094,6 +4094,7 @@ function configToAgentOptions(config) {
4094
4094
  if (r["max-timeout"] !== void 0) result.retryMaxTimeout = r["max-timeout"];
4095
4095
  if (r.enabled === false) result.noRetry = true;
4096
4096
  }
4097
+ if (config["show-hints"] !== void 0) result.showHints = config["show-hints"];
4097
4098
  return result;
4098
4099
  }
4099
4100
 
@@ -4671,6 +4672,10 @@ var BlockRenderer = class _BlockRenderer {
4671
4672
  currentSessionId = 0;
4672
4673
  /** Previous session ID (for deferred cleanup) */
4673
4674
  previousSessionId = null;
4675
+ /** Callback for content state changes (empty to non-empty or vice versa) */
4676
+ onHasContentChangeCallback = null;
4677
+ /** Last reported hasContent state (to avoid duplicate callbacks) */
4678
+ lastHasContentState = false;
4674
4679
  constructor(container, renderCallback, renderNowCallback) {
4675
4680
  this.container = container;
4676
4681
  this.renderCallback = renderCallback;
@@ -4971,6 +4976,16 @@ var BlockRenderer = class _BlockRenderer {
4971
4976
  child.detach();
4972
4977
  }
4973
4978
  this.renderCallback();
4979
+ this.notifyHasContentChange();
4980
+ }
4981
+ /**
4982
+ * Set callback for content state changes.
4983
+ * Called when blocks transition from empty to non-empty or vice versa.
4984
+ * Used by HintsBar to know when "^B browse" hint should be shown.
4985
+ */
4986
+ onHasContentChange(callback) {
4987
+ this.onHasContentChangeCallback = callback;
4988
+ callback(this.blocks.size > 0);
4974
4989
  }
4975
4990
  /**
4976
4991
  * Start a new session. Called at the start of each REPL turn.
@@ -5010,6 +5025,7 @@ var BlockRenderer = class _BlockRenderer {
5010
5025
  }
5011
5026
  this.previousSessionId = null;
5012
5027
  this.renderCallback();
5028
+ this.notifyHasContentChange();
5013
5029
  }
5014
5030
  /**
5015
5031
  * Get the current session ID (for node creation).
@@ -5098,6 +5114,17 @@ var BlockRenderer = class _BlockRenderer {
5098
5114
  // ───────────────────────────────────────────────────────────────────────────
5099
5115
  // Private - Node & Block Management
5100
5116
  // ───────────────────────────────────────────────────────────────────────────
5117
+ /**
5118
+ * Notify callback if content state has changed.
5119
+ * Only fires when transitioning from empty to non-empty or vice versa.
5120
+ */
5121
+ notifyHasContentChange() {
5122
+ const hasContent = this.blocks.size > 0;
5123
+ if (hasContent !== this.lastHasContentState) {
5124
+ this.lastHasContentState = hasContent;
5125
+ this.onHasContentChangeCallback?.(hasContent);
5126
+ }
5127
+ }
5101
5128
  generateId(prefix) {
5102
5129
  return `${prefix}_${++this.nodeIdCounter}`;
5103
5130
  }
@@ -5124,6 +5151,7 @@ var BlockRenderer = class _BlockRenderer {
5124
5151
  }
5125
5152
  this.applyBottomAlignmentAndScroll();
5126
5153
  this.renderCallback();
5154
+ this.notifyHasContentChange();
5127
5155
  }
5128
5156
  /**
5129
5157
  * Render a node and its children recursively.
@@ -5159,16 +5187,16 @@ var BlockRenderer = class _BlockRenderer {
5159
5187
  }
5160
5188
  /**
5161
5189
  * Check if a gadget should render as plain text in focused mode.
5162
- * TellUser and AskUser render as text for a chat-like experience.
5190
+ * TellUser, AskUser, and Finish render as text for a chat-like experience.
5163
5191
  */
5164
5192
  shouldRenderAsText(node) {
5165
5193
  if (this.contentFilterMode !== "focused") return false;
5166
5194
  if (node.type !== "gadget") return false;
5167
5195
  const name = node.name;
5168
- return name === "TellUser" || name === "AskUser";
5196
+ return name === "TellUser" || name === "AskUser" || name === "Finish";
5169
5197
  }
5170
5198
  /**
5171
- * Create a text-like block for TellUser/AskUser gadgets in focused mode.
5199
+ * Create a text-like block for TellUser/AskUser/Finish gadgets in focused mode.
5172
5200
  * Renders just the content without the gadget header.
5173
5201
  */
5174
5202
  createTextLikeBlock(node, top) {
@@ -5185,6 +5213,13 @@ ${renderMarkdown(message)}
5185
5213
  if (typeof question === "string") {
5186
5214
  content = `
5187
5215
  ? ${question}
5216
+ `;
5217
+ }
5218
+ } else if (node.name === "Finish") {
5219
+ const message = node.parameters?.message;
5220
+ if (typeof message === "string" && message.trim()) {
5221
+ content = `
5222
+ \x1B[32m\u2713\x1B[0m ${renderMarkdown(message)}
5188
5223
  `;
5189
5224
  }
5190
5225
  }
@@ -5269,8 +5304,8 @@ ${fullContent}
5269
5304
  case "system_message": {
5270
5305
  const icon = this.getSystemMessageIcon(node.category);
5271
5306
  const color = this.getSystemMessageColor(node.category);
5272
- const RESET2 = "\x1B[0m";
5273
- return `${indent}${color}${icon} ${node.message}${RESET2}`;
5307
+ const RESET3 = "\x1B[0m";
5308
+ return `${indent}${color}${icon} ${node.message}${RESET3}`;
5274
5309
  }
5275
5310
  }
5276
5311
  }
@@ -5297,7 +5332,7 @@ ${fullContent}
5297
5332
  getSystemMessageColor(category) {
5298
5333
  const YELLOW2 = "\x1B[33m";
5299
5334
  const BLUE = "\x1B[34m";
5300
- const GRAY = "\x1B[90m";
5335
+ const GRAY2 = "\x1B[90m";
5301
5336
  const RED2 = "\x1B[31m";
5302
5337
  switch (category) {
5303
5338
  case "throttle":
@@ -5305,7 +5340,7 @@ ${fullContent}
5305
5340
  case "retry":
5306
5341
  return BLUE;
5307
5342
  case "info":
5308
- return GRAY;
5343
+ return GRAY2;
5309
5344
  case "warning":
5310
5345
  return YELLOW2;
5311
5346
  case "error":
@@ -5579,6 +5614,7 @@ ${indicator}`;
5579
5614
  }
5580
5615
  this.applyBottomAlignmentAndScroll();
5581
5616
  this.renderNowCallback();
5617
+ this.notifyHasContentChange();
5582
5618
  }
5583
5619
  /**
5584
5620
  * Get the current content filter mode.
@@ -5606,7 +5642,7 @@ ${indicator}`;
5606
5642
  return false;
5607
5643
  case "gadget": {
5608
5644
  const name = node.name;
5609
- return name === "TellUser" || name === "AskUser";
5645
+ return name === "TellUser" || name === "AskUser" || name === "Finish";
5610
5646
  }
5611
5647
  default:
5612
5648
  return false;
@@ -6036,9 +6072,129 @@ var TUIController = class {
6036
6072
  }
6037
6073
  };
6038
6074
 
6075
+ // src/tui/hints-bar.ts
6076
+ var GRAY = "\x1B[90m";
6077
+ var RESET = "\x1B[0m";
6078
+ var HintsBar = class {
6079
+ hintsBox;
6080
+ renderCallback;
6081
+ focusMode = "browse";
6082
+ contentFilterMode = "full";
6083
+ hasContent = false;
6084
+ constructor(hintsBox, renderCallback) {
6085
+ this.hintsBox = hintsBox;
6086
+ this.renderCallback = renderCallback;
6087
+ this.render();
6088
+ }
6089
+ /**
6090
+ * Update focus mode and re-render hints.
6091
+ */
6092
+ setFocusMode(mode) {
6093
+ if (this.focusMode !== mode) {
6094
+ this.focusMode = mode;
6095
+ this.render();
6096
+ }
6097
+ }
6098
+ /**
6099
+ * Update content filter mode and re-render hints.
6100
+ */
6101
+ setContentFilterMode(mode) {
6102
+ if (this.contentFilterMode !== mode) {
6103
+ this.contentFilterMode = mode;
6104
+ this.render();
6105
+ }
6106
+ }
6107
+ /**
6108
+ * Update whether there's content to browse.
6109
+ * Affects whether ^B browse hint is shown in input mode.
6110
+ */
6111
+ setHasContent(has) {
6112
+ if (this.hasContent !== has) {
6113
+ this.hasContent = has;
6114
+ this.render();
6115
+ }
6116
+ }
6117
+ /**
6118
+ * Get current focus mode.
6119
+ */
6120
+ getFocusMode() {
6121
+ return this.focusMode;
6122
+ }
6123
+ /**
6124
+ * Get current content filter mode.
6125
+ */
6126
+ getContentFilterMode() {
6127
+ return this.contentFilterMode;
6128
+ }
6129
+ /**
6130
+ * Render hints based on current state.
6131
+ */
6132
+ render() {
6133
+ const hints = [];
6134
+ if (this.contentFilterMode === "focused") {
6135
+ hints.push("^K exit focused mode");
6136
+ } else if (this.focusMode === "input") {
6137
+ hints.push("^S multiline");
6138
+ if (this.hasContent) {
6139
+ hints.push("^B browse");
6140
+ }
6141
+ hints.push("^K focused");
6142
+ } else {
6143
+ hints.push("j/k nav");
6144
+ hints.push("Enter expand");
6145
+ hints.push("^B input");
6146
+ hints.push("^K focused");
6147
+ }
6148
+ this.hintsBox.setContent(`${GRAY}${hints.join(" ")}${RESET}`);
6149
+ this.renderCallback();
6150
+ }
6151
+ };
6152
+
6153
+ // src/tui/editor.ts
6154
+ import { spawnSync as spawnSync2 } from "child_process";
6155
+ import { readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
6156
+ import { tmpdir } from "os";
6157
+ import { join as join3 } from "path";
6158
+ function openEditorSync(initialContent = "") {
6159
+ const editor = process.env.VISUAL || process.env.EDITOR || "vi";
6160
+ const tmpFile = join3(tmpdir(), `llmist-input-${Date.now()}.txt`);
6161
+ writeFileSync2(tmpFile, initialContent, "utf-8");
6162
+ try {
6163
+ const parts = editor.split(/\s+/);
6164
+ const cmd = parts[0];
6165
+ const args = [...parts.slice(1), tmpFile];
6166
+ const result = spawnSync2(cmd, args, {
6167
+ stdio: "inherit",
6168
+ // Connect to user's terminal
6169
+ shell: false
6170
+ });
6171
+ if (result.error) {
6172
+ try {
6173
+ unlinkSync(tmpFile);
6174
+ } catch {
6175
+ }
6176
+ return null;
6177
+ }
6178
+ if (result.status === 0) {
6179
+ const content = readFileSync4(tmpFile, "utf-8");
6180
+ unlinkSync(tmpFile);
6181
+ const trimmed = content.trim();
6182
+ return trimmed || null;
6183
+ } else {
6184
+ unlinkSync(tmpFile);
6185
+ return null;
6186
+ }
6187
+ } catch {
6188
+ try {
6189
+ unlinkSync(tmpFile);
6190
+ } catch {
6191
+ }
6192
+ return null;
6193
+ }
6194
+ }
6195
+
6039
6196
  // src/tui/input-handler.ts
6040
- var IDLE_PROMPT = "> ";
6041
- var ACTIVE_PROMPT = ">>> ";
6197
+ var PROMPT = "> ";
6042
6198
  var InputHandler = class {
6043
6199
  inputBar;
6044
6200
  promptLabel;
@@ -6050,6 +6206,14 @@ var InputHandler = class {
6050
6206
  pendingInput = null;
6051
6207
  /** Whether we're waiting for REPL prompt (vs AskUser which should auto-focus) */
6052
6208
  isPendingREPLPrompt = false;
6209
+ /** Whether input mode is currently active (focused, capturing keystrokes) */
6210
+ isActive = false;
6211
+ /** Whether a bracketed paste is in progress */
6212
+ isPasting = false;
6213
+ /** Buffer for accumulating bracketed paste content */
6214
+ pasteBuffer = "";
6215
+ /** Flag to indicate content came from editor (skip paste detection on submit) */
6216
+ fromEditor = false;
6053
6217
  /** Callback when Ctrl+C is pressed */
6054
6218
  ctrlCCallback = null;
6055
6219
  /** Callback when Ctrl+B is pressed (toggle focus mode) */
@@ -6066,16 +6230,23 @@ var InputHandler = class {
6066
6230
  midSessionHandler = null;
6067
6231
  /** Callback to check current focus mode (to avoid conflicts with browse mode) */
6068
6232
  getFocusModeCallback = null;
6069
- constructor(inputBar, promptLabel, body, screen, renderCallback, renderNowCallback) {
6233
+ /** Body height when input bar is visible */
6234
+ bodyHeightWithInput;
6235
+ /** Body height when input bar is hidden (browse mode) */
6236
+ bodyHeightWithoutInput;
6237
+ constructor(inputBar, promptLabel, body, screen, renderCallback, renderNowCallback, hasHints = true) {
6070
6238
  this.inputBar = inputBar;
6071
6239
  this.promptLabel = promptLabel;
6072
6240
  this.body = body;
6073
6241
  this.screen = screen;
6074
6242
  this.renderCallback = renderCallback;
6075
6243
  this.renderNowCallback = renderNowCallback ?? renderCallback;
6244
+ this.bodyHeightWithInput = hasHints ? "100%-3" : "100%-2";
6245
+ this.bodyHeightWithoutInput = hasHints ? "100%-2" : "100%-1";
6076
6246
  this.inputBar.on("submit", (value) => {
6077
6247
  this.handleSubmit(value);
6078
6248
  });
6249
+ this.setupBracketedPasteHandler();
6079
6250
  this.inputBar.on("cancel", () => {
6080
6251
  this.handleCancel();
6081
6252
  });
@@ -6109,6 +6280,10 @@ var InputHandler = class {
6109
6280
  this.ctrlPCallback();
6110
6281
  }
6111
6282
  });
6283
+ this.inputBar.key(["C-s"], () => {
6284
+ const currentValue = this.inputBar.getValue();
6285
+ this.openEditorForInput(currentValue);
6286
+ });
6112
6287
  this.screen.key(["enter"], () => {
6113
6288
  if (this.isPendingREPLPrompt) {
6114
6289
  if (this.getFocusModeCallback?.() === "browse") {
@@ -6246,37 +6421,54 @@ var InputHandler = class {
6246
6421
  // Focus Mode API (controlled by TUIApp)
6247
6422
  // ─────────────────────────────────────────────────────────────────────────────
6248
6423
  /**
6249
- * Activate input mode - show input bar and capture keyboard.
6424
+ * Activate input mode - focus input bar and capture keyboard.
6250
6425
  * Called by TUIApp when switching to input mode.
6251
- * Preserves current prompt indicator and input text.
6426
+ * Shows input bar with active prompt (">>>") and starts capturing keystrokes.
6252
6427
  */
6253
6428
  activate() {
6254
6429
  this.isPendingREPLPrompt = false;
6430
+ this.isActive = true;
6255
6431
  this.promptLabel.show();
6256
6432
  this.inputBar.show();
6433
+ this.body.height = this.bodyHeightWithInput;
6434
+ this.setPrompt(PROMPT);
6257
6435
  this.renderNowCallback();
6258
6436
  this.inputBar.readInput();
6259
6437
  }
6438
+ /** Flag to prevent handleCancel from re-entering during deactivation */
6439
+ isDeactivating = false;
6260
6440
  /**
6261
6441
  * Deactivate input mode - hide input bar completely.
6262
6442
  * Called by TUIApp when switching to browse mode.
6443
+ * Input bar is hidden to give more space to content.
6263
6444
  */
6264
6445
  deactivate() {
6446
+ this.isPendingREPLPrompt = false;
6447
+ this.isActive = false;
6448
+ this.isDeactivating = true;
6449
+ this.inputBar.cancel();
6450
+ this.isDeactivating = false;
6265
6451
  this.promptLabel.hide();
6266
6452
  this.inputBar.hide();
6267
- this.isPendingREPLPrompt = false;
6453
+ this.body.height = this.bodyHeightWithoutInput;
6268
6454
  this.renderNowCallback();
6269
6455
  }
6270
6456
  /**
6271
- * Check if input mode is active (input bar visible and focused).
6457
+ * Check if input mode is active (focused, capturing keystrokes).
6272
6458
  */
6273
6459
  isInputActive() {
6274
- return this.inputBar.visible !== false;
6460
+ return this.isActive;
6275
6461
  }
6276
6462
  /**
6277
6463
  * Handle input submission.
6278
6464
  */
6279
6465
  handleSubmit(rawValue) {
6466
+ if (this.isPasting) {
6467
+ return;
6468
+ }
6469
+ if (this.fromEditor) {
6470
+ this.fromEditor = false;
6471
+ }
6280
6472
  const value = rawValue.trim();
6281
6473
  if (!value) {
6282
6474
  this.inputBar.readInput();
@@ -6298,18 +6490,93 @@ var InputHandler = class {
6298
6490
  * Handle input cancellation (ESC key).
6299
6491
  */
6300
6492
  handleCancel() {
6493
+ if (this.isDeactivating) {
6494
+ return;
6495
+ }
6301
6496
  if (this.pendingInput) {
6302
6497
  this.inputBar.readInput();
6303
6498
  } else {
6304
6499
  this.setIdle();
6305
6500
  }
6306
6501
  }
6502
+ /**
6503
+ * Set up bracketed paste mode detection.
6504
+ *
6505
+ * Terminal emulators that support bracketed paste send:
6506
+ * - \x1b[200~ before pasted content
6507
+ * - \x1b[201~ after pasted content
6508
+ *
6509
+ * This allows reliable detection of paste vs typed input.
6510
+ */
6511
+ setupBracketedPasteHandler() {
6512
+ const PASTE_START = "\x1B[200~";
6513
+ const PASTE_END = "\x1B[201~";
6514
+ this.screen.program.input.on("data", (data) => {
6515
+ const str = data.toString();
6516
+ if (str.includes(PASTE_START)) {
6517
+ this.isPasting = true;
6518
+ this.pasteBuffer = "";
6519
+ const startIdx = str.indexOf(PASTE_START) + PASTE_START.length;
6520
+ const afterStart = str.slice(startIdx);
6521
+ if (afterStart.includes(PASTE_END)) {
6522
+ const endIdx = afterStart.indexOf(PASTE_END);
6523
+ this.pasteBuffer = afterStart.slice(0, endIdx);
6524
+ this.isPasting = false;
6525
+ this.handlePaste(this.pasteBuffer);
6526
+ this.pasteBuffer = "";
6527
+ } else {
6528
+ this.pasteBuffer = afterStart;
6529
+ }
6530
+ return;
6531
+ }
6532
+ if (this.isPasting && str.includes(PASTE_END)) {
6533
+ const endIdx = str.indexOf(PASTE_END);
6534
+ this.pasteBuffer += str.slice(0, endIdx);
6535
+ this.isPasting = false;
6536
+ this.handlePaste(this.pasteBuffer);
6537
+ this.pasteBuffer = "";
6538
+ return;
6539
+ }
6540
+ if (this.isPasting) {
6541
+ this.pasteBuffer += str;
6542
+ }
6543
+ });
6544
+ }
6545
+ /**
6546
+ * Handle completed paste content.
6547
+ *
6548
+ * If content contains newlines, opens $EDITOR for multiline editing.
6549
+ * Otherwise, inserts directly into the input bar.
6550
+ */
6551
+ handlePaste(content) {
6552
+ if (!content) return;
6553
+ if (content.includes("\n")) {
6554
+ const currentValue = this.inputBar.getValue();
6555
+ this.openEditorForInput(currentValue + content);
6556
+ } else {
6557
+ const currentValue = this.inputBar.getValue();
6558
+ this.inputBar.setValue(currentValue + content);
6559
+ this.inputBar.readInput();
6560
+ }
6561
+ }
6562
+ /**
6563
+ * Set prompt text and dynamically adjust layout.
6564
+ * Idle prompt "> " uses 2 chars, active prompt ">>> " uses 4 chars.
6565
+ */
6566
+ setPrompt(prompt) {
6567
+ const width = prompt.length;
6568
+ this.promptLabel.width = width;
6569
+ this.promptLabel.setContent(prompt);
6570
+ this.inputBar.left = width;
6571
+ this.inputBar.width = `100%-${width}`;
6572
+ }
6307
6573
  /**
6308
6574
  * Set input to idle state.
6309
6575
  */
6310
6576
  setIdle() {
6311
6577
  this.isPendingREPLPrompt = false;
6312
- this.promptLabel.setContent(IDLE_PROMPT);
6578
+ this.isActive = false;
6579
+ this.setPrompt(PROMPT);
6313
6580
  this.inputBar.setValue("");
6314
6581
  this.renderCallback();
6315
6582
  }
@@ -6327,7 +6594,7 @@ var InputHandler = class {
6327
6594
  */
6328
6595
  setPendingPrompt() {
6329
6596
  this.isPendingREPLPrompt = true;
6330
- this.promptLabel.setContent(IDLE_PROMPT);
6597
+ this.setPrompt(PROMPT);
6331
6598
  this.inputBar.setValue("");
6332
6599
  this.renderCallback();
6333
6600
  }
@@ -6336,11 +6603,42 @@ var InputHandler = class {
6336
6603
  */
6337
6604
  setActive() {
6338
6605
  this.isPendingREPLPrompt = false;
6339
- this.promptLabel.setContent(ACTIVE_PROMPT);
6606
+ this.isActive = true;
6607
+ this.promptLabel.show();
6608
+ this.inputBar.show();
6609
+ this.body.height = this.bodyHeightWithInput;
6610
+ this.setPrompt(PROMPT);
6340
6611
  this.inputBar.setValue("");
6341
6612
  this.renderNowCallback();
6342
6613
  this.inputBar.readInput();
6343
6614
  }
6615
+ /**
6616
+ * Open $EDITOR for multiline input.
6617
+ * Called when user presses Ctrl+S or pastes multiline content.
6618
+ */
6619
+ openEditorForInput(initialContent) {
6620
+ this.screen.program.clear();
6621
+ this.screen.program.disableMouse();
6622
+ this.screen.program.showCursor();
6623
+ this.screen.program.normalBuffer();
6624
+ const result = openEditorSync(initialContent);
6625
+ this.screen.program.alternateBuffer();
6626
+ this.screen.program.hideCursor();
6627
+ this.screen.program.enableMouse();
6628
+ this.screen.alloc();
6629
+ this.screen.render();
6630
+ this.isPasting = false;
6631
+ this.pasteBuffer = "";
6632
+ setImmediate(() => {
6633
+ if (result) {
6634
+ this.fromEditor = true;
6635
+ this.handleSubmit(result);
6636
+ } else {
6637
+ this.inputBar.setValue(initialContent);
6638
+ this.inputBar.readInput();
6639
+ }
6640
+ });
6641
+ }
6344
6642
  };
6345
6643
 
6346
6644
  // src/tui/keymap.ts
@@ -6383,10 +6681,18 @@ var KeyboardManager = class {
6383
6681
  onAction({ type: "scroll_page", direction: 1 });
6384
6682
  });
6385
6683
  screen.key(["up", "k"], () => {
6684
+ if (this.config.getContentFilterMode() === "focused") {
6685
+ onAction({ type: "scroll_line", direction: -1 });
6686
+ return;
6687
+ }
6386
6688
  if (this.config.getFocusMode() !== "browse") return;
6387
6689
  onAction({ type: "navigation", action: "select_previous" });
6388
6690
  });
6389
6691
  screen.key(["down", "j"], () => {
6692
+ if (this.config.getContentFilterMode() === "focused") {
6693
+ onAction({ type: "scroll_line", direction: 1 });
6694
+ return;
6695
+ }
6390
6696
  if (this.config.getFocusMode() !== "browse") return;
6391
6697
  onAction({ type: "navigation", action: "select_next" });
6392
6698
  });
@@ -6446,13 +6752,16 @@ var KeyboardManager = class {
6446
6752
 
6447
6753
  // src/tui/layout.ts
6448
6754
  import { Box as Box2, ScrollableBox, Text, Textbox } from "@unblessed/node";
6449
- function createBlockLayout(screen) {
6755
+ function createBlockLayout(screen, showHints = true) {
6756
+ const inputBottom = showHints ? 2 : 1;
6757
+ const statusBottom = showHints ? 1 : 0;
6758
+ const bodyHeight = showHints ? "100%-3" : "100%-2";
6450
6759
  const body = new ScrollableBox({
6451
6760
  parent: screen,
6452
6761
  top: 0,
6453
6762
  left: 0,
6454
6763
  width: "100%",
6455
- height: "100%-2",
6764
+ height: bodyHeight,
6456
6765
  // Scrolling configuration
6457
6766
  scrollable: true,
6458
6767
  alwaysScroll: true,
@@ -6475,10 +6784,10 @@ function createBlockLayout(screen) {
6475
6784
  });
6476
6785
  const promptLabel = new Text({
6477
6786
  parent: screen,
6478
- bottom: 1,
6787
+ bottom: inputBottom,
6479
6788
  left: 0,
6480
- width: 4,
6481
- // ">>> " = 4 chars (max prompt width)
6789
+ width: 2,
6790
+ // "> " = 2 chars
6482
6791
  height: 1,
6483
6792
  content: "> ",
6484
6793
  style: {
@@ -6488,10 +6797,10 @@ function createBlockLayout(screen) {
6488
6797
  });
6489
6798
  const inputBar = new Textbox({
6490
6799
  parent: screen,
6491
- bottom: 1,
6492
- left: 4,
6493
- // Position after prompt label
6494
- width: "100%-4",
6800
+ bottom: inputBottom,
6801
+ left: 2,
6802
+ // Position after prompt label ("> " = 2 chars)
6803
+ width: "100%-2",
6495
6804
  height: 1,
6496
6805
  keys: true,
6497
6806
  mouse: true,
@@ -6502,7 +6811,7 @@ function createBlockLayout(screen) {
6502
6811
  });
6503
6812
  const statusBar = new Box2({
6504
6813
  parent: screen,
6505
- bottom: 0,
6814
+ bottom: statusBottom,
6506
6815
  left: 0,
6507
6816
  width: "100%",
6508
6817
  height: 1,
@@ -6512,7 +6821,19 @@ function createBlockLayout(screen) {
6512
6821
  bg: "black"
6513
6822
  }
6514
6823
  });
6515
- return { body, promptLabel, inputBar, statusBar };
6824
+ const hintsBar = showHints ? new Box2({
6825
+ parent: screen,
6826
+ bottom: 0,
6827
+ left: 0,
6828
+ width: "100%",
6829
+ height: 1,
6830
+ tags: false,
6831
+ style: {
6832
+ fg: "white",
6833
+ bg: "black"
6834
+ }
6835
+ }) : null;
6836
+ return { body, promptLabel, inputBar, statusBar, hintsBar };
6516
6837
  }
6517
6838
 
6518
6839
  // src/tui/approval-dialog.ts
@@ -6633,7 +6954,7 @@ function escapeContent(str) {
6633
6954
 
6634
6955
  // src/tui/raw-viewer.ts
6635
6956
  import { Box as Box4 } from "@unblessed/node";
6636
- var RESET = "\x1B[0m";
6957
+ var RESET2 = "\x1B[0m";
6637
6958
  var BOLD = "\x1B[1m";
6638
6959
  var DIM = "\x1B[2m";
6639
6960
  var RED = "\x1B[31m";
@@ -6665,17 +6986,17 @@ function showRawViewer(options) {
6665
6986
  if (mode === "request") {
6666
6987
  title = ` Raw Parameters - ${gadgetName} `;
6667
6988
  if (!parameters || Object.keys(parameters).length === 0) {
6668
- content = `${DIM}No parameters${RESET}`;
6989
+ content = `${DIM}No parameters${RESET2}`;
6669
6990
  } else {
6670
6991
  content = formatGadgetParameters(parameters);
6671
6992
  }
6672
6993
  } else {
6673
6994
  title = ` Raw Result - ${gadgetName} `;
6674
6995
  if (error) {
6675
- content = `${RED}${BOLD}Error:${RESET}
6996
+ content = `${RED}${BOLD}Error:${RESET2}
6676
6997
  ${error}`;
6677
6998
  } else if (!result) {
6678
- content = `${DIM}No result data available${RESET}`;
6999
+ content = `${DIM}No result data available${RESET2}`;
6679
7000
  } else {
6680
7001
  content = formatGadgetResult(result);
6681
7002
  }
@@ -6684,14 +7005,14 @@ ${error}`;
6684
7005
  if (mode === "request") {
6685
7006
  title = ` Raw Request - #${iteration} ${model} `;
6686
7007
  if (!request || request.length === 0) {
6687
- content = `${DIM}No request data available${RESET}`;
7008
+ content = `${DIM}No request data available${RESET2}`;
6688
7009
  } else {
6689
7010
  content = formatMessages(request);
6690
7011
  }
6691
7012
  } else {
6692
7013
  title = ` Raw Response - #${iteration} ${model} `;
6693
7014
  if (!response) {
6694
- content = `${DIM}No response data available${RESET}`;
7015
+ content = `${DIM}No response data available${RESET2}`;
6695
7016
  } else {
6696
7017
  content = response;
6697
7018
  }
@@ -6732,7 +7053,7 @@ ${error}`;
6732
7053
  left: 0,
6733
7054
  width: "100%",
6734
7055
  height: 1,
6735
- content: `${DIM} [${WHITE}\u2191/\u2193/PgUp/PgDn${DIM}] Scroll [${WHITE}Home/End${DIM}] Jump [${WHITE}Escape/q${DIM}] Close${RESET}`,
7056
+ content: `${DIM} [${WHITE}\u2191/\u2193/PgUp/PgDn${DIM}] Scroll [${WHITE}Home/End${DIM}] Jump [${WHITE}Escape/q${DIM}] Close${RESET2}`,
6736
7057
  tags: false,
6737
7058
  style: { fg: "white", bg: "black" }
6738
7059
  });
@@ -6764,11 +7085,11 @@ function formatMessages(messages) {
6764
7085
  const msg = messages[i];
6765
7086
  const roleColor = getRoleColor(msg.role);
6766
7087
  const roleName = msg.role.toUpperCase();
6767
- lines.push(`${DIM}${separator}${RESET}`);
7088
+ lines.push(`${DIM}${separator}${RESET2}`);
6768
7089
  lines.push(
6769
- `${roleColor}${BOLD}[${roleName}]${RESET} ${DIM}Message ${i + 1} of ${messages.length}${RESET}`
7090
+ `${roleColor}${BOLD}[${roleName}]${RESET2} ${DIM}Message ${i + 1} of ${messages.length}${RESET2}`
6770
7091
  );
6771
- lines.push(`${DIM}${separator}${RESET}`);
7092
+ lines.push(`${DIM}${separator}${RESET2}`);
6772
7093
  lines.push("");
6773
7094
  const contentLines = formatMessageContent(msg.content);
6774
7095
  lines.push(...contentLines);
@@ -6791,19 +7112,19 @@ function formatMessageContent(content) {
6791
7112
  lines.push(...part.text.split("\n"));
6792
7113
  } else if (isImagePart(part)) {
6793
7114
  const mediaType = part.source?.media_type || "unknown";
6794
- lines.push(`${DIM}[Image: ${mediaType}]${RESET}`);
7115
+ lines.push(`${DIM}[Image: ${mediaType}]${RESET2}`);
6795
7116
  } else if (isAudioPart(part)) {
6796
7117
  const mediaType = part.source?.media_type || "unknown";
6797
- lines.push(`${DIM}[Audio: ${mediaType}]${RESET}`);
7118
+ lines.push(`${DIM}[Audio: ${mediaType}]${RESET2}`);
6798
7119
  } else if (isToolUsePart(part)) {
6799
- lines.push(`${YELLOW}${BOLD}[Tool Use: ${part.name}]${RESET}`);
6800
- lines.push(`${DIM}ID: ${part.id}${RESET}`);
6801
- lines.push(`${DIM}Input:${RESET}`);
7120
+ lines.push(`${YELLOW}${BOLD}[Tool Use: ${part.name}]${RESET2}`);
7121
+ lines.push(`${DIM}ID: ${part.id}${RESET2}`);
7122
+ lines.push(`${DIM}Input:${RESET2}`);
6802
7123
  const inputStr = JSON.stringify(part.input, null, 2);
6803
7124
  lines.push(...inputStr.split("\n").map((l) => ` ${l}`));
6804
7125
  } else if (isToolResultPart(part)) {
6805
- lines.push(`${CYAN}${BOLD}[Tool Result]${RESET}`);
6806
- lines.push(`${DIM}Tool Use ID: ${part.tool_use_id}${RESET}`);
7126
+ lines.push(`${CYAN}${BOLD}[Tool Result]${RESET2}`);
7127
+ lines.push(`${DIM}Tool Use ID: ${part.tool_use_id}${RESET2}`);
6807
7128
  if (typeof part.content === "string") {
6808
7129
  lines.push(...part.content.split("\n"));
6809
7130
  } else {
@@ -6811,7 +7132,7 @@ function formatMessageContent(content) {
6811
7132
  }
6812
7133
  } else {
6813
7134
  const partType = part.type || "unknown";
6814
- lines.push(`${DIM}[${partType}]${RESET}`);
7135
+ lines.push(`${DIM}[${partType}]${RESET2}`);
6815
7136
  lines.push(JSON.stringify(part, null, 2));
6816
7137
  }
6817
7138
  }
@@ -6847,9 +7168,9 @@ function isToolResultPart(part) {
6847
7168
  function formatGadgetParameters(params) {
6848
7169
  const lines = [];
6849
7170
  const separator = "\u2500".repeat(78);
6850
- lines.push(`${DIM}${separator}${RESET}`);
6851
- lines.push(`${CYAN}${BOLD}Parameters${RESET}`);
6852
- lines.push(`${DIM}${separator}${RESET}`);
7171
+ lines.push(`${DIM}${separator}${RESET2}`);
7172
+ lines.push(`${CYAN}${BOLD}Parameters${RESET2}`);
7173
+ lines.push(`${DIM}${separator}${RESET2}`);
6853
7174
  lines.push("");
6854
7175
  const json = JSON.stringify(params, null, 2);
6855
7176
  const highlighted = highlightJson(json);
@@ -6859,9 +7180,9 @@ function formatGadgetParameters(params) {
6859
7180
  function formatGadgetResult(result) {
6860
7181
  const lines = [];
6861
7182
  const separator = "\u2500".repeat(78);
6862
- lines.push(`${DIM}${separator}${RESET}`);
6863
- lines.push(`${GREEN}${BOLD}Result${RESET}`);
6864
- lines.push(`${DIM}${separator}${RESET}`);
7183
+ lines.push(`${DIM}${separator}${RESET2}`);
7184
+ lines.push(`${GREEN}${BOLD}Result${RESET2}`);
7185
+ lines.push(`${DIM}${separator}${RESET2}`);
6865
7186
  lines.push("");
6866
7187
  const trimmed = result.trim();
6867
7188
  if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
@@ -6877,10 +7198,10 @@ function formatGadgetResult(result) {
6877
7198
  return lines.join("\n");
6878
7199
  }
6879
7200
  function highlightJson(json) {
6880
- let result = json.replace(/"([^"]+)":/g, `${CYAN}"$1"${RESET}:`);
6881
- result = result.replace(/: "([^"]*)"/g, `: ${GREEN}"$1"${RESET}`);
6882
- result = result.replace(/: (-?\d+\.?\d*)/g, `: ${YELLOW}$1${RESET}`);
6883
- result = result.replace(/: (true|false|null)/g, `: ${MAGENTA}$1${RESET}`);
7201
+ let result = json.replace(/"([^"]+)":/g, `${CYAN}"$1"${RESET2}:`);
7202
+ result = result.replace(/: "([^"]*)"/g, `: ${GREEN}"$1"${RESET2}`);
7203
+ result = result.replace(/: (-?\d+\.?\d*)/g, `: ${YELLOW}$1${RESET2}`);
7204
+ result = result.replace(/: (true|false|null)/g, `: ${MAGENTA}$1${RESET2}`);
6884
7205
  return result;
6885
7206
  }
6886
7207
 
@@ -6959,6 +7280,7 @@ function createScreen(options) {
6959
7280
  // Use alternate screen buffer (restores on exit)
6960
7281
  useBCE: true
6961
7282
  });
7283
+ screen.program.write("\x1B[?2004h");
6962
7284
  let isDestroyed = false;
6963
7285
  let renderPending = false;
6964
7286
  let renderTimeout = null;
@@ -6989,6 +7311,7 @@ function createScreen(options) {
6989
7311
  clearTimeout(renderTimeout);
6990
7312
  renderTimeout = null;
6991
7313
  }
7314
+ screen.program.write("\x1B[?2004l");
6992
7315
  screen.destroy();
6993
7316
  process.stdout.write("\x1B[?25h");
6994
7317
  };
@@ -7030,7 +7353,7 @@ var StatusBar = class {
7030
7353
  streamingOutputTokens = 0;
7031
7354
  /** Whether we're currently streaming */
7032
7355
  isStreaming = false;
7033
- /** Active LLM calls: Map from label ("#1") to model name */
7356
+ /** Active LLM calls: Map from label ("#1") to model name and start time */
7034
7357
  activeLLMCalls = /* @__PURE__ */ new Map();
7035
7358
  /** Active gadgets (by name) */
7036
7359
  activeGadgets = /* @__PURE__ */ new Set();
@@ -7127,7 +7450,7 @@ var StatusBar = class {
7127
7450
  * @param model - Full model name like "gemini:gemini-2.5-flash"
7128
7451
  */
7129
7452
  startLLMCall(label, model) {
7130
- this.activeLLMCalls.set(label, model);
7453
+ this.activeLLMCalls.set(label, { model, startTime: Date.now() });
7131
7454
  this.startSpinner();
7132
7455
  this.render();
7133
7456
  }
@@ -7140,6 +7463,19 @@ var StatusBar = class {
7140
7463
  this.maybeStopSpinner();
7141
7464
  this.render();
7142
7465
  }
7466
+ /**
7467
+ * Get the start time of the earliest running LLM call.
7468
+ * Used to show elapsed time since the first call started (for concurrent subagents).
7469
+ * @returns Start time in ms, or null if no LLM calls are active
7470
+ */
7471
+ getEarliestLLMCallStartTime() {
7472
+ if (this.activeLLMCalls.size === 0) return null;
7473
+ let earliest = Infinity;
7474
+ for (const { startTime } of this.activeLLMCalls.values()) {
7475
+ if (startTime < earliest) earliest = startTime;
7476
+ }
7477
+ return earliest;
7478
+ }
7143
7479
  /**
7144
7480
  * Track a gadget as active.
7145
7481
  * @param name - Gadget name like "ReadFile" or "BrowseWeb"
@@ -7271,6 +7607,15 @@ var StatusBar = class {
7271
7607
  this.startLLMCall(label, event.model);
7272
7608
  break;
7273
7609
  }
7610
+ case "llm_response_end": {
7611
+ const label = this.nodeIdToLabel.get(event.nodeId);
7612
+ if (label) {
7613
+ this.activeLLMCalls.delete(label);
7614
+ this.maybeStopSpinner();
7615
+ this.render();
7616
+ }
7617
+ break;
7618
+ }
7274
7619
  case "llm_call_complete": {
7275
7620
  const label = this.nodeIdToLabel.get(event.nodeId);
7276
7621
  if (label) {
@@ -7414,68 +7759,72 @@ var StatusBar = class {
7414
7759
  * @param immediate - If true, render immediately without debouncing
7415
7760
  */
7416
7761
  render(immediate = false) {
7417
- const elapsed = this.getElapsedSeconds().toFixed(1);
7418
7762
  const YELLOW2 = "\x1B[33m";
7419
7763
  const GREEN2 = "\x1B[32m";
7420
7764
  const BLUE = "\x1B[34m";
7421
7765
  const CYAN2 = "\x1B[36m";
7422
7766
  const MAGENTA2 = "\x1B[35m";
7423
- const GRAY = "\x1B[90m";
7424
- const RESET2 = "\x1B[0m";
7767
+ const GRAY2 = "\x1B[90m";
7768
+ const RESET3 = "\x1B[0m";
7425
7769
  const BG_BLUE = "\x1B[44m";
7426
- const BG_GREEN = "\x1B[42m";
7427
7770
  const WHITE2 = "\x1B[37m";
7428
- const BLACK = "\x1B[30m";
7429
7771
  const displayInputTokens = this.metrics.inputTokens + this.streamingInputTokens;
7430
7772
  const displayOutputTokens = this.metrics.outputTokens + this.streamingOutputTokens;
7431
7773
  const parts = [];
7432
- if (this.contentFilterMode === "focused") {
7433
- parts.push(`${BG_BLUE}${WHITE2} FOCUSED ${RESET2}`);
7434
- } else if (this.focusMode === "browse") {
7435
- parts.push(`${BG_BLUE}${WHITE2} BROWSE ${RESET2}`);
7436
- } else {
7437
- parts.push(`${BG_GREEN}${BLACK} INPUT ${RESET2}`);
7774
+ if (this.focusMode === "browse" && this.contentFilterMode !== "focused") {
7775
+ parts.push(`${BG_BLUE}${WHITE2} BROWSE ${RESET3}`);
7438
7776
  }
7439
7777
  if (this.profiles.length > 0) {
7440
7778
  const profile = this.profiles[this.currentProfileIndex];
7441
7779
  const display = profile.length > 12 ? `${profile.slice(0, 11)}\u2026` : profile;
7442
- parts.push(`${YELLOW2}${display}${RESET2}`);
7780
+ parts.push(`${YELLOW2}${display}${RESET3}`);
7781
+ }
7782
+ if (displayInputTokens > 0) {
7783
+ const inputPrefix = this.isStreaming && this.streamingInputTokens > 0 ? "~" : "";
7784
+ parts.push(`${YELLOW2}\u2191${inputPrefix}${formatTokens(displayInputTokens)}${RESET3}`);
7443
7785
  }
7444
- const inputPrefix = this.isStreaming && this.streamingInputTokens > 0 ? "~" : "";
7445
- parts.push(`${YELLOW2}\u2191 ${inputPrefix}${formatTokens(displayInputTokens)}${RESET2}`);
7446
7786
  if (this.metrics.cachedTokens > 0) {
7447
- parts.push(`${BLUE}\u293F ${formatTokens(this.metrics.cachedTokens)}${RESET2}`);
7787
+ parts.push(`${BLUE}\u293F${formatTokens(this.metrics.cachedTokens)}${RESET3}`);
7788
+ }
7789
+ if (displayOutputTokens > 0) {
7790
+ const outputPrefix = this.isStreaming ? "~" : "";
7791
+ parts.push(`${GREEN2}\u2193${outputPrefix}${formatTokens(displayOutputTokens)}${RESET3}`);
7792
+ }
7793
+ const earliestStart = this.getEarliestLLMCallStartTime();
7794
+ if (earliestStart !== null) {
7795
+ const elapsedSeconds = (Date.now() - earliestStart) / 1e3;
7796
+ const timeStr = elapsedSeconds % 1 === 0 ? `${elapsedSeconds}s` : `${elapsedSeconds.toFixed(1)}s`;
7797
+ parts.push(`${GRAY2}${timeStr}${RESET3}`);
7798
+ }
7799
+ if (this.metrics.cost > 0) {
7800
+ parts.push(`${CYAN2}$${formatCost(this.metrics.cost)}${RESET3}`);
7448
7801
  }
7449
- const outputPrefix = this.isStreaming ? "~" : "";
7450
- parts.push(`${GREEN2}\u2193 ${outputPrefix}${formatTokens(displayOutputTokens)}${RESET2}`);
7451
- parts.push(`${GRAY}${elapsed}s${RESET2}`);
7452
- parts.push(`${CYAN2}$${formatCost(this.metrics.cost)}${RESET2}`);
7453
7802
  if (this.selectionDebugCallback) {
7454
7803
  const debug = this.selectionDebugCallback();
7455
7804
  const debugStr = `sel:${debug.index}/${debug.total}`;
7456
7805
  const typeStr = debug.nodeType ? ` [${debug.nodeType}]` : "";
7457
- parts.push(`${GRAY}${debugStr}${typeStr}${RESET2}`);
7806
+ parts.push(`${GRAY2}${debugStr}${typeStr}${RESET3}`);
7458
7807
  }
7459
7808
  if (this.rateLimitState?.isThrottling) {
7460
7809
  const { triggeredBy } = this.rateLimitState;
7461
7810
  if (triggeredBy?.daily) {
7462
- parts.push(`${YELLOW2}\u23F8 Daily limit, resets midnight UTC${RESET2}`);
7811
+ parts.push(`${YELLOW2}\u23F8 Daily limit, resets midnight UTC${RESET3}`);
7463
7812
  } else {
7464
7813
  const seconds = Math.ceil(this.rateLimitState.delayMs / 1e3);
7465
7814
  const reason = triggeredBy?.rpm ? " (RPM)" : triggeredBy?.tpm ? " (TPM)" : "";
7466
- parts.push(`${YELLOW2}\u23F8 Throttled ${seconds}s${reason}${RESET2}`);
7815
+ parts.push(`${YELLOW2}\u23F8 Throttled ${seconds}s${reason}${RESET3}`);
7467
7816
  }
7468
7817
  }
7469
7818
  if (this.retryState) {
7470
7819
  const { attemptNumber, retriesLeft } = this.retryState;
7471
7820
  const totalAttempts = attemptNumber + retriesLeft;
7472
- parts.push(`${BLUE}\u{1F504} Retry ${attemptNumber}/${totalAttempts}${RESET2}`);
7821
+ parts.push(`${BLUE}\u{1F504} Retry ${attemptNumber}/${totalAttempts}${RESET3}`);
7473
7822
  }
7474
7823
  if (this.activeLLMCalls.size > 0 || this.activeGadgets.size > 0) {
7475
7824
  const spinner = SPINNER_FRAMES2[this.spinnerFrame];
7476
7825
  if (this.activeLLMCalls.size > 0) {
7477
7826
  const byModel = /* @__PURE__ */ new Map();
7478
- for (const [label, model] of this.activeLLMCalls) {
7827
+ for (const [label, { model }] of this.activeLLMCalls) {
7479
7828
  const shortModel = this.shortenModelName(model);
7480
7829
  if (!byModel.has(shortModel)) byModel.set(shortModel, []);
7481
7830
  byModel.get(shortModel)?.push(label);
@@ -7484,15 +7833,15 @@ var StatusBar = class {
7484
7833
  for (const [model, labels] of byModel) {
7485
7834
  llmParts.push(`${model} ${labels.join(", ")}`);
7486
7835
  }
7487
- parts.push(`${spinner} ${MAGENTA2}${llmParts.join(" | ")}${RESET2}`);
7836
+ parts.push(`${spinner} ${MAGENTA2}${llmParts.join(" | ")}${RESET3}`);
7488
7837
  }
7489
7838
  if (this.activeGadgets.size > 0) {
7490
7839
  const gadgetList = [...this.activeGadgets].slice(0, 3).join(", ");
7491
7840
  const more = this.activeGadgets.size > 3 ? ` +${this.activeGadgets.size - 3}` : "";
7492
- parts.push(`${CYAN2}\u23F5 ${gadgetList}${more}${RESET2}`);
7841
+ parts.push(`${CYAN2}\u23F5 ${gadgetList}${more}${RESET3}`);
7493
7842
  }
7494
7843
  }
7495
- this.statusBox.setContent(parts.join(` ${GRAY}|${RESET2} `));
7844
+ this.statusBox.setContent(parts.join(" "));
7496
7845
  if (immediate) {
7497
7846
  this.renderNowCallback();
7498
7847
  } else {
@@ -7538,7 +7887,12 @@ var TUIApp = class _TUIApp {
7538
7887
  title: "llmist"
7539
7888
  });
7540
7889
  const { screen } = screenCtx;
7541
- const layout = createBlockLayout(screen);
7890
+ const showHints = options.showHints ?? true;
7891
+ const layout = createBlockLayout(screen, showHints);
7892
+ let hintsBar = null;
7893
+ if (layout.hintsBar) {
7894
+ hintsBar = new HintsBar(layout.hintsBar, () => screenCtx.requestRender());
7895
+ }
7542
7896
  const statusBar = new StatusBar(
7543
7897
  layout.statusBar,
7544
7898
  options.model,
@@ -7551,25 +7905,34 @@ var TUIApp = class _TUIApp {
7551
7905
  layout.body,
7552
7906
  screen,
7553
7907
  () => screenCtx.requestRender(),
7554
- () => screenCtx.renderNow()
7908
+ () => screenCtx.renderNow(),
7909
+ showHints
7555
7910
  );
7556
7911
  const blockRenderer = new BlockRenderer(
7557
7912
  layout.body,
7558
7913
  () => screenCtx.requestRender(),
7559
7914
  () => screenCtx.renderNow()
7560
7915
  );
7916
+ if (hintsBar) {
7917
+ blockRenderer.onHasContentChange((hasContent) => {
7918
+ hintsBar.setHasContent(hasContent);
7919
+ });
7920
+ }
7561
7921
  const controller = new TUIController({
7562
7922
  onFocusModeChange: (mode) => {
7563
7923
  applyFocusMode(mode, layout, statusBar, inputHandler, screenCtx);
7924
+ hintsBar?.setFocusMode(mode);
7564
7925
  },
7565
7926
  onContentFilterModeChange: (mode) => {
7566
7927
  applyContentFilterMode(mode, blockRenderer, statusBar, screenCtx);
7928
+ hintsBar?.setContentFilterMode(mode);
7567
7929
  }
7568
7930
  });
7569
7931
  const modalManager = new ModalManager();
7570
7932
  const keyboardManager = new KeyboardManager({
7571
7933
  screen,
7572
7934
  getFocusMode: () => controller.getFocusMode(),
7935
+ getContentFilterMode: () => controller.getContentFilterMode(),
7573
7936
  isWaitingForREPLPrompt: () => inputHandler.isWaitingForREPLPrompt(),
7574
7937
  hasPendingInput: () => inputHandler.hasPendingInput(),
7575
7938
  isBlockExpanded: () => blockRenderer.getSelectedBlock()?.expanded ?? false,
@@ -7717,13 +8080,11 @@ var TUIApp = class _TUIApp {
7717
8080
  }
7718
8081
  /**
7719
8082
  * Wait for user to enter a new prompt (REPL mode).
7720
- * Stays in current mode (browse) - user can Tab to input or Enter to start typing.
7721
- * After the prompt is submitted, focus mode switches to BROWSE.
8083
+ * Stays in input mode after submission - user can watch output and type next message.
8084
+ * User can Ctrl+B to browse if they want to navigate blocks.
7722
8085
  */
7723
8086
  async waitForPrompt() {
7724
- const result = await this.inputHandler.waitForPrompt();
7725
- this.controller.setFocusMode("browse");
7726
- return result;
8087
+ return this.inputHandler.waitForPrompt();
7727
8088
  }
7728
8089
  /**
7729
8090
  * Enter the pending REPL prompt state without blocking.
@@ -7942,17 +8303,13 @@ var TUIApp = class _TUIApp {
7942
8303
  };
7943
8304
  function applyFocusMode(mode, layout, statusBar, inputHandler, screenCtx) {
7944
8305
  statusBar.setFocusMode(mode);
7945
- if (mode === "input") {
7946
- layout.body.height = "100%-2";
7947
- } else {
7948
- layout.body.height = "100%-1";
7949
- }
7950
- screenCtx.renderNow();
7951
8306
  if (mode === "input") {
7952
8307
  inputHandler.activate();
7953
8308
  } else {
7954
8309
  inputHandler.deactivate();
8310
+ layout.body.focus();
7955
8311
  }
8312
+ screenCtx.renderNow();
7956
8313
  }
7957
8314
  function applyContentFilterMode(mode, blockRenderer, statusBar, screenCtx) {
7958
8315
  blockRenderer.setContentFilterMode(mode);
@@ -7997,6 +8354,14 @@ function handleKeyAction(action, controller, blockRenderer, statusBar, screenCtx
7997
8354
  screenCtx.renderNow();
7998
8355
  break;
7999
8356
  }
8357
+ case "scroll_line": {
8358
+ const body = layout.body;
8359
+ if (!body.scroll) return;
8360
+ body.scroll(action.direction);
8361
+ blockRenderer.handleUserScroll();
8362
+ screenCtx.renderNow();
8363
+ break;
8364
+ }
8000
8365
  case "navigation":
8001
8366
  switch (action.action) {
8002
8367
  case "select_next":
@@ -8082,7 +8447,8 @@ async function executeAgent(promptArg, options, env, commandName) {
8082
8447
  tui = await TUIApp.create({
8083
8448
  model: options.model,
8084
8449
  stdin: env.stdin,
8085
- stdout: env.stdout
8450
+ stdout: env.stdout,
8451
+ showHints: options.showHints
8086
8452
  });
8087
8453
  try {
8088
8454
  const fullConfig = loadConfig();
@@ -8471,7 +8837,8 @@ function registerAgentCommand(program, env, config, globalSubagents, globalRateL
8471
8837
  globalRateLimits,
8472
8838
  globalRetry,
8473
8839
  profileRateLimits: config?.["rate-limits"],
8474
- profileRetry: config?.retry
8840
+ profileRetry: config?.retry,
8841
+ showHints: config?.["show-hints"]
8475
8842
  };
8476
8843
  return executeAgent(prompt, mergedOptions, env, "agent");
8477
8844
  }, env)
@@ -8696,7 +9063,7 @@ System Prompt (${chars.toLocaleString()} chars, ${lines} lines):
8696
9063
  }
8697
9064
 
8698
9065
  // src/environment.ts
8699
- import { join as join3 } from "path";
9066
+ import { join as join4 } from "path";
8700
9067
  import readline from "readline";
8701
9068
  import chalk4 from "chalk";
8702
9069
  import { createLogger, LLMist } from "llmist";
@@ -8719,7 +9086,7 @@ function createLoggerFactory(config, sessionLogDir) {
8719
9086
  }
8720
9087
  }
8721
9088
  if (sessionLogDir) {
8722
- const logFile = join3(sessionLogDir, "session.log.jsonl");
9089
+ const logFile = join4(sessionLogDir, "session.log.jsonl");
8723
9090
  const originalLogFile = process.env.LLMIST_LOG_FILE;
8724
9091
  process.env.LLMIST_LOG_FILE = logFile;
8725
9092
  const logger = createLogger(options);
@@ -9265,7 +9632,7 @@ function registerGadgetCommand(program, env) {
9265
9632
  }
9266
9633
 
9267
9634
  // src/image-command.ts
9268
- import { writeFileSync as writeFileSync2 } from "fs";
9635
+ import { writeFileSync as writeFileSync3 } from "fs";
9269
9636
  var DEFAULT_IMAGE_MODEL = "dall-e-3";
9270
9637
  async function executeImage(promptArg, options, env) {
9271
9638
  const prompt = await resolvePrompt(promptArg, env);
@@ -9289,7 +9656,7 @@ async function executeImage(promptArg, options, env) {
9289
9656
  const imageData = result.images[0];
9290
9657
  if (imageData.b64Json) {
9291
9658
  const buffer = Buffer.from(imageData.b64Json, "base64");
9292
- writeFileSync2(options.output, buffer);
9659
+ writeFileSync3(options.output, buffer);
9293
9660
  if (!options.quiet) {
9294
9661
  env.stderr.write(`${SUMMARY_PREFIX} Image saved to ${options.output}
9295
9662
  `);
@@ -9328,7 +9695,7 @@ function registerImageCommand(program, env, config) {
9328
9695
  }
9329
9696
 
9330
9697
  // src/init-command.ts
9331
- import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3 } from "fs";
9698
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync4 } from "fs";
9332
9699
  import { dirname as dirname2 } from "path";
9333
9700
  var STARTER_CONFIG = `# ~/.llmist/cli.toml
9334
9701
  # llmist CLI configuration file
@@ -9399,7 +9766,7 @@ async function executeInit(_options, env) {
9399
9766
  if (!existsSync3(configDir)) {
9400
9767
  mkdirSync(configDir, { recursive: true });
9401
9768
  }
9402
- writeFileSync3(configPath, STARTER_CONFIG, "utf-8");
9769
+ writeFileSync4(configPath, STARTER_CONFIG, "utf-8");
9403
9770
  env.stderr.write(`Created ${configPath}
9404
9771
  `);
9405
9772
  env.stderr.write("\n");
@@ -9831,7 +10198,7 @@ function registerModelsCommand(program, env) {
9831
10198
  import { existsSync as existsSync4 } from "fs";
9832
10199
  import { mkdir as mkdir2 } from "fs/promises";
9833
10200
  import { homedir as homedir3 } from "os";
9834
- import { join as join4 } from "path";
10201
+ import { join as join5 } from "path";
9835
10202
 
9836
10203
  // src/session-names.ts
9837
10204
  var ADJECTIVES = [
@@ -9966,16 +10333,16 @@ function generateSessionName() {
9966
10333
 
9967
10334
  // src/session.ts
9968
10335
  var currentSession;
9969
- var SESSION_LOGS_BASE = join4(homedir3(), ".llmist", "logs");
10336
+ var SESSION_LOGS_BASE = join5(homedir3(), ".llmist", "logs");
9970
10337
  function findUniqueName(baseName) {
9971
- const baseDir = join4(SESSION_LOGS_BASE, baseName);
10338
+ const baseDir = join5(SESSION_LOGS_BASE, baseName);
9972
10339
  if (!existsSync4(baseDir)) {
9973
10340
  return baseName;
9974
10341
  }
9975
10342
  let suffix = 2;
9976
10343
  while (suffix < 1e3) {
9977
10344
  const name = `${baseName}-${suffix}`;
9978
- const dir = join4(SESSION_LOGS_BASE, name);
10345
+ const dir = join5(SESSION_LOGS_BASE, name);
9979
10346
  if (!existsSync4(dir)) {
9980
10347
  return name;
9981
10348
  }
@@ -9989,14 +10356,14 @@ async function initSession() {
9989
10356
  }
9990
10357
  const baseName = generateSessionName();
9991
10358
  const name = findUniqueName(baseName);
9992
- const logDir = join4(SESSION_LOGS_BASE, name);
10359
+ const logDir = join5(SESSION_LOGS_BASE, name);
9993
10360
  await mkdir2(logDir, { recursive: true });
9994
10361
  currentSession = { name, logDir };
9995
10362
  return currentSession;
9996
10363
  }
9997
10364
 
9998
10365
  // src/speech-command.ts
9999
- import { writeFileSync as writeFileSync4 } from "fs";
10366
+ import { writeFileSync as writeFileSync5 } from "fs";
10000
10367
  var DEFAULT_SPEECH_MODEL = "tts-1";
10001
10368
  var DEFAULT_VOICE = "nova";
10002
10369
  async function executeSpeech(textArg, options, env) {
@@ -10019,7 +10386,7 @@ async function executeSpeech(textArg, options, env) {
10019
10386
  });
10020
10387
  const audioBuffer = Buffer.from(result.audio);
10021
10388
  if (options.output) {
10022
- writeFileSync4(options.output, audioBuffer);
10389
+ writeFileSync5(options.output, audioBuffer);
10023
10390
  if (!options.quiet) {
10024
10391
  env.stderr.write(`${SUMMARY_PREFIX} Audio saved to ${options.output}
10025
10392
  `);