@hydra-acp/cli 0.1.6 → 0.1.7

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
@@ -32,6 +32,9 @@ var init_paths = __esm({
32
32
  paths = {
33
33
  home: hydraHome,
34
34
  config: () => path.join(hydraHome(), "config.json"),
35
+ // Auth token lives in its own file so config.json can be version-
36
+ // controlled without leaking the secret. Raw string contents, mode 0600.
37
+ authToken: () => path.join(hydraHome(), "auth-token"),
35
38
  pidFile: () => path.join(hydraHome(), "daemon.pid"),
36
39
  logFile: () => path.join(hydraHome(), "daemon.log"),
37
40
  currentLogFile: () => path.join(hydraHome(), "current.log"),
@@ -68,61 +71,95 @@ function extensionList(config) {
68
71
  ...body
69
72
  }));
70
73
  }
71
- async function loadConfig() {
72
- const configPath = paths.config();
74
+ async function readConfigFile() {
73
75
  let raw;
74
76
  try {
75
- raw = await fs.readFile(configPath, "utf8");
77
+ raw = await fs.readFile(paths.config(), "utf8");
76
78
  } catch (err) {
77
79
  const e = err;
78
80
  if (e.code === "ENOENT") {
79
- throw new Error(
80
- `No config found at ${configPath}. Run \`hydra-acp init\` to create one.`
81
- );
81
+ return {};
82
82
  }
83
83
  throw err;
84
84
  }
85
- const parsed = JSON.parse(raw);
86
- return HydraConfig.parse(parsed);
85
+ return JSON.parse(raw);
87
86
  }
88
- async function ensureConfig() {
87
+ async function loadAuthToken() {
88
+ let tokenFile;
89
89
  try {
90
- await fs.access(paths.config());
90
+ const text = await fs.readFile(paths.authToken(), "utf8");
91
+ const trimmed = text.trim();
92
+ if (trimmed.length > 0) {
93
+ tokenFile = trimmed;
94
+ }
91
95
  } catch (err) {
92
96
  const e = err;
93
97
  if (e.code !== "ENOENT") {
94
98
  throw err;
95
99
  }
96
- const config = await writeMinimalInitConfig();
97
- process.stderr.write(
98
- `hydra-acp: initialized ${paths.config()} with a fresh auth token.
99
- `
100
+ }
101
+ const raw = await readConfigFile();
102
+ const daemon = raw.daemon;
103
+ const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
104
+ if (tokenFile && legacy) {
105
+ throw new Error(
106
+ `Auth token present in both ${paths.authToken()} and ${paths.config()} (daemon.authToken). Remove daemon.authToken from config.json to resolve.`
100
107
  );
101
- return config;
102
108
  }
103
- return loadConfig();
109
+ if (tokenFile) {
110
+ return tokenFile;
111
+ }
112
+ if (legacy) {
113
+ await migrateLegacyAuthToken(raw, daemon, legacy);
114
+ return legacy;
115
+ }
116
+ return void 0;
104
117
  }
105
- async function writeMinimalInitConfig(authToken) {
106
- const token = authToken ?? generateAuthToken();
107
- const minimal = { daemon: { authToken: token } };
108
- await fs.mkdir(paths.home(), { recursive: true });
109
- await fs.writeFile(paths.config(), JSON.stringify(minimal, null, 2) + "\n", {
118
+ async function migrateLegacyAuthToken(raw, daemon, token) {
119
+ await writeAuthToken(token);
120
+ delete daemon.authToken;
121
+ if (Object.keys(daemon).length === 0) {
122
+ delete raw.daemon;
123
+ }
124
+ await fs.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
110
125
  encoding: "utf8",
111
126
  mode: 384
112
127
  });
113
- return HydraConfig.parse(minimal);
114
- }
115
- async function updateConfigField(mutate) {
116
- const path7 = paths.config();
117
- const text = await fs.readFile(path7, "utf8");
118
- const raw = JSON.parse(text);
119
- mutate(raw);
120
- HydraConfig.parse(raw);
121
- await fs.writeFile(path7, JSON.stringify(raw, null, 2) + "\n", {
128
+ process.stderr.write(
129
+ `hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
130
+ `
131
+ );
132
+ }
133
+ async function writeAuthToken(token) {
134
+ await fs.mkdir(paths.home(), { recursive: true });
135
+ await fs.writeFile(paths.authToken(), token + "\n", {
122
136
  encoding: "utf8",
123
137
  mode: 384
124
138
  });
125
139
  }
140
+ async function loadConfig() {
141
+ const token = await loadAuthToken();
142
+ if (!token) {
143
+ throw new Error(
144
+ `No auth token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
145
+ );
146
+ }
147
+ const raw = await readConfigFile();
148
+ const daemon = raw.daemon ??= {};
149
+ daemon.authToken = token;
150
+ return HydraConfig.parse(raw);
151
+ }
152
+ async function ensureConfig() {
153
+ if (!await loadAuthToken()) {
154
+ const token = generateAuthToken();
155
+ await writeAuthToken(token);
156
+ process.stderr.write(
157
+ `hydra-acp: initialized ${paths.authToken()} with a fresh auth token.
158
+ `
159
+ );
160
+ }
161
+ return loadConfig();
162
+ }
126
163
  function generateAuthToken() {
127
164
  const bytes = new Uint8Array(32);
128
165
  crypto.getRandomValues(bytes);
@@ -177,7 +214,13 @@ var init_config = __esm({
177
214
  // Cap on logical lines retained in the in-memory scrollback render
178
215
  // buffer. Oldest lines are dropped on overflow. The on-disk session
179
216
  // history is unaffected; this only bounds the TUI's local view buffer.
180
- maxScrollbackLines: z.number().int().positive().default(1e4)
217
+ maxScrollbackLines: z.number().int().positive().default(1e4),
218
+ // When true (default), the TUI captures mouse events so the wheel can
219
+ // drive scrollback. The cost: terminals route clicks to the app, so
220
+ // text selection requires shift+drag to bypass mouse reporting. Set
221
+ // false to disable capture — wheel scrollback stops working, but
222
+ // plain click-drag selects text via the terminal emulator.
223
+ mouse: z.boolean().default(true)
181
224
  });
182
225
  ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
183
226
  ExtensionBody = z.object({
@@ -210,7 +253,11 @@ var init_config = __esm({
210
253
  // recency and truncated to this count. `--all` overrides in the CLI.
211
254
  sessionListColdLimit: z.number().int().nonnegative().default(20),
212
255
  extensions: z.record(ExtensionName, ExtensionBody).default({}),
213
- tui: TuiConfig.default({ repaintThrottleMs: 1e3, maxScrollbackLines: 1e4 })
256
+ tui: TuiConfig.default({
257
+ repaintThrottleMs: 1e3,
258
+ maxScrollbackLines: 1e4,
259
+ mouse: true
260
+ })
214
261
  });
215
262
  }
216
263
  });
@@ -395,7 +442,7 @@ var init_connection = __esm({
395
442
  "src/acp/connection.ts"() {
396
443
  "use strict";
397
444
  init_types();
398
- JsonRpcConnection = class {
445
+ JsonRpcConnection = class _JsonRpcConnection {
399
446
  constructor(stream) {
400
447
  this.stream = stream;
401
448
  this.stream.onMessage((m) => this.handleIncoming(m));
@@ -405,6 +452,16 @@ var init_connection = __esm({
405
452
  requestHandlers = /* @__PURE__ */ new Map();
406
453
  defaultRequestHandler;
407
454
  notificationHandlers = /* @__PURE__ */ new Map();
455
+ // Notifications received before a handler was registered. Some agents
456
+ // (e.g. claude-acp) advertise their command list in the same chunk as
457
+ // the `session/new` response, which is processed before the consumer
458
+ // can attach its `session/update` handler. Without this buffer those
459
+ // notifications would be silently dropped, so e.g. `/model` would
460
+ // never appear in the TUI's slash-completion palette. Capped per
461
+ // method to keep the buffer from growing unboundedly when nothing
462
+ // ever subscribes.
463
+ bufferedNotifications = /* @__PURE__ */ new Map();
464
+ static MAX_BUFFERED_PER_METHOD = 64;
408
465
  pending = /* @__PURE__ */ new Map();
409
466
  closed = false;
410
467
  closeHandlers = [];
@@ -416,6 +473,17 @@ var init_connection = __esm({
416
473
  }
417
474
  onNotification(method, handler) {
418
475
  this.notificationHandlers.set(method, handler);
476
+ const queued = this.bufferedNotifications.get(method);
477
+ if (!queued) {
478
+ return;
479
+ }
480
+ this.bufferedNotifications.delete(method);
481
+ for (const note of queued) {
482
+ try {
483
+ handler(note.params, note.method);
484
+ } catch {
485
+ }
486
+ }
419
487
  }
420
488
  onClose(handler) {
421
489
  this.closeHandlers.push(handler);
@@ -501,6 +569,16 @@ var init_connection = __esm({
501
569
  const handler = this.notificationHandlers.get(note.method);
502
570
  if (handler) {
503
571
  handler(note.params, note.method);
572
+ return;
573
+ }
574
+ let queued = this.bufferedNotifications.get(note.method);
575
+ if (!queued) {
576
+ queued = [];
577
+ this.bufferedNotifications.set(note.method, queued);
578
+ }
579
+ queued.push(note);
580
+ if (queued.length > _JsonRpcConnection.MAX_BUFFERED_PER_METHOD) {
581
+ queued.shift();
504
582
  }
505
583
  }
506
584
  handleResponse(res) {
@@ -557,12 +635,12 @@ var init_hydra_commands = __esm({
557
635
  HYDRA_COMMANDS = [
558
636
  {
559
637
  verb: "title",
560
- name: "/hydra title",
638
+ name: "hydra title",
561
639
  description: "Regenerate the session title via the agent (or set manually with an arg)"
562
640
  },
563
641
  {
564
642
  verb: "agent",
565
- name: "/hydra agent",
643
+ name: "hydra agent",
566
644
  argsHint: "<agent>",
567
645
  description: "Swap the agent backing this session, preserving context"
568
646
  }
@@ -3437,6 +3515,7 @@ var init_screen = __esm({
3437
3515
  streamingActive = false;
3438
3516
  lastPromptRows = 0;
3439
3517
  queuedTexts = [];
3518
+ lastQueueEditingIndex = -1;
3440
3519
  repaintPaused = 0;
3441
3520
  repaintPending = false;
3442
3521
  lastRepaintAt = 0;
@@ -3493,12 +3572,14 @@ var init_screen = __esm({
3493
3572
  pasteActive = false;
3494
3573
  pasteBuffer = "";
3495
3574
  rawStdinHandler;
3575
+ mouseEnabled;
3496
3576
  constructor(opts) {
3497
3577
  this.term = opts.term;
3498
3578
  this.dispatcher = opts.dispatcher;
3499
3579
  this.onKey = opts.onKey;
3500
3580
  this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
3501
3581
  this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
3582
+ this.mouseEnabled = opts.mouse ?? true;
3502
3583
  this.resizeHandler = () => this.repaint();
3503
3584
  this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
3504
3585
  this.mouseHandler = (name) => this.handleMouse(name);
@@ -3515,10 +3596,16 @@ var init_screen = __esm({
3515
3596
  this.lastFrameH = 0;
3516
3597
  this.lastWindowTitle = null;
3517
3598
  process.stdout.write("\x1B[?7l");
3518
- this.term.grabInput({ mouse: "button" });
3599
+ if (this.mouseEnabled) {
3600
+ this.term.grabInput({ mouse: "button" });
3601
+ } else {
3602
+ this.term.grabInput(true);
3603
+ }
3519
3604
  this.term.hideCursor(false);
3520
3605
  this.term.on("key", this.keyHandler);
3521
- this.term.on("mouse", this.mouseHandler);
3606
+ if (this.mouseEnabled) {
3607
+ this.term.on("mouse", this.mouseHandler);
3608
+ }
3522
3609
  this.term.on("resize", this.resizeHandler);
3523
3610
  this.installBracketedPaste();
3524
3611
  this.repaint();
@@ -3530,7 +3617,9 @@ var init_screen = __esm({
3530
3617
  this.started = false;
3531
3618
  this.uninstallBracketedPaste();
3532
3619
  this.term.off("key", this.keyHandler);
3533
- this.term.off("mouse", this.mouseHandler);
3620
+ if (this.mouseEnabled) {
3621
+ this.term.off("mouse", this.mouseHandler);
3622
+ }
3534
3623
  this.term.off("resize", this.resizeHandler);
3535
3624
  this.term.grabInput(false);
3536
3625
  this.term.hideCursor(false);
@@ -3880,6 +3969,7 @@ var init_screen = __esm({
3880
3969
  }
3881
3970
  setQueuedPrompts(texts) {
3882
3971
  this.queuedTexts = [...texts];
3972
+ this.lastQueueEditingIndex = this.dispatcher.state().queueIndex;
3883
3973
  this.repaint();
3884
3974
  }
3885
3975
  // While a permission prompt is active, the prompt area is replaced with
@@ -3932,12 +4022,19 @@ var init_screen = __esm({
3932
4022
  // row count changed (alt+enter added a line, backspace joined two), the
3933
4023
  // separator and scrollback bottom shift, so we need a full repaint;
3934
4024
  // otherwise an in-place prompt redraw is enough. (Queued-zone changes
3935
- // already trigger a full repaint via setQueuedPrompts.)
4025
+ // already trigger a full repaint via setQueuedPrompts.) Queue-edit
4026
+ // navigation may also change which queued row is marked, so check
4027
+ // for that and redraw just that zone in-place.
3936
4028
  refreshPrompt() {
3937
4029
  if (this.promptRows() !== this.lastPromptRows) {
3938
4030
  this.repaint();
3939
4031
  return;
3940
4032
  }
4033
+ const editingIndex = this.dispatcher.state().queueIndex;
4034
+ if (editingIndex !== this.lastQueueEditingIndex) {
4035
+ this.lastQueueEditingIndex = editingIndex;
4036
+ this.drawQueuedZone();
4037
+ }
3941
4038
  this.drawPrompt();
3942
4039
  this.placeCursor();
3943
4040
  }
@@ -3988,6 +4085,14 @@ var init_screen = __esm({
3988
4085
  this.scrollOffset = 0;
3989
4086
  this.repaint();
3990
4087
  }
4088
+ scrollToTop() {
4089
+ const max = this.maxScrollOffset();
4090
+ if (this.scrollOffset === max) {
4091
+ return;
4092
+ }
4093
+ this.scrollOffset = max;
4094
+ this.repaint();
4095
+ }
3991
4096
  scrollPageSize() {
3992
4097
  return Math.max(1, this.scrollbackVisibleRows() - 2);
3993
4098
  }
@@ -4215,19 +4320,26 @@ var init_screen = __esm({
4215
4320
  const separatorRow = this.term.height - promptRows - BANNER_ROWS;
4216
4321
  const queuedBottom = separatorRow - 1;
4217
4322
  const queuedTop = queuedBottom - rows + 1;
4323
+ const editingIndex = this.dispatcher.state().queueIndex;
4218
4324
  for (let i = 0; i < rows; i++) {
4219
4325
  const row = queuedTop + i;
4220
4326
  const text = this.queuedTexts[i];
4221
4327
  const isLast = i === rows - 1 && this.queuedTexts.length > MAX_QUEUED_ROWS;
4222
4328
  const overflow = this.queuedTexts.length - MAX_QUEUED_ROWS;
4223
4329
  const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
4224
- const sig = text === void 0 ? `queued|${w}|empty` : `queued|${w}|${isLast ? "ovf" : "row"}|${summary}`;
4330
+ const editing = !isLast && i === editingIndex;
4331
+ const sig = text === void 0 ? `queued|${w}|empty` : `queued|${w}|${editing ? "edit" : isLast ? "ovf" : "row"}|${summary}`;
4225
4332
  this.paintRow(row, sig, () => {
4226
4333
  if (text === void 0) {
4227
4334
  return;
4228
4335
  }
4229
- const display = ` \u23F3 ${summary}`;
4230
- const padded = display + " ".repeat(Math.max(0, w - display.length));
4336
+ const rest = `\u23F3 ${summary}`;
4337
+ const padded = rest + " ".repeat(Math.max(0, w - 1 - rest.length));
4338
+ if (editing) {
4339
+ this.term.bgBlue.brightYellow("\u25B8");
4340
+ } else {
4341
+ this.term.bgBlue(" ");
4342
+ }
4231
4343
  this.term.bgBlue.brightWhite.noFormat(padded);
4232
4344
  });
4233
4345
  }
@@ -4533,8 +4645,17 @@ var init_input = __esm({
4533
4645
  col = 0;
4534
4646
  planMode = false;
4535
4647
  historyIndex = -1;
4648
+ // Queue editing: when the user walks Up past row 0 with queued prompts
4649
+ // present, the most-recently-queued item lands in the buffer and
4650
+ // queueIndex tracks which slot of `queue` is being edited. Enter submits
4651
+ // the edit (queue-edit) or, on an empty buffer, drops the slot
4652
+ // (queue-remove). -1 means not editing a queue slot.
4653
+ queueIndex = -1;
4536
4654
  savedDraft = null;
4537
4655
  history = [];
4656
+ // Waiting queue snapshot (excludes the in-flight head). Newest item lives
4657
+ // at the end so Up walks the array right-to-left.
4658
+ queue = [];
4538
4659
  turnRunning = false;
4539
4660
  // Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
4540
4661
  // here so ^Y can yank it back. Standard readline keeps a stack; we
@@ -4550,7 +4671,8 @@ var init_input = __esm({
4550
4671
  row: this.row,
4551
4672
  col: this.col,
4552
4673
  planMode: this.planMode,
4553
- historyIndex: this.historyIndex
4674
+ historyIndex: this.historyIndex,
4675
+ queueIndex: this.queueIndex
4554
4676
  };
4555
4677
  }
4556
4678
  setTurnRunning(running) {
@@ -4561,6 +4683,16 @@ var init_input = __esm({
4561
4683
  this.historyIndex = -1;
4562
4684
  this.savedDraft = null;
4563
4685
  }
4686
+ // Snapshot of the waiting queue (head excluded). Called by the app after
4687
+ // every queue mutation so Up/Down can walk a fresh view. queueIndex is
4688
+ // only invalidated when it falls outside the new bounds — staying in
4689
+ // bounds preserves the user's edit if the queue grew or stayed put.
4690
+ setQueue(queue) {
4691
+ this.queue = [...queue];
4692
+ if (this.queueIndex >= this.queue.length) {
4693
+ this.queueIndex = -1;
4694
+ }
4695
+ }
4564
4696
  // Replace the contents of the first row, leaving subsequent rows alone.
4565
4697
  // Used by slash-command completion: the partial /foo gets swapped for the
4566
4698
  // matched command name. Cursor moves to the end of the replacement.
@@ -4570,6 +4702,15 @@ var init_input = __esm({
4570
4702
  this.col = text.length;
4571
4703
  }
4572
4704
  }
4705
+ // Public seed for the buffer (used for Escape pre-fill). Treated like a
4706
+ // fresh draft: nav state and any saved draft are cleared, cursor lands
4707
+ // at the end so the user can edit immediately.
4708
+ setBuffer(text) {
4709
+ this.loadEntry(text);
4710
+ this.historyIndex = -1;
4711
+ this.queueIndex = -1;
4712
+ this.savedDraft = null;
4713
+ }
4573
4714
  feed(event) {
4574
4715
  if (event.type === "char") {
4575
4716
  this.insertChar(event.ch);
@@ -4607,14 +4748,16 @@ var init_input = __esm({
4607
4748
  case "right":
4608
4749
  this.moveRight();
4609
4750
  return [];
4610
- case "home":
4611
4751
  case "ctrl-a":
4612
4752
  this.col = 0;
4613
4753
  return [];
4614
- case "end":
4615
4754
  case "ctrl-e":
4616
4755
  this.col = this.currentLine().length;
4617
4756
  return [];
4757
+ case "home":
4758
+ return this.handleHome();
4759
+ case "end":
4760
+ return this.handleEnd();
4618
4761
  case "ctrl-b":
4619
4762
  this.moveLeft();
4620
4763
  return [];
@@ -4637,7 +4780,11 @@ var init_input = __esm({
4637
4780
  case "ctrl-c":
4638
4781
  return this.handleCtrlC();
4639
4782
  case "ctrl-d":
4640
- return this.bufferIsEmpty() ? [{ type: "exit" }] : [];
4783
+ if (this.bufferIsEmpty()) {
4784
+ return [{ type: "exit" }];
4785
+ }
4786
+ this.deleteForward();
4787
+ return [];
4641
4788
  case "ctrl-l":
4642
4789
  return [{ type: "redraw" }];
4643
4790
  case "ctrl-p":
@@ -4652,6 +4799,9 @@ var init_input = __esm({
4652
4799
  this.yank();
4653
4800
  return [];
4654
4801
  case "escape":
4802
+ if (this.turnRunning) {
4803
+ return [{ type: "cancel", prefill: true }];
4804
+ }
4655
4805
  return [];
4656
4806
  }
4657
4807
  }
@@ -4672,6 +4822,7 @@ var init_input = __esm({
4672
4822
  this.row = 0;
4673
4823
  this.col = 0;
4674
4824
  this.historyIndex = -1;
4825
+ this.queueIndex = -1;
4675
4826
  this.savedDraft = null;
4676
4827
  }
4677
4828
  insertChar(ch) {
@@ -4805,50 +4956,92 @@ var init_input = __esm({
4805
4956
  this.col = 0;
4806
4957
  }
4807
4958
  }
4808
- // Up scrolls back through history when the cursor is on the first line of
4809
- // the buffer; otherwise it just moves the cursor up one line.
4959
+ // Up walks the navigation stack from newest to oldest: pending queue
4960
+ // items first (so the user can edit something they just enqueued),
4961
+ // then prompt history. Cursor movement within a multi-line buffer
4962
+ // takes priority when not already navigating.
4810
4963
  handleUp() {
4811
4964
  if (this.row > 0) {
4812
4965
  this.row -= 1;
4813
4966
  this.col = Math.min(this.col, this.currentLine().length);
4814
4967
  return [];
4815
4968
  }
4816
- if (this.history.length === 0) {
4817
- return [];
4818
- }
4819
- if (this.historyIndex === -1) {
4969
+ if (this.queueIndex === -1 && this.historyIndex === -1) {
4970
+ if (this.queue.length === 0 && this.history.length === 0) {
4971
+ return [];
4972
+ }
4820
4973
  this.savedDraft = {
4821
4974
  buffer: [...this.buffer],
4822
4975
  row: this.row,
4823
4976
  col: this.col
4824
4977
  };
4978
+ if (this.queue.length > 0) {
4979
+ this.queueIndex = this.queue.length - 1;
4980
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
4981
+ } else {
4982
+ this.historyIndex = this.history.length - 1;
4983
+ this.loadEntry(this.history[this.historyIndex] ?? "");
4984
+ }
4985
+ return [];
4986
+ }
4987
+ if (this.queueIndex >= 0) {
4988
+ if (this.queueIndex > 0) {
4989
+ this.queueIndex -= 1;
4990
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
4991
+ return [];
4992
+ }
4993
+ if (this.history.length === 0) {
4994
+ return [];
4995
+ }
4996
+ this.queueIndex = -1;
4825
4997
  this.historyIndex = this.history.length - 1;
4826
- } else if (this.historyIndex > 0) {
4827
- this.historyIndex -= 1;
4828
- } else {
4998
+ this.loadEntry(this.history[this.historyIndex] ?? "");
4829
4999
  return [];
4830
5000
  }
4831
- this.loadHistoryEntry(this.historyIndex);
5001
+ if (this.historyIndex > 0) {
5002
+ this.historyIndex -= 1;
5003
+ this.loadEntry(this.history[this.historyIndex] ?? "");
5004
+ }
4832
5005
  return [];
4833
5006
  }
4834
- // Down advances within history; when we walk off the end, restore the
4835
- // saved draft. When already on a multi-line buffer's middle row, just
4836
- // moves the cursor down.
5007
+ // Down reverses the Up walk: history (older newer), then queue
5008
+ // (oldest newest), then restore the original draft. Within a
5009
+ // multi-line buffer, plain cursor movement still wins when no
5010
+ // navigation is in progress.
4837
5011
  handleDown() {
4838
- if (this.row < this.buffer.length - 1 && this.historyIndex === -1) {
5012
+ if (this.row < this.buffer.length - 1 && this.historyIndex === -1 && this.queueIndex === -1) {
4839
5013
  this.row += 1;
4840
5014
  this.col = Math.min(this.col, this.currentLine().length);
4841
5015
  return [];
4842
5016
  }
4843
- if (this.historyIndex === -1) {
5017
+ if (this.historyIndex >= 0) {
5018
+ if (this.historyIndex < this.history.length - 1) {
5019
+ this.historyIndex += 1;
5020
+ this.loadEntry(this.history[this.historyIndex] ?? "");
5021
+ return [];
5022
+ }
5023
+ this.historyIndex = -1;
5024
+ if (this.queue.length > 0) {
5025
+ this.queueIndex = 0;
5026
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
5027
+ return [];
5028
+ }
5029
+ this.restoreDraft();
4844
5030
  return [];
4845
5031
  }
4846
- if (this.historyIndex < this.history.length - 1) {
4847
- this.historyIndex += 1;
4848
- this.loadHistoryEntry(this.historyIndex);
5032
+ if (this.queueIndex >= 0) {
5033
+ if (this.queueIndex < this.queue.length - 1) {
5034
+ this.queueIndex += 1;
5035
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
5036
+ return [];
5037
+ }
5038
+ this.queueIndex = -1;
5039
+ this.restoreDraft();
4849
5040
  return [];
4850
5041
  }
4851
- this.historyIndex = -1;
5042
+ return [];
5043
+ }
5044
+ restoreDraft() {
4852
5045
  if (this.savedDraft) {
4853
5046
  this.buffer = [...this.savedDraft.buffer];
4854
5047
  this.row = this.savedDraft.row;
@@ -4857,11 +5050,9 @@ var init_input = __esm({
4857
5050
  } else {
4858
5051
  this.clearBuffer();
4859
5052
  }
4860
- return [];
4861
5053
  }
4862
- loadHistoryEntry(index) {
4863
- const entry = this.history[index] ?? "";
4864
- this.buffer = entry.split("\n");
5054
+ loadEntry(text) {
5055
+ this.buffer = text.split("\n");
4865
5056
  if (this.buffer.length === 0) {
4866
5057
  this.buffer = [""];
4867
5058
  }
@@ -4870,6 +5061,14 @@ var init_input = __esm({
4870
5061
  }
4871
5062
  send() {
4872
5063
  const text = this.bufferText();
5064
+ if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
5065
+ const index = this.queueIndex;
5066
+ this.clearBuffer();
5067
+ if (text.trim().length === 0) {
5068
+ return [{ type: "queue-remove", index }];
5069
+ }
5070
+ return [{ type: "queue-edit", index, text }];
5071
+ }
4873
5072
  if (text.trim().length === 0) {
4874
5073
  return [];
4875
5074
  }
@@ -4877,25 +5076,105 @@ var init_input = __esm({
4877
5076
  this.clearBuffer();
4878
5077
  return [{ type: "send", text, planMode }];
4879
5078
  }
4880
- handleCtrlC() {
4881
- if (this.turnRunning) {
4882
- return [{ type: "cancel" }];
5079
+ // Home: jump to the very start of the prompt buffer. If we're already
5080
+ // there, fall through to scrolling the scrollback to its top.
5081
+ handleHome() {
5082
+ if (this.row !== 0 || this.col !== 0) {
5083
+ this.row = 0;
5084
+ this.col = 0;
5085
+ return [];
5086
+ }
5087
+ return [{ type: "scroll-to-top" }];
5088
+ }
5089
+ // End: jump to the end of the last line of the prompt buffer. Already
5090
+ // there → scroll the scrollback to the bottom (newest).
5091
+ handleEnd() {
5092
+ const lastRow = this.buffer.length - 1;
5093
+ const lastCol = (this.buffer[lastRow] ?? "").length;
5094
+ if (this.row !== lastRow || this.col !== lastCol) {
5095
+ this.row = lastRow;
5096
+ this.col = lastCol;
5097
+ return [];
4883
5098
  }
5099
+ return [{ type: "scroll-to-bottom" }];
5100
+ }
5101
+ handleCtrlC() {
4884
5102
  if (!this.bufferIsEmpty()) {
4885
- this.clearBuffer();
5103
+ this.buffer = [""];
5104
+ this.row = 0;
5105
+ this.col = 0;
5106
+ if (this.queueIndex === -1) {
5107
+ this.historyIndex = -1;
5108
+ this.savedDraft = null;
5109
+ }
4886
5110
  return [];
4887
5111
  }
5112
+ if (this.queueIndex >= 0) {
5113
+ this.queueIndex = -1;
5114
+ this.restoreDraft();
5115
+ return [];
5116
+ }
5117
+ if (this.turnRunning) {
5118
+ return [{ type: "cancel" }];
5119
+ }
4888
5120
  return [{ type: "exit" }];
4889
5121
  }
4890
5122
  };
4891
5123
  }
4892
5124
  });
4893
5125
 
5126
+ // src/tui/completion.ts
5127
+ function longestCommonPrefix(names) {
5128
+ if (names.length === 0) {
5129
+ return "";
5130
+ }
5131
+ let prefix = names[0] ?? "";
5132
+ for (let i = 1; i < names.length; i++) {
5133
+ const n = names[i] ?? "";
5134
+ let j = 0;
5135
+ while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
5136
+ j += 1;
5137
+ }
5138
+ prefix = prefix.slice(0, j);
5139
+ if (prefix.length === 0) {
5140
+ break;
5141
+ }
5142
+ }
5143
+ return prefix;
5144
+ }
5145
+ function computeTabCompletion(args) {
5146
+ const { matches, firstLine: firstLine3 } = args;
5147
+ if (matches.length === 0) {
5148
+ return null;
5149
+ }
5150
+ const space = firstLine3.indexOf(" ");
5151
+ const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
5152
+ const tail = space === -1 ? "" : firstLine3.slice(space);
5153
+ if (matches.length === 1) {
5154
+ const name = matches[0] ?? "";
5155
+ const suffix = tail.startsWith(" ") ? "" : " ";
5156
+ return name + suffix + tail;
5157
+ }
5158
+ const commonPrefix = longestCommonPrefix(matches);
5159
+ if (commonPrefix.length <= typedPrefix.length) {
5160
+ return null;
5161
+ }
5162
+ return commonPrefix + tail;
5163
+ }
5164
+ var init_completion = __esm({
5165
+ "src/tui/completion.ts"() {
5166
+ "use strict";
5167
+ }
5168
+ });
5169
+
4894
5170
  // src/tui/render-update.ts
4895
5171
  import stripAnsi from "strip-ansi";
4896
5172
  function sanitizeWireText(text) {
4897
5173
  return stripAnsi(text).replace(STRIP_CONTROLS, "");
4898
5174
  }
5175
+ function sanitizeSingleLine(text) {
5176
+ return sanitizeWireText(text).replace(/[\n\t]+/g, " ").replace(/ +/g, " ").trim();
5177
+ }
4899
5178
  function mapUpdate(update) {
4900
5179
  if (!update || typeof update !== "object") {
4901
5180
  return null;
@@ -4939,7 +5218,7 @@ function mapUpdate(update) {
4939
5218
  }
4940
5219
  function mapSessionInfo(u) {
4941
5220
  const rawTitle = readString(u, "title");
4942
- const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
5221
+ const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
4943
5222
  const meta = u._meta;
4944
5223
  let agentId;
4945
5224
  if (meta && typeof meta === "object" && !Array.isArray(meta)) {
@@ -4963,10 +5242,9 @@ function mapSessionInfo(u) {
4963
5242
  }
4964
5243
  return event;
4965
5244
  }
4966
- function mapAvailableCommands(u) {
4967
- const list = u.availableCommands ?? u.commands;
5245
+ function normalizeAdvertisedCommands(list) {
4968
5246
  if (!Array.isArray(list)) {
4969
- return null;
5247
+ return [];
4970
5248
  }
4971
5249
  const out = [];
4972
5250
  for (const raw of list) {
@@ -4978,13 +5256,20 @@ function mapAvailableCommands(u) {
4978
5256
  continue;
4979
5257
  }
4980
5258
  const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
4981
- const cmd = { name: sanitizeWireText(rawName) };
5259
+ const cmd = { name: sanitizeSingleLine(rawName) };
4982
5260
  if (typeof c.description === "string") {
4983
- cmd.description = sanitizeWireText(c.description);
5261
+ cmd.description = sanitizeSingleLine(c.description);
4984
5262
  }
4985
5263
  out.push(cmd);
4986
5264
  }
4987
- return { kind: "available-commands", commands: out };
5265
+ return out;
5266
+ }
5267
+ function mapAvailableCommands(u) {
5268
+ const list = u.availableCommands ?? u.commands;
5269
+ if (!Array.isArray(list)) {
5270
+ return null;
5271
+ }
5272
+ return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
4988
5273
  }
4989
5274
  function mapUsage(u) {
4990
5275
  const event = { kind: "usage-update" };
@@ -5046,7 +5331,7 @@ function mapToolCall(u) {
5046
5331
  return null;
5047
5332
  }
5048
5333
  const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
5049
- const title = sanitizeWireText(rawTitle);
5334
+ const title = sanitizeSingleLine(rawTitle);
5050
5335
  const status = readString(u, "status");
5051
5336
  const rawKind = readString(u, "kind");
5052
5337
  const event = { kind: "tool-call", toolCallId, title };
@@ -5064,7 +5349,7 @@ function mapToolCallUpdate(u) {
5064
5349
  return null;
5065
5350
  }
5066
5351
  const rawTitle = readString(u, "title");
5067
- const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
5352
+ const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
5068
5353
  const status = readString(u, "status");
5069
5354
  const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
5070
5355
  if (!meaningful) {
@@ -5090,7 +5375,7 @@ function mapPlan(u) {
5090
5375
  continue;
5091
5376
  }
5092
5377
  const e = raw;
5093
- const content = typeof e.content === "string" ? sanitizeWireText(e.content) : void 0;
5378
+ const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
5094
5379
  if (!content) {
5095
5380
  continue;
5096
5381
  }
@@ -5110,14 +5395,14 @@ function mapMode(u) {
5110
5395
  if (!mode) {
5111
5396
  return null;
5112
5397
  }
5113
- return { kind: "mode-changed", mode: sanitizeWireText(mode) };
5398
+ return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
5114
5399
  }
5115
5400
  function mapModel(u) {
5116
5401
  const model = readString(u, "currentModel") ?? readString(u, "model");
5117
5402
  if (!model) {
5118
5403
  return null;
5119
5404
  }
5120
- return { kind: "model-changed", model: sanitizeWireText(model) };
5405
+ return { kind: "model-changed", model: sanitizeSingleLine(model) };
5121
5406
  }
5122
5407
  function mapTurnComplete(u) {
5123
5408
  const stopReason = readString(u, "stopReason");
@@ -5606,7 +5891,8 @@ async function runSession(term, config, opts, exitHint) {
5606
5891
  const { update } = params ?? {};
5607
5892
  const event = mapUpdate(update);
5608
5893
  debugLogUpdate(update, event);
5609
- if (event?.kind === "user-text") {
5894
+ const rawTag = update?.sessionUpdate;
5895
+ if (rawTag === "prompt_received") {
5610
5896
  adjustPendingTurns(1);
5611
5897
  } else if (event?.kind === "turn-complete") {
5612
5898
  adjustPendingTurns(-1);
@@ -5677,11 +5963,11 @@ async function runSession(term, config, opts, exitHint) {
5677
5963
  const rawOptions = Array.isArray(p.options) ? p.options : [];
5678
5964
  const options = rawOptions.map((o) => ({
5679
5965
  optionId: o.optionId,
5680
- name: sanitizeWireText(o.name ?? ""),
5966
+ name: sanitizeSingleLine(o.name ?? ""),
5681
5967
  ...o.kind !== void 0 ? { kind: o.kind } : {}
5682
5968
  }));
5683
5969
  const rawTitle = p.toolCall?.title ?? p.toolCall?.name ?? "tool";
5684
- const title = sanitizeWireText(rawTitle);
5970
+ const title = sanitizeSingleLine(rawTitle);
5685
5971
  const toolCallId = p.toolCall?.toolCallId;
5686
5972
  if (options.length === 0) {
5687
5973
  screen.appendLines([
@@ -5759,9 +6045,7 @@ async function runSession(term, config, opts, exitHint) {
5759
6045
  initialMode = hydraMeta.currentMode;
5760
6046
  initialTurnStartedAt = hydraMeta.turnStartedAt;
5761
6047
  if (hydraMeta.availableCommands) {
5762
- initialCommands = hydraMeta.availableCommands.map(
5763
- (c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
5764
- );
6048
+ initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
5765
6049
  }
5766
6050
  } else {
5767
6051
  const attached = await conn.request("session/attach", {
@@ -5786,9 +6070,7 @@ async function runSession(term, config, opts, exitHint) {
5786
6070
  initialMode = hydraMeta.currentMode;
5787
6071
  initialTurnStartedAt = hydraMeta.turnStartedAt;
5788
6072
  if (hydraMeta.availableCommands) {
5789
- initialCommands = hydraMeta.availableCommands.map(
5790
- (c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
5791
- );
6073
+ initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
5792
6074
  }
5793
6075
  }
5794
6076
  const historyFile = paths.tuiHistoryFile(resolvedSessionId);
@@ -5799,11 +6081,13 @@ async function runSession(term, config, opts, exitHint) {
5799
6081
  dispatcher.setTurnRunning(true);
5800
6082
  }
5801
6083
  let turnInFlight = null;
6084
+ let pendingPrefill = null;
5802
6085
  const screen = new Screen({
5803
6086
  term,
5804
6087
  dispatcher,
5805
6088
  repaintThrottleMs: config.tui.repaintThrottleMs,
5806
6089
  maxScrollbackLines: config.tui.maxScrollbackLines,
6090
+ mouse: config.tui.mouse,
5807
6091
  onKey: (events) => {
5808
6092
  for (const ev of events) {
5809
6093
  if (pendingPermission && tryHandlePermissionKey(ev)) {
@@ -5830,6 +6114,7 @@ async function runSession(term, config, opts, exitHint) {
5830
6114
  { name: "/quit", description: "Exit the TUI" },
5831
6115
  { name: "/clear", description: "Clear scrollback" },
5832
6116
  { name: "/sessions", description: "List sessions" },
6117
+ { name: "/model", description: "Switch model: /model <model-id>" },
5833
6118
  { name: "/demo-plan", description: "Inject synthetic plan events (UI test)" },
5834
6119
  { name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
5835
6120
  ];
@@ -5864,48 +6149,24 @@ async function runSession(term, config, opts, exitHint) {
5864
6149
  screen.setCompletions(currentCompletions());
5865
6150
  };
5866
6151
  const tryHandleCompletionKey = (ev) => {
5867
- if (ev.type !== "key") {
6152
+ if (ev.type !== "key" || ev.name !== "tab") {
5868
6153
  return false;
5869
6154
  }
5870
- if (ev.name === "tab") {
5871
- const matches = currentCompletions();
5872
- const first = matches[0];
5873
- if (!first) {
5874
- return false;
5875
- }
5876
- const commonPrefix = longestCommonPrefix(matches.map((m) => m.name));
5877
- const buf = dispatcher.state().buffer;
5878
- const firstLine3 = buf[0] ?? "";
5879
- const space = firstLine3.indexOf(" ");
5880
- const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
5881
- const tail = space === -1 ? "" : firstLine3.slice(space);
5882
- let next = commonPrefix;
5883
- if (commonPrefix.length <= typedPrefix.length || matches.length === 1) {
5884
- next = first.name + (tail.startsWith(" ") ? "" : " ");
5885
- }
5886
- dispatcher.replaceFirstLine(next + tail);
6155
+ const matches = currentCompletions();
6156
+ if (matches.length === 0) {
6157
+ return false;
6158
+ }
6159
+ const firstLine3 = dispatcher.state().buffer[0] ?? "";
6160
+ const next = computeTabCompletion({
6161
+ matches: matches.map((m) => m.name),
6162
+ firstLine: firstLine3
6163
+ });
6164
+ if (next === null) {
5887
6165
  return true;
5888
6166
  }
5889
- return false;
6167
+ dispatcher.replaceFirstLine(next);
6168
+ return true;
5890
6169
  };
5891
- function longestCommonPrefix(names) {
5892
- if (names.length === 0) {
5893
- return "";
5894
- }
5895
- let prefix = names[0] ?? "";
5896
- for (let i = 1; i < names.length; i++) {
5897
- const n = names[i] ?? "";
5898
- let j = 0;
5899
- while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
5900
- j += 1;
5901
- }
5902
- prefix = prefix.slice(0, j);
5903
- if (prefix.length === 0) {
5904
- break;
5905
- }
5906
- }
5907
- return prefix;
5908
- }
5909
6170
  const tryHandlePermissionKey = (ev) => {
5910
6171
  if (!pendingPermission) {
5911
6172
  return false;
@@ -6112,22 +6373,45 @@ async function runSession(term, config, opts, exitHint) {
6112
6373
  }
6113
6374
  resume(nextOpts);
6114
6375
  };
6376
+ const queueHeadOffset = () => workerActive ? 1 : 0;
6115
6377
  const handleEffect = (effect) => {
6116
6378
  switch (effect.type) {
6117
6379
  case "send":
6118
6380
  enqueuePrompt(effect.text, effect.planMode);
6119
6381
  return;
6120
- case "cancel":
6382
+ case "queue-edit": {
6383
+ const realIdx = effect.index + queueHeadOffset();
6384
+ const existing = promptQueue[realIdx];
6385
+ if (existing) {
6386
+ promptQueue[realIdx] = { text: effect.text, planMode: existing.planMode };
6387
+ refreshQueueDisplay();
6388
+ }
6389
+ return;
6390
+ }
6391
+ case "queue-remove": {
6392
+ const realIdx = effect.index + queueHeadOffset();
6393
+ if (realIdx >= 0 && realIdx < promptQueue.length) {
6394
+ promptQueue.splice(realIdx, 1);
6395
+ refreshQueueDisplay();
6396
+ }
6397
+ return;
6398
+ }
6399
+ case "cancel": {
6400
+ if (effect.prefill && turnInFlight) {
6401
+ const headOffset = workerActive ? 1 : 0;
6402
+ const waitingEmpty = promptQueue.length <= headOffset;
6403
+ const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
6404
+ if (waitingEmpty && bufferEmpty) {
6405
+ pendingPrefill = turnInFlight.text;
6406
+ }
6407
+ }
6121
6408
  if (turnInFlight) {
6122
6409
  turnInFlight.cancel();
6123
6410
  } else if (pendingTurns > 0) {
6124
6411
  cancelRemoteTurn();
6125
6412
  }
6126
- if (promptQueue.length > (workerActive ? 1 : 0)) {
6127
- promptQueue.length = workerActive ? 1 : 0;
6128
- refreshQueueDisplay();
6129
- }
6130
6413
  return;
6414
+ }
6131
6415
  case "exit":
6132
6416
  void requestExit();
6133
6417
  return;
@@ -6140,6 +6424,12 @@ async function runSession(term, config, opts, exitHint) {
6140
6424
  case "redraw":
6141
6425
  screen.fullRedraw();
6142
6426
  return;
6427
+ case "scroll-to-top":
6428
+ screen.scrollToTop();
6429
+ return;
6430
+ case "scroll-to-bottom":
6431
+ screen.scrollToBottom();
6432
+ return;
6143
6433
  case "switch-session":
6144
6434
  void switchSession();
6145
6435
  return;
@@ -6155,6 +6445,7 @@ async function runSession(term, config, opts, exitHint) {
6155
6445
  const waiting = promptQueue.slice(workerActive ? 1 : 0);
6156
6446
  screen.setQueuedPrompts(waiting.map((p) => p.text));
6157
6447
  screen.setBanner({ queued: waiting.length });
6448
+ dispatcher.setQueue(waiting.map((p) => p.text));
6158
6449
  };
6159
6450
  const enqueuePrompt = (text, planMode) => {
6160
6451
  screen.scrollToBottom();
@@ -6283,6 +6574,40 @@ async function runSession(term, config, opts, exitHint) {
6283
6574
  }
6284
6575
  ]);
6285
6576
  return true;
6577
+ case "/model": {
6578
+ const arg = space === -1 ? "" : trimmed.slice(space + 1).trim();
6579
+ if (arg === "") {
6580
+ screen.appendLines([
6581
+ {
6582
+ prefix: " ",
6583
+ body: "Usage: /model <model-id>",
6584
+ bodyStyle: "info"
6585
+ }
6586
+ ]);
6587
+ return true;
6588
+ }
6589
+ conn.request("session/set_model", {
6590
+ sessionId: resolvedSessionId,
6591
+ modelId: arg
6592
+ }).then(() => {
6593
+ screen.appendLines([
6594
+ {
6595
+ prefix: " ",
6596
+ body: `model set to ${arg}`,
6597
+ bodyStyle: "system"
6598
+ }
6599
+ ]);
6600
+ }).catch((err) => {
6601
+ screen.appendLines([
6602
+ {
6603
+ prefix: " ",
6604
+ body: `set_model failed: ${err.message}`,
6605
+ bodyStyle: "tool-status-fail"
6606
+ }
6607
+ ]);
6608
+ });
6609
+ return true;
6610
+ }
6286
6611
  default:
6287
6612
  return false;
6288
6613
  }
@@ -6302,6 +6627,15 @@ async function runSession(term, config, opts, exitHint) {
6302
6627
  } finally {
6303
6628
  workerActive = false;
6304
6629
  refreshQueueDisplay();
6630
+ if (pendingPrefill !== null) {
6631
+ const text = pendingPrefill;
6632
+ pendingPrefill = null;
6633
+ const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
6634
+ if (bufferEmpty) {
6635
+ dispatcher.setBuffer(text);
6636
+ screen.refreshPrompt();
6637
+ }
6638
+ }
6305
6639
  }
6306
6640
  };
6307
6641
  const processPrompt = async (text, planMode) => {
@@ -6311,6 +6645,7 @@ async function runSession(term, config, opts, exitHint) {
6311
6645
  appendRender({ kind: "user-text", text });
6312
6646
  let cancelled = false;
6313
6647
  turnInFlight = {
6648
+ text,
6314
6649
  cancel: () => {
6315
6650
  if (cancelled) {
6316
6651
  return;
@@ -6407,12 +6742,13 @@ async function runSession(term, config, opts, exitHint) {
6407
6742
  }
6408
6743
  summary = parts.join(" \xB7 ");
6409
6744
  }
6745
+ const pureThinking = total === 0 && inProgress;
6410
6746
  const lines = [
6411
6747
  {
6412
6748
  prefix: "\u2692 ",
6413
- prefixStyle: "tool",
6749
+ prefixStyle: pureThinking ? "tool-status-running" : "tool",
6414
6750
  body: summary,
6415
- bodyStyle: "dim"
6751
+ bodyStyle: pureThinking ? "tool-status-running" : "dim"
6416
6752
  }
6417
6753
  ];
6418
6754
  for (const id of visibleIds) {
@@ -6588,6 +6924,8 @@ async function runSession(term, config, opts, exitHint) {
6588
6924
  }, 1e3);
6589
6925
  }
6590
6926
  startToolsBlock();
6927
+ } else if (initialTurnStartedAt === void 0 && pendingTurns > 0) {
6928
+ adjustPendingTurns(-pendingTurns);
6591
6929
  }
6592
6930
  const resetInFlightUiState = () => {
6593
6931
  if (pendingPermission) {
@@ -6789,6 +7127,7 @@ var init_app = __esm({
6789
7127
  init_picker();
6790
7128
  init_screen();
6791
7129
  init_input();
7130
+ init_completion();
6792
7131
  init_render_update();
6793
7132
  init_format();
6794
7133
  PLAN_PREFIX_TEXT = "Plan mode is on. Outline what you would do without making any changes. Do not edit files, run shell commands, or otherwise execute side effects; produce a plan only.";
@@ -6875,35 +7214,28 @@ init_config();
6875
7214
  import * as fs2 from "fs/promises";
6876
7215
  async function runInit(flags) {
6877
7216
  await fs2.mkdir(paths.home(), { recursive: true });
6878
- let existing;
6879
- try {
6880
- existing = await loadConfig();
6881
- } catch {
6882
- existing = void 0;
6883
- }
6884
- if (!existing) {
6885
- const config = await writeMinimalInitConfig();
7217
+ const existingToken = await loadAuthToken();
7218
+ if (!existingToken) {
7219
+ const token = generateAuthToken();
7220
+ await writeAuthToken(token);
6886
7221
  process.stdout.write(
6887
- `Initialized ${paths.config()}
6888
- Auth token: ${config.daemon.authToken}
7222
+ `Initialized ${paths.authToken()}
7223
+ Auth token: ${token}
6889
7224
  `
6890
7225
  );
6891
7226
  return;
6892
7227
  }
6893
7228
  if (flagBool(flags, "rotate-token")) {
6894
7229
  const newToken = generateAuthToken();
6895
- await updateConfigField((raw) => {
6896
- const daemon = raw.daemon ??= {};
6897
- daemon.authToken = newToken;
6898
- });
7230
+ await writeAuthToken(newToken);
6899
7231
  process.stdout.write(
6900
- `Rotated token in ${paths.config()}
7232
+ `Rotated token in ${paths.authToken()}
6901
7233
  New token: ${newToken}
6902
7234
  `
6903
7235
  );
6904
7236
  return;
6905
7237
  }
6906
- process.stdout.write(`Config already exists at ${paths.config()}.
7238
+ process.stdout.write(`Auth token already exists at ${paths.authToken()}.
6907
7239
  `);
6908
7240
  process.stdout.write("Pass --rotate-token to generate a new auth token.\n");
6909
7241
  }
@@ -7294,13 +7626,13 @@ function npxPackageBasename(agent) {
7294
7626
  const atIdx = afterSlash.lastIndexOf("@");
7295
7627
  return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
7296
7628
  }
7297
- async function planSpawn(agent, extraArgs = []) {
7629
+ async function planSpawn(agent, callerArgs = []) {
7298
7630
  if (agent.distribution.npx) {
7299
7631
  const npx = agent.distribution.npx;
7300
- const args = ["-y", npx.package, ...npx.args ?? [], ...extraArgs];
7632
+ const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
7301
7633
  return {
7302
7634
  command: "npx",
7303
- args,
7635
+ args: ["-y", npx.package, ...tail],
7304
7636
  env: npx.env ?? {}
7305
7637
  };
7306
7638
  }
@@ -7316,18 +7648,19 @@ async function planSpawn(agent, extraArgs = []) {
7316
7648
  version: agent.version ?? "current",
7317
7649
  target
7318
7650
  });
7651
+ const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
7319
7652
  return {
7320
7653
  command: cmdPath,
7321
- args: [...target.args ?? [], ...extraArgs],
7654
+ args: tail,
7322
7655
  env: target.env ?? {}
7323
7656
  };
7324
7657
  }
7325
7658
  if (agent.distribution.uvx) {
7326
7659
  const uvx = agent.distribution.uvx;
7327
- const args = [uvx.package, ...uvx.args ?? [], ...extraArgs];
7660
+ const tail = callerArgs.length > 0 ? callerArgs : uvx.args ?? [];
7328
7661
  return {
7329
7662
  command: "uvx",
7330
- args,
7663
+ args: [uvx.package, ...tail],
7331
7664
  env: uvx.env ?? {}
7332
7665
  };
7333
7666
  }