@llmist/cli 15.4.1 → 15.6.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.4.1",
101
+ version: "15.6.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.4.1",
157
+ llmist: "^15.6.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.4.1",
171
+ "@llmist/testing": "^15.6.0",
172
172
  "@types/diff": "^8.0.0",
173
173
  "@types/js-yaml": "^4.0.9",
174
174
  "@types/marked-terminal": "^6.1.1",
@@ -1780,6 +1780,8 @@ Uses layered matching strategies (in order):
1780
1780
 
1781
1781
  For multiple edits to the same file, call this gadget multiple times.
1782
1782
  Each call provides immediate feedback, allowing you to adjust subsequent edits.`,
1783
+ maxConcurrent: 1,
1784
+ // Sequential execution to prevent race conditions
1783
1785
  schema: z2.object({
1784
1786
  filePath: z2.string().describe("Path to the file to edit (relative or absolute)"),
1785
1787
  search: z2.string().describe("The content to search for in the file"),
@@ -2036,6 +2038,8 @@ import { z as z5 } from "zod";
2036
2038
  var writeFile = createGadget5({
2037
2039
  name: "WriteFile",
2038
2040
  description: "Write content to a file. Creates parent directories if needed. Overwrites existing files. The file path must be within the current working directory or its subdirectories.",
2041
+ maxConcurrent: 1,
2042
+ // Sequential execution to prevent race conditions
2039
2043
  schema: z5.object({
2040
2044
  filePath: z5.string().describe("Path to the file to write (relative or absolute)"),
2041
2045
  content: z5.string().describe("Content to write to the file")
@@ -4090,6 +4094,7 @@ function configToAgentOptions(config) {
4090
4094
  if (r["max-timeout"] !== void 0) result.retryMaxTimeout = r["max-timeout"];
4091
4095
  if (r.enabled === false) result.noRetry = true;
4092
4096
  }
4097
+ if (config["show-hints"] !== void 0) result.showHints = config["show-hints"];
4093
4098
  return result;
4094
4099
  }
4095
4100
 
@@ -4667,6 +4672,10 @@ var BlockRenderer = class _BlockRenderer {
4667
4672
  currentSessionId = 0;
4668
4673
  /** Previous session ID (for deferred cleanup) */
4669
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;
4670
4679
  constructor(container, renderCallback, renderNowCallback) {
4671
4680
  this.container = container;
4672
4681
  this.renderCallback = renderCallback;
@@ -4967,6 +4976,16 @@ var BlockRenderer = class _BlockRenderer {
4967
4976
  child.detach();
4968
4977
  }
4969
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);
4970
4989
  }
4971
4990
  /**
4972
4991
  * Start a new session. Called at the start of each REPL turn.
@@ -5006,6 +5025,7 @@ var BlockRenderer = class _BlockRenderer {
5006
5025
  }
5007
5026
  this.previousSessionId = null;
5008
5027
  this.renderCallback();
5028
+ this.notifyHasContentChange();
5009
5029
  }
5010
5030
  /**
5011
5031
  * Get the current session ID (for node creation).
@@ -5094,6 +5114,17 @@ var BlockRenderer = class _BlockRenderer {
5094
5114
  // ───────────────────────────────────────────────────────────────────────────
5095
5115
  // Private - Node & Block Management
5096
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
+ }
5097
5128
  generateId(prefix) {
5098
5129
  return `${prefix}_${++this.nodeIdCounter}`;
5099
5130
  }
@@ -5120,6 +5151,7 @@ var BlockRenderer = class _BlockRenderer {
5120
5151
  }
5121
5152
  this.applyBottomAlignmentAndScroll();
5122
5153
  this.renderCallback();
5154
+ this.notifyHasContentChange();
5123
5155
  }
5124
5156
  /**
5125
5157
  * Render a node and its children recursively.
@@ -5265,8 +5297,8 @@ ${fullContent}
5265
5297
  case "system_message": {
5266
5298
  const icon = this.getSystemMessageIcon(node.category);
5267
5299
  const color = this.getSystemMessageColor(node.category);
5268
- const RESET2 = "\x1B[0m";
5269
- return `${indent}${color}${icon} ${node.message}${RESET2}`;
5300
+ const RESET3 = "\x1B[0m";
5301
+ return `${indent}${color}${icon} ${node.message}${RESET3}`;
5270
5302
  }
5271
5303
  }
5272
5304
  }
@@ -5293,7 +5325,7 @@ ${fullContent}
5293
5325
  getSystemMessageColor(category) {
5294
5326
  const YELLOW2 = "\x1B[33m";
5295
5327
  const BLUE = "\x1B[34m";
5296
- const GRAY = "\x1B[90m";
5328
+ const GRAY2 = "\x1B[90m";
5297
5329
  const RED2 = "\x1B[31m";
5298
5330
  switch (category) {
5299
5331
  case "throttle":
@@ -5301,7 +5333,7 @@ ${fullContent}
5301
5333
  case "retry":
5302
5334
  return BLUE;
5303
5335
  case "info":
5304
- return GRAY;
5336
+ return GRAY2;
5305
5337
  case "warning":
5306
5338
  return YELLOW2;
5307
5339
  case "error":
@@ -5575,6 +5607,7 @@ ${indicator}`;
5575
5607
  }
5576
5608
  this.applyBottomAlignmentAndScroll();
5577
5609
  this.renderNowCallback();
5610
+ this.notifyHasContentChange();
5578
5611
  }
5579
5612
  /**
5580
5613
  * Get the current content filter mode.
@@ -6032,9 +6065,129 @@ var TUIController = class {
6032
6065
  }
6033
6066
  };
6034
6067
 
6068
+ // src/tui/hints-bar.ts
6069
+ var GRAY = "\x1B[90m";
6070
+ var RESET = "\x1B[0m";
6071
+ var HintsBar = class {
6072
+ hintsBox;
6073
+ renderCallback;
6074
+ focusMode = "browse";
6075
+ contentFilterMode = "full";
6076
+ hasContent = false;
6077
+ constructor(hintsBox, renderCallback) {
6078
+ this.hintsBox = hintsBox;
6079
+ this.renderCallback = renderCallback;
6080
+ this.render();
6081
+ }
6082
+ /**
6083
+ * Update focus mode and re-render hints.
6084
+ */
6085
+ setFocusMode(mode) {
6086
+ if (this.focusMode !== mode) {
6087
+ this.focusMode = mode;
6088
+ this.render();
6089
+ }
6090
+ }
6091
+ /**
6092
+ * Update content filter mode and re-render hints.
6093
+ */
6094
+ setContentFilterMode(mode) {
6095
+ if (this.contentFilterMode !== mode) {
6096
+ this.contentFilterMode = mode;
6097
+ this.render();
6098
+ }
6099
+ }
6100
+ /**
6101
+ * Update whether there's content to browse.
6102
+ * Affects whether ^B browse hint is shown in input mode.
6103
+ */
6104
+ setHasContent(has) {
6105
+ if (this.hasContent !== has) {
6106
+ this.hasContent = has;
6107
+ this.render();
6108
+ }
6109
+ }
6110
+ /**
6111
+ * Get current focus mode.
6112
+ */
6113
+ getFocusMode() {
6114
+ return this.focusMode;
6115
+ }
6116
+ /**
6117
+ * Get current content filter mode.
6118
+ */
6119
+ getContentFilterMode() {
6120
+ return this.contentFilterMode;
6121
+ }
6122
+ /**
6123
+ * Render hints based on current state.
6124
+ */
6125
+ render() {
6126
+ const hints = [];
6127
+ if (this.contentFilterMode === "focused") {
6128
+ hints.push("^K exit focused mode");
6129
+ } else if (this.focusMode === "input") {
6130
+ hints.push("^S multiline");
6131
+ if (this.hasContent) {
6132
+ hints.push("^B browse");
6133
+ }
6134
+ hints.push("^K focused");
6135
+ } else {
6136
+ hints.push("j/k nav");
6137
+ hints.push("Enter expand");
6138
+ hints.push("^B input");
6139
+ hints.push("^K focused");
6140
+ }
6141
+ this.hintsBox.setContent(`${GRAY}${hints.join(" ")}${RESET}`);
6142
+ this.renderCallback();
6143
+ }
6144
+ };
6145
+
6146
+ // src/tui/editor.ts
6147
+ import { spawnSync as spawnSync2 } from "child_process";
6148
+ import { readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
6149
+ import { tmpdir } from "os";
6150
+ import { join as join3 } from "path";
6151
+ function openEditorSync(initialContent = "") {
6152
+ const editor = process.env.VISUAL || process.env.EDITOR || "vi";
6153
+ const tmpFile = join3(tmpdir(), `llmist-input-${Date.now()}.txt`);
6154
+ writeFileSync2(tmpFile, initialContent, "utf-8");
6155
+ try {
6156
+ const parts = editor.split(/\s+/);
6157
+ const cmd = parts[0];
6158
+ const args = [...parts.slice(1), tmpFile];
6159
+ const result = spawnSync2(cmd, args, {
6160
+ stdio: "inherit",
6161
+ // Connect to user's terminal
6162
+ shell: false
6163
+ });
6164
+ if (result.error) {
6165
+ try {
6166
+ unlinkSync(tmpFile);
6167
+ } catch {
6168
+ }
6169
+ return null;
6170
+ }
6171
+ if (result.status === 0) {
6172
+ const content = readFileSync4(tmpFile, "utf-8");
6173
+ unlinkSync(tmpFile);
6174
+ const trimmed = content.trim();
6175
+ return trimmed || null;
6176
+ } else {
6177
+ unlinkSync(tmpFile);
6178
+ return null;
6179
+ }
6180
+ } catch {
6181
+ try {
6182
+ unlinkSync(tmpFile);
6183
+ } catch {
6184
+ }
6185
+ return null;
6186
+ }
6187
+ }
6188
+
6035
6189
  // src/tui/input-handler.ts
6036
- var IDLE_PROMPT = "> ";
6037
- var ACTIVE_PROMPT = ">>> ";
6190
+ var PROMPT = "> ";
6038
6191
  var InputHandler = class {
6039
6192
  inputBar;
6040
6193
  promptLabel;
@@ -6046,6 +6199,14 @@ var InputHandler = class {
6046
6199
  pendingInput = null;
6047
6200
  /** Whether we're waiting for REPL prompt (vs AskUser which should auto-focus) */
6048
6201
  isPendingREPLPrompt = false;
6202
+ /** Whether input mode is currently active (focused, capturing keystrokes) */
6203
+ isActive = false;
6204
+ /** Whether a bracketed paste is in progress */
6205
+ isPasting = false;
6206
+ /** Buffer for accumulating bracketed paste content */
6207
+ pasteBuffer = "";
6208
+ /** Flag to indicate content came from editor (skip paste detection on submit) */
6209
+ fromEditor = false;
6049
6210
  /** Callback when Ctrl+C is pressed */
6050
6211
  ctrlCCallback = null;
6051
6212
  /** Callback when Ctrl+B is pressed (toggle focus mode) */
@@ -6062,16 +6223,23 @@ var InputHandler = class {
6062
6223
  midSessionHandler = null;
6063
6224
  /** Callback to check current focus mode (to avoid conflicts with browse mode) */
6064
6225
  getFocusModeCallback = null;
6065
- constructor(inputBar, promptLabel, body, screen, renderCallback, renderNowCallback) {
6226
+ /** Body height when input bar is visible */
6227
+ bodyHeightWithInput;
6228
+ /** Body height when input bar is hidden (browse mode) */
6229
+ bodyHeightWithoutInput;
6230
+ constructor(inputBar, promptLabel, body, screen, renderCallback, renderNowCallback, hasHints = true) {
6066
6231
  this.inputBar = inputBar;
6067
6232
  this.promptLabel = promptLabel;
6068
6233
  this.body = body;
6069
6234
  this.screen = screen;
6070
6235
  this.renderCallback = renderCallback;
6071
6236
  this.renderNowCallback = renderNowCallback ?? renderCallback;
6237
+ this.bodyHeightWithInput = hasHints ? "100%-3" : "100%-2";
6238
+ this.bodyHeightWithoutInput = hasHints ? "100%-2" : "100%-1";
6072
6239
  this.inputBar.on("submit", (value) => {
6073
6240
  this.handleSubmit(value);
6074
6241
  });
6242
+ this.setupBracketedPasteHandler();
6075
6243
  this.inputBar.on("cancel", () => {
6076
6244
  this.handleCancel();
6077
6245
  });
@@ -6105,6 +6273,10 @@ var InputHandler = class {
6105
6273
  this.ctrlPCallback();
6106
6274
  }
6107
6275
  });
6276
+ this.inputBar.key(["C-s"], () => {
6277
+ const currentValue = this.inputBar.getValue();
6278
+ this.openEditorForInput(currentValue);
6279
+ });
6108
6280
  this.screen.key(["enter"], () => {
6109
6281
  if (this.isPendingREPLPrompt) {
6110
6282
  if (this.getFocusModeCallback?.() === "browse") {
@@ -6242,37 +6414,54 @@ var InputHandler = class {
6242
6414
  // Focus Mode API (controlled by TUIApp)
6243
6415
  // ─────────────────────────────────────────────────────────────────────────────
6244
6416
  /**
6245
- * Activate input mode - show input bar and capture keyboard.
6417
+ * Activate input mode - focus input bar and capture keyboard.
6246
6418
  * Called by TUIApp when switching to input mode.
6247
- * Preserves current prompt indicator and input text.
6419
+ * Shows input bar with active prompt (">>>") and starts capturing keystrokes.
6248
6420
  */
6249
6421
  activate() {
6250
6422
  this.isPendingREPLPrompt = false;
6423
+ this.isActive = true;
6251
6424
  this.promptLabel.show();
6252
6425
  this.inputBar.show();
6426
+ this.body.height = this.bodyHeightWithInput;
6427
+ this.setPrompt(PROMPT);
6253
6428
  this.renderNowCallback();
6254
6429
  this.inputBar.readInput();
6255
6430
  }
6431
+ /** Flag to prevent handleCancel from re-entering during deactivation */
6432
+ isDeactivating = false;
6256
6433
  /**
6257
6434
  * Deactivate input mode - hide input bar completely.
6258
6435
  * Called by TUIApp when switching to browse mode.
6436
+ * Input bar is hidden to give more space to content.
6259
6437
  */
6260
6438
  deactivate() {
6439
+ this.isPendingREPLPrompt = false;
6440
+ this.isActive = false;
6441
+ this.isDeactivating = true;
6442
+ this.inputBar.cancel();
6443
+ this.isDeactivating = false;
6261
6444
  this.promptLabel.hide();
6262
6445
  this.inputBar.hide();
6263
- this.isPendingREPLPrompt = false;
6446
+ this.body.height = this.bodyHeightWithoutInput;
6264
6447
  this.renderNowCallback();
6265
6448
  }
6266
6449
  /**
6267
- * Check if input mode is active (input bar visible and focused).
6450
+ * Check if input mode is active (focused, capturing keystrokes).
6268
6451
  */
6269
6452
  isInputActive() {
6270
- return this.inputBar.visible !== false;
6453
+ return this.isActive;
6271
6454
  }
6272
6455
  /**
6273
6456
  * Handle input submission.
6274
6457
  */
6275
6458
  handleSubmit(rawValue) {
6459
+ if (this.isPasting) {
6460
+ return;
6461
+ }
6462
+ if (this.fromEditor) {
6463
+ this.fromEditor = false;
6464
+ }
6276
6465
  const value = rawValue.trim();
6277
6466
  if (!value) {
6278
6467
  this.inputBar.readInput();
@@ -6294,18 +6483,93 @@ var InputHandler = class {
6294
6483
  * Handle input cancellation (ESC key).
6295
6484
  */
6296
6485
  handleCancel() {
6486
+ if (this.isDeactivating) {
6487
+ return;
6488
+ }
6297
6489
  if (this.pendingInput) {
6298
6490
  this.inputBar.readInput();
6299
6491
  } else {
6300
6492
  this.setIdle();
6301
6493
  }
6302
6494
  }
6495
+ /**
6496
+ * Set up bracketed paste mode detection.
6497
+ *
6498
+ * Terminal emulators that support bracketed paste send:
6499
+ * - \x1b[200~ before pasted content
6500
+ * - \x1b[201~ after pasted content
6501
+ *
6502
+ * This allows reliable detection of paste vs typed input.
6503
+ */
6504
+ setupBracketedPasteHandler() {
6505
+ const PASTE_START = "\x1B[200~";
6506
+ const PASTE_END = "\x1B[201~";
6507
+ this.screen.program.input.on("data", (data) => {
6508
+ const str = data.toString();
6509
+ if (str.includes(PASTE_START)) {
6510
+ this.isPasting = true;
6511
+ this.pasteBuffer = "";
6512
+ const startIdx = str.indexOf(PASTE_START) + PASTE_START.length;
6513
+ const afterStart = str.slice(startIdx);
6514
+ if (afterStart.includes(PASTE_END)) {
6515
+ const endIdx = afterStart.indexOf(PASTE_END);
6516
+ this.pasteBuffer = afterStart.slice(0, endIdx);
6517
+ this.isPasting = false;
6518
+ this.handlePaste(this.pasteBuffer);
6519
+ this.pasteBuffer = "";
6520
+ } else {
6521
+ this.pasteBuffer = afterStart;
6522
+ }
6523
+ return;
6524
+ }
6525
+ if (this.isPasting && str.includes(PASTE_END)) {
6526
+ const endIdx = str.indexOf(PASTE_END);
6527
+ this.pasteBuffer += str.slice(0, endIdx);
6528
+ this.isPasting = false;
6529
+ this.handlePaste(this.pasteBuffer);
6530
+ this.pasteBuffer = "";
6531
+ return;
6532
+ }
6533
+ if (this.isPasting) {
6534
+ this.pasteBuffer += str;
6535
+ }
6536
+ });
6537
+ }
6538
+ /**
6539
+ * Handle completed paste content.
6540
+ *
6541
+ * If content contains newlines, opens $EDITOR for multiline editing.
6542
+ * Otherwise, inserts directly into the input bar.
6543
+ */
6544
+ handlePaste(content) {
6545
+ if (!content) return;
6546
+ if (content.includes("\n")) {
6547
+ const currentValue = this.inputBar.getValue();
6548
+ this.openEditorForInput(currentValue + content);
6549
+ } else {
6550
+ const currentValue = this.inputBar.getValue();
6551
+ this.inputBar.setValue(currentValue + content);
6552
+ this.inputBar.readInput();
6553
+ }
6554
+ }
6555
+ /**
6556
+ * Set prompt text and dynamically adjust layout.
6557
+ * Idle prompt "> " uses 2 chars, active prompt ">>> " uses 4 chars.
6558
+ */
6559
+ setPrompt(prompt) {
6560
+ const width = prompt.length;
6561
+ this.promptLabel.width = width;
6562
+ this.promptLabel.setContent(prompt);
6563
+ this.inputBar.left = width;
6564
+ this.inputBar.width = `100%-${width}`;
6565
+ }
6303
6566
  /**
6304
6567
  * Set input to idle state.
6305
6568
  */
6306
6569
  setIdle() {
6307
6570
  this.isPendingREPLPrompt = false;
6308
- this.promptLabel.setContent(IDLE_PROMPT);
6571
+ this.isActive = false;
6572
+ this.setPrompt(PROMPT);
6309
6573
  this.inputBar.setValue("");
6310
6574
  this.renderCallback();
6311
6575
  }
@@ -6323,7 +6587,7 @@ var InputHandler = class {
6323
6587
  */
6324
6588
  setPendingPrompt() {
6325
6589
  this.isPendingREPLPrompt = true;
6326
- this.promptLabel.setContent(IDLE_PROMPT);
6590
+ this.setPrompt(PROMPT);
6327
6591
  this.inputBar.setValue("");
6328
6592
  this.renderCallback();
6329
6593
  }
@@ -6332,11 +6596,42 @@ var InputHandler = class {
6332
6596
  */
6333
6597
  setActive() {
6334
6598
  this.isPendingREPLPrompt = false;
6335
- this.promptLabel.setContent(ACTIVE_PROMPT);
6599
+ this.isActive = true;
6600
+ this.promptLabel.show();
6601
+ this.inputBar.show();
6602
+ this.body.height = this.bodyHeightWithInput;
6603
+ this.setPrompt(PROMPT);
6336
6604
  this.inputBar.setValue("");
6337
6605
  this.renderNowCallback();
6338
6606
  this.inputBar.readInput();
6339
6607
  }
6608
+ /**
6609
+ * Open $EDITOR for multiline input.
6610
+ * Called when user presses Ctrl+S or pastes multiline content.
6611
+ */
6612
+ openEditorForInput(initialContent) {
6613
+ this.screen.program.clear();
6614
+ this.screen.program.disableMouse();
6615
+ this.screen.program.showCursor();
6616
+ this.screen.program.normalBuffer();
6617
+ const result = openEditorSync(initialContent);
6618
+ this.screen.program.alternateBuffer();
6619
+ this.screen.program.hideCursor();
6620
+ this.screen.program.enableMouse();
6621
+ this.screen.alloc();
6622
+ this.screen.render();
6623
+ this.isPasting = false;
6624
+ this.pasteBuffer = "";
6625
+ setImmediate(() => {
6626
+ if (result) {
6627
+ this.fromEditor = true;
6628
+ this.handleSubmit(result);
6629
+ } else {
6630
+ this.inputBar.setValue(initialContent);
6631
+ this.inputBar.readInput();
6632
+ }
6633
+ });
6634
+ }
6340
6635
  };
6341
6636
 
6342
6637
  // src/tui/keymap.ts
@@ -6442,13 +6737,16 @@ var KeyboardManager = class {
6442
6737
 
6443
6738
  // src/tui/layout.ts
6444
6739
  import { Box as Box2, ScrollableBox, Text, Textbox } from "@unblessed/node";
6445
- function createBlockLayout(screen) {
6740
+ function createBlockLayout(screen, showHints = true) {
6741
+ const inputBottom = showHints ? 2 : 1;
6742
+ const statusBottom = showHints ? 1 : 0;
6743
+ const bodyHeight = showHints ? "100%-3" : "100%-2";
6446
6744
  const body = new ScrollableBox({
6447
6745
  parent: screen,
6448
6746
  top: 0,
6449
6747
  left: 0,
6450
6748
  width: "100%",
6451
- height: "100%-2",
6749
+ height: bodyHeight,
6452
6750
  // Scrolling configuration
6453
6751
  scrollable: true,
6454
6752
  alwaysScroll: true,
@@ -6471,10 +6769,10 @@ function createBlockLayout(screen) {
6471
6769
  });
6472
6770
  const promptLabel = new Text({
6473
6771
  parent: screen,
6474
- bottom: 1,
6772
+ bottom: inputBottom,
6475
6773
  left: 0,
6476
- width: 4,
6477
- // ">>> " = 4 chars (max prompt width)
6774
+ width: 2,
6775
+ // "> " = 2 chars
6478
6776
  height: 1,
6479
6777
  content: "> ",
6480
6778
  style: {
@@ -6484,10 +6782,10 @@ function createBlockLayout(screen) {
6484
6782
  });
6485
6783
  const inputBar = new Textbox({
6486
6784
  parent: screen,
6487
- bottom: 1,
6488
- left: 4,
6489
- // Position after prompt label
6490
- width: "100%-4",
6785
+ bottom: inputBottom,
6786
+ left: 2,
6787
+ // Position after prompt label ("> " = 2 chars)
6788
+ width: "100%-2",
6491
6789
  height: 1,
6492
6790
  keys: true,
6493
6791
  mouse: true,
@@ -6498,7 +6796,7 @@ function createBlockLayout(screen) {
6498
6796
  });
6499
6797
  const statusBar = new Box2({
6500
6798
  parent: screen,
6501
- bottom: 0,
6799
+ bottom: statusBottom,
6502
6800
  left: 0,
6503
6801
  width: "100%",
6504
6802
  height: 1,
@@ -6508,7 +6806,19 @@ function createBlockLayout(screen) {
6508
6806
  bg: "black"
6509
6807
  }
6510
6808
  });
6511
- return { body, promptLabel, inputBar, statusBar };
6809
+ const hintsBar = showHints ? new Box2({
6810
+ parent: screen,
6811
+ bottom: 0,
6812
+ left: 0,
6813
+ width: "100%",
6814
+ height: 1,
6815
+ tags: false,
6816
+ style: {
6817
+ fg: "white",
6818
+ bg: "black"
6819
+ }
6820
+ }) : null;
6821
+ return { body, promptLabel, inputBar, statusBar, hintsBar };
6512
6822
  }
6513
6823
 
6514
6824
  // src/tui/approval-dialog.ts
@@ -6629,7 +6939,7 @@ function escapeContent(str) {
6629
6939
 
6630
6940
  // src/tui/raw-viewer.ts
6631
6941
  import { Box as Box4 } from "@unblessed/node";
6632
- var RESET = "\x1B[0m";
6942
+ var RESET2 = "\x1B[0m";
6633
6943
  var BOLD = "\x1B[1m";
6634
6944
  var DIM = "\x1B[2m";
6635
6945
  var RED = "\x1B[31m";
@@ -6661,17 +6971,17 @@ function showRawViewer(options) {
6661
6971
  if (mode === "request") {
6662
6972
  title = ` Raw Parameters - ${gadgetName} `;
6663
6973
  if (!parameters || Object.keys(parameters).length === 0) {
6664
- content = `${DIM}No parameters${RESET}`;
6974
+ content = `${DIM}No parameters${RESET2}`;
6665
6975
  } else {
6666
6976
  content = formatGadgetParameters(parameters);
6667
6977
  }
6668
6978
  } else {
6669
6979
  title = ` Raw Result - ${gadgetName} `;
6670
6980
  if (error) {
6671
- content = `${RED}${BOLD}Error:${RESET}
6981
+ content = `${RED}${BOLD}Error:${RESET2}
6672
6982
  ${error}`;
6673
6983
  } else if (!result) {
6674
- content = `${DIM}No result data available${RESET}`;
6984
+ content = `${DIM}No result data available${RESET2}`;
6675
6985
  } else {
6676
6986
  content = formatGadgetResult(result);
6677
6987
  }
@@ -6680,14 +6990,14 @@ ${error}`;
6680
6990
  if (mode === "request") {
6681
6991
  title = ` Raw Request - #${iteration} ${model} `;
6682
6992
  if (!request || request.length === 0) {
6683
- content = `${DIM}No request data available${RESET}`;
6993
+ content = `${DIM}No request data available${RESET2}`;
6684
6994
  } else {
6685
6995
  content = formatMessages(request);
6686
6996
  }
6687
6997
  } else {
6688
6998
  title = ` Raw Response - #${iteration} ${model} `;
6689
6999
  if (!response) {
6690
- content = `${DIM}No response data available${RESET}`;
7000
+ content = `${DIM}No response data available${RESET2}`;
6691
7001
  } else {
6692
7002
  content = response;
6693
7003
  }
@@ -6728,7 +7038,7 @@ ${error}`;
6728
7038
  left: 0,
6729
7039
  width: "100%",
6730
7040
  height: 1,
6731
- content: `${DIM} [${WHITE}\u2191/\u2193/PgUp/PgDn${DIM}] Scroll [${WHITE}Home/End${DIM}] Jump [${WHITE}Escape/q${DIM}] Close${RESET}`,
7041
+ content: `${DIM} [${WHITE}\u2191/\u2193/PgUp/PgDn${DIM}] Scroll [${WHITE}Home/End${DIM}] Jump [${WHITE}Escape/q${DIM}] Close${RESET2}`,
6732
7042
  tags: false,
6733
7043
  style: { fg: "white", bg: "black" }
6734
7044
  });
@@ -6760,11 +7070,11 @@ function formatMessages(messages) {
6760
7070
  const msg = messages[i];
6761
7071
  const roleColor = getRoleColor(msg.role);
6762
7072
  const roleName = msg.role.toUpperCase();
6763
- lines.push(`${DIM}${separator}${RESET}`);
7073
+ lines.push(`${DIM}${separator}${RESET2}`);
6764
7074
  lines.push(
6765
- `${roleColor}${BOLD}[${roleName}]${RESET} ${DIM}Message ${i + 1} of ${messages.length}${RESET}`
7075
+ `${roleColor}${BOLD}[${roleName}]${RESET2} ${DIM}Message ${i + 1} of ${messages.length}${RESET2}`
6766
7076
  );
6767
- lines.push(`${DIM}${separator}${RESET}`);
7077
+ lines.push(`${DIM}${separator}${RESET2}`);
6768
7078
  lines.push("");
6769
7079
  const contentLines = formatMessageContent(msg.content);
6770
7080
  lines.push(...contentLines);
@@ -6787,19 +7097,19 @@ function formatMessageContent(content) {
6787
7097
  lines.push(...part.text.split("\n"));
6788
7098
  } else if (isImagePart(part)) {
6789
7099
  const mediaType = part.source?.media_type || "unknown";
6790
- lines.push(`${DIM}[Image: ${mediaType}]${RESET}`);
7100
+ lines.push(`${DIM}[Image: ${mediaType}]${RESET2}`);
6791
7101
  } else if (isAudioPart(part)) {
6792
7102
  const mediaType = part.source?.media_type || "unknown";
6793
- lines.push(`${DIM}[Audio: ${mediaType}]${RESET}`);
7103
+ lines.push(`${DIM}[Audio: ${mediaType}]${RESET2}`);
6794
7104
  } else if (isToolUsePart(part)) {
6795
- lines.push(`${YELLOW}${BOLD}[Tool Use: ${part.name}]${RESET}`);
6796
- lines.push(`${DIM}ID: ${part.id}${RESET}`);
6797
- lines.push(`${DIM}Input:${RESET}`);
7105
+ lines.push(`${YELLOW}${BOLD}[Tool Use: ${part.name}]${RESET2}`);
7106
+ lines.push(`${DIM}ID: ${part.id}${RESET2}`);
7107
+ lines.push(`${DIM}Input:${RESET2}`);
6798
7108
  const inputStr = JSON.stringify(part.input, null, 2);
6799
7109
  lines.push(...inputStr.split("\n").map((l) => ` ${l}`));
6800
7110
  } else if (isToolResultPart(part)) {
6801
- lines.push(`${CYAN}${BOLD}[Tool Result]${RESET}`);
6802
- lines.push(`${DIM}Tool Use ID: ${part.tool_use_id}${RESET}`);
7111
+ lines.push(`${CYAN}${BOLD}[Tool Result]${RESET2}`);
7112
+ lines.push(`${DIM}Tool Use ID: ${part.tool_use_id}${RESET2}`);
6803
7113
  if (typeof part.content === "string") {
6804
7114
  lines.push(...part.content.split("\n"));
6805
7115
  } else {
@@ -6807,7 +7117,7 @@ function formatMessageContent(content) {
6807
7117
  }
6808
7118
  } else {
6809
7119
  const partType = part.type || "unknown";
6810
- lines.push(`${DIM}[${partType}]${RESET}`);
7120
+ lines.push(`${DIM}[${partType}]${RESET2}`);
6811
7121
  lines.push(JSON.stringify(part, null, 2));
6812
7122
  }
6813
7123
  }
@@ -6843,9 +7153,9 @@ function isToolResultPart(part) {
6843
7153
  function formatGadgetParameters(params) {
6844
7154
  const lines = [];
6845
7155
  const separator = "\u2500".repeat(78);
6846
- lines.push(`${DIM}${separator}${RESET}`);
6847
- lines.push(`${CYAN}${BOLD}Parameters${RESET}`);
6848
- lines.push(`${DIM}${separator}${RESET}`);
7156
+ lines.push(`${DIM}${separator}${RESET2}`);
7157
+ lines.push(`${CYAN}${BOLD}Parameters${RESET2}`);
7158
+ lines.push(`${DIM}${separator}${RESET2}`);
6849
7159
  lines.push("");
6850
7160
  const json = JSON.stringify(params, null, 2);
6851
7161
  const highlighted = highlightJson(json);
@@ -6855,9 +7165,9 @@ function formatGadgetParameters(params) {
6855
7165
  function formatGadgetResult(result) {
6856
7166
  const lines = [];
6857
7167
  const separator = "\u2500".repeat(78);
6858
- lines.push(`${DIM}${separator}${RESET}`);
6859
- lines.push(`${GREEN}${BOLD}Result${RESET}`);
6860
- lines.push(`${DIM}${separator}${RESET}`);
7168
+ lines.push(`${DIM}${separator}${RESET2}`);
7169
+ lines.push(`${GREEN}${BOLD}Result${RESET2}`);
7170
+ lines.push(`${DIM}${separator}${RESET2}`);
6861
7171
  lines.push("");
6862
7172
  const trimmed = result.trim();
6863
7173
  if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
@@ -6873,10 +7183,10 @@ function formatGadgetResult(result) {
6873
7183
  return lines.join("\n");
6874
7184
  }
6875
7185
  function highlightJson(json) {
6876
- let result = json.replace(/"([^"]+)":/g, `${CYAN}"$1"${RESET}:`);
6877
- result = result.replace(/: "([^"]*)"/g, `: ${GREEN}"$1"${RESET}`);
6878
- result = result.replace(/: (-?\d+\.?\d*)/g, `: ${YELLOW}$1${RESET}`);
6879
- result = result.replace(/: (true|false|null)/g, `: ${MAGENTA}$1${RESET}`);
7186
+ let result = json.replace(/"([^"]+)":/g, `${CYAN}"$1"${RESET2}:`);
7187
+ result = result.replace(/: "([^"]*)"/g, `: ${GREEN}"$1"${RESET2}`);
7188
+ result = result.replace(/: (-?\d+\.?\d*)/g, `: ${YELLOW}$1${RESET2}`);
7189
+ result = result.replace(/: (true|false|null)/g, `: ${MAGENTA}$1${RESET2}`);
6880
7190
  return result;
6881
7191
  }
6882
7192
 
@@ -6955,6 +7265,7 @@ function createScreen(options) {
6955
7265
  // Use alternate screen buffer (restores on exit)
6956
7266
  useBCE: true
6957
7267
  });
7268
+ screen.program.write("\x1B[?2004h");
6958
7269
  let isDestroyed = false;
6959
7270
  let renderPending = false;
6960
7271
  let renderTimeout = null;
@@ -6985,6 +7296,7 @@ function createScreen(options) {
6985
7296
  clearTimeout(renderTimeout);
6986
7297
  renderTimeout = null;
6987
7298
  }
7299
+ screen.program.write("\x1B[?2004l");
6988
7300
  screen.destroy();
6989
7301
  process.stdout.write("\x1B[?25h");
6990
7302
  };
@@ -7026,7 +7338,7 @@ var StatusBar = class {
7026
7338
  streamingOutputTokens = 0;
7027
7339
  /** Whether we're currently streaming */
7028
7340
  isStreaming = false;
7029
- /** Active LLM calls: Map from label ("#1") to model name */
7341
+ /** Active LLM calls: Map from label ("#1") to model name and start time */
7030
7342
  activeLLMCalls = /* @__PURE__ */ new Map();
7031
7343
  /** Active gadgets (by name) */
7032
7344
  activeGadgets = /* @__PURE__ */ new Set();
@@ -7123,7 +7435,7 @@ var StatusBar = class {
7123
7435
  * @param model - Full model name like "gemini:gemini-2.5-flash"
7124
7436
  */
7125
7437
  startLLMCall(label, model) {
7126
- this.activeLLMCalls.set(label, model);
7438
+ this.activeLLMCalls.set(label, { model, startTime: Date.now() });
7127
7439
  this.startSpinner();
7128
7440
  this.render();
7129
7441
  }
@@ -7136,6 +7448,19 @@ var StatusBar = class {
7136
7448
  this.maybeStopSpinner();
7137
7449
  this.render();
7138
7450
  }
7451
+ /**
7452
+ * Get the start time of the earliest running LLM call.
7453
+ * Used to show elapsed time since the first call started (for concurrent subagents).
7454
+ * @returns Start time in ms, or null if no LLM calls are active
7455
+ */
7456
+ getEarliestLLMCallStartTime() {
7457
+ if (this.activeLLMCalls.size === 0) return null;
7458
+ let earliest = Infinity;
7459
+ for (const { startTime } of this.activeLLMCalls.values()) {
7460
+ if (startTime < earliest) earliest = startTime;
7461
+ }
7462
+ return earliest;
7463
+ }
7139
7464
  /**
7140
7465
  * Track a gadget as active.
7141
7466
  * @param name - Gadget name like "ReadFile" or "BrowseWeb"
@@ -7267,6 +7592,15 @@ var StatusBar = class {
7267
7592
  this.startLLMCall(label, event.model);
7268
7593
  break;
7269
7594
  }
7595
+ case "llm_response_end": {
7596
+ const label = this.nodeIdToLabel.get(event.nodeId);
7597
+ if (label) {
7598
+ this.activeLLMCalls.delete(label);
7599
+ this.maybeStopSpinner();
7600
+ this.render();
7601
+ }
7602
+ break;
7603
+ }
7270
7604
  case "llm_call_complete": {
7271
7605
  const label = this.nodeIdToLabel.get(event.nodeId);
7272
7606
  if (label) {
@@ -7410,68 +7744,72 @@ var StatusBar = class {
7410
7744
  * @param immediate - If true, render immediately without debouncing
7411
7745
  */
7412
7746
  render(immediate = false) {
7413
- const elapsed = this.getElapsedSeconds().toFixed(1);
7414
7747
  const YELLOW2 = "\x1B[33m";
7415
7748
  const GREEN2 = "\x1B[32m";
7416
7749
  const BLUE = "\x1B[34m";
7417
7750
  const CYAN2 = "\x1B[36m";
7418
7751
  const MAGENTA2 = "\x1B[35m";
7419
- const GRAY = "\x1B[90m";
7420
- const RESET2 = "\x1B[0m";
7752
+ const GRAY2 = "\x1B[90m";
7753
+ const RESET3 = "\x1B[0m";
7421
7754
  const BG_BLUE = "\x1B[44m";
7422
- const BG_GREEN = "\x1B[42m";
7423
7755
  const WHITE2 = "\x1B[37m";
7424
- const BLACK = "\x1B[30m";
7425
7756
  const displayInputTokens = this.metrics.inputTokens + this.streamingInputTokens;
7426
7757
  const displayOutputTokens = this.metrics.outputTokens + this.streamingOutputTokens;
7427
7758
  const parts = [];
7428
- if (this.contentFilterMode === "focused") {
7429
- parts.push(`${BG_BLUE}${WHITE2} FOCUSED ${RESET2}`);
7430
- } else if (this.focusMode === "browse") {
7431
- parts.push(`${BG_BLUE}${WHITE2} BROWSE ${RESET2}`);
7432
- } else {
7433
- parts.push(`${BG_GREEN}${BLACK} INPUT ${RESET2}`);
7759
+ if (this.focusMode === "browse" && this.contentFilterMode !== "focused") {
7760
+ parts.push(`${BG_BLUE}${WHITE2} BROWSE ${RESET3}`);
7434
7761
  }
7435
7762
  if (this.profiles.length > 0) {
7436
7763
  const profile = this.profiles[this.currentProfileIndex];
7437
7764
  const display = profile.length > 12 ? `${profile.slice(0, 11)}\u2026` : profile;
7438
- parts.push(`${YELLOW2}${display}${RESET2}`);
7765
+ parts.push(`${YELLOW2}${display}${RESET3}`);
7766
+ }
7767
+ if (displayInputTokens > 0) {
7768
+ const inputPrefix = this.isStreaming && this.streamingInputTokens > 0 ? "~" : "";
7769
+ parts.push(`${YELLOW2}\u2191${inputPrefix}${formatTokens(displayInputTokens)}${RESET3}`);
7439
7770
  }
7440
- const inputPrefix = this.isStreaming && this.streamingInputTokens > 0 ? "~" : "";
7441
- parts.push(`${YELLOW2}\u2191 ${inputPrefix}${formatTokens(displayInputTokens)}${RESET2}`);
7442
7771
  if (this.metrics.cachedTokens > 0) {
7443
- parts.push(`${BLUE}\u293F ${formatTokens(this.metrics.cachedTokens)}${RESET2}`);
7772
+ parts.push(`${BLUE}\u293F${formatTokens(this.metrics.cachedTokens)}${RESET3}`);
7773
+ }
7774
+ if (displayOutputTokens > 0) {
7775
+ const outputPrefix = this.isStreaming ? "~" : "";
7776
+ parts.push(`${GREEN2}\u2193${outputPrefix}${formatTokens(displayOutputTokens)}${RESET3}`);
7777
+ }
7778
+ const earliestStart = this.getEarliestLLMCallStartTime();
7779
+ if (earliestStart !== null) {
7780
+ const elapsedSeconds = (Date.now() - earliestStart) / 1e3;
7781
+ const timeStr = elapsedSeconds % 1 === 0 ? `${elapsedSeconds}s` : `${elapsedSeconds.toFixed(1)}s`;
7782
+ parts.push(`${GRAY2}${timeStr}${RESET3}`);
7783
+ }
7784
+ if (this.metrics.cost > 0) {
7785
+ parts.push(`${CYAN2}$${formatCost(this.metrics.cost)}${RESET3}`);
7444
7786
  }
7445
- const outputPrefix = this.isStreaming ? "~" : "";
7446
- parts.push(`${GREEN2}\u2193 ${outputPrefix}${formatTokens(displayOutputTokens)}${RESET2}`);
7447
- parts.push(`${GRAY}${elapsed}s${RESET2}`);
7448
- parts.push(`${CYAN2}$${formatCost(this.metrics.cost)}${RESET2}`);
7449
7787
  if (this.selectionDebugCallback) {
7450
7788
  const debug = this.selectionDebugCallback();
7451
7789
  const debugStr = `sel:${debug.index}/${debug.total}`;
7452
7790
  const typeStr = debug.nodeType ? ` [${debug.nodeType}]` : "";
7453
- parts.push(`${GRAY}${debugStr}${typeStr}${RESET2}`);
7791
+ parts.push(`${GRAY2}${debugStr}${typeStr}${RESET3}`);
7454
7792
  }
7455
7793
  if (this.rateLimitState?.isThrottling) {
7456
7794
  const { triggeredBy } = this.rateLimitState;
7457
7795
  if (triggeredBy?.daily) {
7458
- parts.push(`${YELLOW2}\u23F8 Daily limit, resets midnight UTC${RESET2}`);
7796
+ parts.push(`${YELLOW2}\u23F8 Daily limit, resets midnight UTC${RESET3}`);
7459
7797
  } else {
7460
7798
  const seconds = Math.ceil(this.rateLimitState.delayMs / 1e3);
7461
7799
  const reason = triggeredBy?.rpm ? " (RPM)" : triggeredBy?.tpm ? " (TPM)" : "";
7462
- parts.push(`${YELLOW2}\u23F8 Throttled ${seconds}s${reason}${RESET2}`);
7800
+ parts.push(`${YELLOW2}\u23F8 Throttled ${seconds}s${reason}${RESET3}`);
7463
7801
  }
7464
7802
  }
7465
7803
  if (this.retryState) {
7466
7804
  const { attemptNumber, retriesLeft } = this.retryState;
7467
7805
  const totalAttempts = attemptNumber + retriesLeft;
7468
- parts.push(`${BLUE}\u{1F504} Retry ${attemptNumber}/${totalAttempts}${RESET2}`);
7806
+ parts.push(`${BLUE}\u{1F504} Retry ${attemptNumber}/${totalAttempts}${RESET3}`);
7469
7807
  }
7470
7808
  if (this.activeLLMCalls.size > 0 || this.activeGadgets.size > 0) {
7471
7809
  const spinner = SPINNER_FRAMES2[this.spinnerFrame];
7472
7810
  if (this.activeLLMCalls.size > 0) {
7473
7811
  const byModel = /* @__PURE__ */ new Map();
7474
- for (const [label, model] of this.activeLLMCalls) {
7812
+ for (const [label, { model }] of this.activeLLMCalls) {
7475
7813
  const shortModel = this.shortenModelName(model);
7476
7814
  if (!byModel.has(shortModel)) byModel.set(shortModel, []);
7477
7815
  byModel.get(shortModel)?.push(label);
@@ -7480,15 +7818,15 @@ var StatusBar = class {
7480
7818
  for (const [model, labels] of byModel) {
7481
7819
  llmParts.push(`${model} ${labels.join(", ")}`);
7482
7820
  }
7483
- parts.push(`${spinner} ${MAGENTA2}${llmParts.join(" | ")}${RESET2}`);
7821
+ parts.push(`${spinner} ${MAGENTA2}${llmParts.join(" | ")}${RESET3}`);
7484
7822
  }
7485
7823
  if (this.activeGadgets.size > 0) {
7486
7824
  const gadgetList = [...this.activeGadgets].slice(0, 3).join(", ");
7487
7825
  const more = this.activeGadgets.size > 3 ? ` +${this.activeGadgets.size - 3}` : "";
7488
- parts.push(`${CYAN2}\u23F5 ${gadgetList}${more}${RESET2}`);
7826
+ parts.push(`${CYAN2}\u23F5 ${gadgetList}${more}${RESET3}`);
7489
7827
  }
7490
7828
  }
7491
- this.statusBox.setContent(parts.join(` ${GRAY}|${RESET2} `));
7829
+ this.statusBox.setContent(parts.join(" "));
7492
7830
  if (immediate) {
7493
7831
  this.renderNowCallback();
7494
7832
  } else {
@@ -7534,7 +7872,12 @@ var TUIApp = class _TUIApp {
7534
7872
  title: "llmist"
7535
7873
  });
7536
7874
  const { screen } = screenCtx;
7537
- const layout = createBlockLayout(screen);
7875
+ const showHints = options.showHints ?? true;
7876
+ const layout = createBlockLayout(screen, showHints);
7877
+ let hintsBar = null;
7878
+ if (layout.hintsBar) {
7879
+ hintsBar = new HintsBar(layout.hintsBar, () => screenCtx.requestRender());
7880
+ }
7538
7881
  const statusBar = new StatusBar(
7539
7882
  layout.statusBar,
7540
7883
  options.model,
@@ -7547,19 +7890,27 @@ var TUIApp = class _TUIApp {
7547
7890
  layout.body,
7548
7891
  screen,
7549
7892
  () => screenCtx.requestRender(),
7550
- () => screenCtx.renderNow()
7893
+ () => screenCtx.renderNow(),
7894
+ showHints
7551
7895
  );
7552
7896
  const blockRenderer = new BlockRenderer(
7553
7897
  layout.body,
7554
7898
  () => screenCtx.requestRender(),
7555
7899
  () => screenCtx.renderNow()
7556
7900
  );
7901
+ if (hintsBar) {
7902
+ blockRenderer.onHasContentChange((hasContent) => {
7903
+ hintsBar.setHasContent(hasContent);
7904
+ });
7905
+ }
7557
7906
  const controller = new TUIController({
7558
7907
  onFocusModeChange: (mode) => {
7559
7908
  applyFocusMode(mode, layout, statusBar, inputHandler, screenCtx);
7909
+ hintsBar?.setFocusMode(mode);
7560
7910
  },
7561
7911
  onContentFilterModeChange: (mode) => {
7562
7912
  applyContentFilterMode(mode, blockRenderer, statusBar, screenCtx);
7913
+ hintsBar?.setContentFilterMode(mode);
7563
7914
  }
7564
7915
  });
7565
7916
  const modalManager = new ModalManager();
@@ -7713,13 +8064,11 @@ var TUIApp = class _TUIApp {
7713
8064
  }
7714
8065
  /**
7715
8066
  * Wait for user to enter a new prompt (REPL mode).
7716
- * Stays in current mode (browse) - user can Tab to input or Enter to start typing.
7717
- * After the prompt is submitted, focus mode switches to BROWSE.
8067
+ * Stays in input mode after submission - user can watch output and type next message.
8068
+ * User can Ctrl+B to browse if they want to navigate blocks.
7718
8069
  */
7719
8070
  async waitForPrompt() {
7720
- const result = await this.inputHandler.waitForPrompt();
7721
- this.controller.setFocusMode("browse");
7722
- return result;
8071
+ return this.inputHandler.waitForPrompt();
7723
8072
  }
7724
8073
  /**
7725
8074
  * Enter the pending REPL prompt state without blocking.
@@ -7938,17 +8287,13 @@ var TUIApp = class _TUIApp {
7938
8287
  };
7939
8288
  function applyFocusMode(mode, layout, statusBar, inputHandler, screenCtx) {
7940
8289
  statusBar.setFocusMode(mode);
7941
- if (mode === "input") {
7942
- layout.body.height = "100%-2";
7943
- } else {
7944
- layout.body.height = "100%-1";
7945
- }
7946
- screenCtx.renderNow();
7947
8290
  if (mode === "input") {
7948
8291
  inputHandler.activate();
7949
8292
  } else {
7950
8293
  inputHandler.deactivate();
8294
+ layout.body.focus();
7951
8295
  }
8296
+ screenCtx.renderNow();
7952
8297
  }
7953
8298
  function applyContentFilterMode(mode, blockRenderer, statusBar, screenCtx) {
7954
8299
  blockRenderer.setContentFilterMode(mode);
@@ -8078,7 +8423,8 @@ async function executeAgent(promptArg, options, env, commandName) {
8078
8423
  tui = await TUIApp.create({
8079
8424
  model: options.model,
8080
8425
  stdin: env.stdin,
8081
- stdout: env.stdout
8426
+ stdout: env.stdout,
8427
+ showHints: options.showHints
8082
8428
  });
8083
8429
  try {
8084
8430
  const fullConfig = loadConfig();
@@ -8467,7 +8813,8 @@ function registerAgentCommand(program, env, config, globalSubagents, globalRateL
8467
8813
  globalRateLimits,
8468
8814
  globalRetry,
8469
8815
  profileRateLimits: config?.["rate-limits"],
8470
- profileRetry: config?.retry
8816
+ profileRetry: config?.retry,
8817
+ showHints: config?.["show-hints"]
8471
8818
  };
8472
8819
  return executeAgent(prompt, mergedOptions, env, "agent");
8473
8820
  }, env)
@@ -8692,7 +9039,7 @@ System Prompt (${chars.toLocaleString()} chars, ${lines} lines):
8692
9039
  }
8693
9040
 
8694
9041
  // src/environment.ts
8695
- import { join as join3 } from "path";
9042
+ import { join as join4 } from "path";
8696
9043
  import readline from "readline";
8697
9044
  import chalk4 from "chalk";
8698
9045
  import { createLogger, LLMist } from "llmist";
@@ -8715,7 +9062,7 @@ function createLoggerFactory(config, sessionLogDir) {
8715
9062
  }
8716
9063
  }
8717
9064
  if (sessionLogDir) {
8718
- const logFile = join3(sessionLogDir, "session.log.jsonl");
9065
+ const logFile = join4(sessionLogDir, "session.log.jsonl");
8719
9066
  const originalLogFile = process.env.LLMIST_LOG_FILE;
8720
9067
  process.env.LLMIST_LOG_FILE = logFile;
8721
9068
  const logger = createLogger(options);
@@ -9261,7 +9608,7 @@ function registerGadgetCommand(program, env) {
9261
9608
  }
9262
9609
 
9263
9610
  // src/image-command.ts
9264
- import { writeFileSync as writeFileSync2 } from "fs";
9611
+ import { writeFileSync as writeFileSync3 } from "fs";
9265
9612
  var DEFAULT_IMAGE_MODEL = "dall-e-3";
9266
9613
  async function executeImage(promptArg, options, env) {
9267
9614
  const prompt = await resolvePrompt(promptArg, env);
@@ -9285,7 +9632,7 @@ async function executeImage(promptArg, options, env) {
9285
9632
  const imageData = result.images[0];
9286
9633
  if (imageData.b64Json) {
9287
9634
  const buffer = Buffer.from(imageData.b64Json, "base64");
9288
- writeFileSync2(options.output, buffer);
9635
+ writeFileSync3(options.output, buffer);
9289
9636
  if (!options.quiet) {
9290
9637
  env.stderr.write(`${SUMMARY_PREFIX} Image saved to ${options.output}
9291
9638
  `);
@@ -9324,7 +9671,7 @@ function registerImageCommand(program, env, config) {
9324
9671
  }
9325
9672
 
9326
9673
  // src/init-command.ts
9327
- import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3 } from "fs";
9674
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync4 } from "fs";
9328
9675
  import { dirname as dirname2 } from "path";
9329
9676
  var STARTER_CONFIG = `# ~/.llmist/cli.toml
9330
9677
  # llmist CLI configuration file
@@ -9395,7 +9742,7 @@ async function executeInit(_options, env) {
9395
9742
  if (!existsSync3(configDir)) {
9396
9743
  mkdirSync(configDir, { recursive: true });
9397
9744
  }
9398
- writeFileSync3(configPath, STARTER_CONFIG, "utf-8");
9745
+ writeFileSync4(configPath, STARTER_CONFIG, "utf-8");
9399
9746
  env.stderr.write(`Created ${configPath}
9400
9747
  `);
9401
9748
  env.stderr.write("\n");
@@ -9827,7 +10174,7 @@ function registerModelsCommand(program, env) {
9827
10174
  import { existsSync as existsSync4 } from "fs";
9828
10175
  import { mkdir as mkdir2 } from "fs/promises";
9829
10176
  import { homedir as homedir3 } from "os";
9830
- import { join as join4 } from "path";
10177
+ import { join as join5 } from "path";
9831
10178
 
9832
10179
  // src/session-names.ts
9833
10180
  var ADJECTIVES = [
@@ -9962,16 +10309,16 @@ function generateSessionName() {
9962
10309
 
9963
10310
  // src/session.ts
9964
10311
  var currentSession;
9965
- var SESSION_LOGS_BASE = join4(homedir3(), ".llmist", "logs");
10312
+ var SESSION_LOGS_BASE = join5(homedir3(), ".llmist", "logs");
9966
10313
  function findUniqueName(baseName) {
9967
- const baseDir = join4(SESSION_LOGS_BASE, baseName);
10314
+ const baseDir = join5(SESSION_LOGS_BASE, baseName);
9968
10315
  if (!existsSync4(baseDir)) {
9969
10316
  return baseName;
9970
10317
  }
9971
10318
  let suffix = 2;
9972
10319
  while (suffix < 1e3) {
9973
10320
  const name = `${baseName}-${suffix}`;
9974
- const dir = join4(SESSION_LOGS_BASE, name);
10321
+ const dir = join5(SESSION_LOGS_BASE, name);
9975
10322
  if (!existsSync4(dir)) {
9976
10323
  return name;
9977
10324
  }
@@ -9985,14 +10332,14 @@ async function initSession() {
9985
10332
  }
9986
10333
  const baseName = generateSessionName();
9987
10334
  const name = findUniqueName(baseName);
9988
- const logDir = join4(SESSION_LOGS_BASE, name);
10335
+ const logDir = join5(SESSION_LOGS_BASE, name);
9989
10336
  await mkdir2(logDir, { recursive: true });
9990
10337
  currentSession = { name, logDir };
9991
10338
  return currentSession;
9992
10339
  }
9993
10340
 
9994
10341
  // src/speech-command.ts
9995
- import { writeFileSync as writeFileSync4 } from "fs";
10342
+ import { writeFileSync as writeFileSync5 } from "fs";
9996
10343
  var DEFAULT_SPEECH_MODEL = "tts-1";
9997
10344
  var DEFAULT_VOICE = "nova";
9998
10345
  async function executeSpeech(textArg, options, env) {
@@ -10015,7 +10362,7 @@ async function executeSpeech(textArg, options, env) {
10015
10362
  });
10016
10363
  const audioBuffer = Buffer.from(result.audio);
10017
10364
  if (options.output) {
10018
- writeFileSync4(options.output, audioBuffer);
10365
+ writeFileSync5(options.output, audioBuffer);
10019
10366
  if (!options.quiet) {
10020
10367
  env.stderr.write(`${SUMMARY_PREFIX} Audio saved to ${options.output}
10021
10368
  `);