@hydra-acp/cli 0.1.5 → 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
  });
@@ -248,6 +295,9 @@ function extractHydraMeta(meta) {
248
295
  out.resume = parsed.data;
249
296
  }
250
297
  }
298
+ if (typeof obj.model === "string") {
299
+ out.model = obj.model;
300
+ }
251
301
  if (typeof obj.currentModel === "string") {
252
302
  out.currentModel = obj.currentModel;
253
303
  }
@@ -392,7 +442,7 @@ var init_connection = __esm({
392
442
  "src/acp/connection.ts"() {
393
443
  "use strict";
394
444
  init_types();
395
- JsonRpcConnection = class {
445
+ JsonRpcConnection = class _JsonRpcConnection {
396
446
  constructor(stream) {
397
447
  this.stream = stream;
398
448
  this.stream.onMessage((m) => this.handleIncoming(m));
@@ -402,6 +452,16 @@ var init_connection = __esm({
402
452
  requestHandlers = /* @__PURE__ */ new Map();
403
453
  defaultRequestHandler;
404
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;
405
465
  pending = /* @__PURE__ */ new Map();
406
466
  closed = false;
407
467
  closeHandlers = [];
@@ -413,6 +473,17 @@ var init_connection = __esm({
413
473
  }
414
474
  onNotification(method, handler) {
415
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
+ }
416
487
  }
417
488
  onClose(handler) {
418
489
  this.closeHandlers.push(handler);
@@ -498,6 +569,16 @@ var init_connection = __esm({
498
569
  const handler = this.notificationHandlers.get(note.method);
499
570
  if (handler) {
500
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();
501
582
  }
502
583
  }
503
584
  handleResponse(res) {
@@ -554,12 +635,12 @@ var init_hydra_commands = __esm({
554
635
  HYDRA_COMMANDS = [
555
636
  {
556
637
  verb: "title",
557
- name: "/hydra title",
638
+ name: "hydra title",
558
639
  description: "Regenerate the session title via the agent (or set manually with an arg)"
559
640
  },
560
641
  {
561
642
  verb: "agent",
562
- name: "/hydra agent",
643
+ name: "hydra agent",
563
644
  argsHint: "<agent>",
564
645
  description: "Swap the agent backing this session, preserving context"
565
646
  }
@@ -3434,6 +3515,7 @@ var init_screen = __esm({
3434
3515
  streamingActive = false;
3435
3516
  lastPromptRows = 0;
3436
3517
  queuedTexts = [];
3518
+ lastQueueEditingIndex = -1;
3437
3519
  repaintPaused = 0;
3438
3520
  repaintPending = false;
3439
3521
  lastRepaintAt = 0;
@@ -3490,12 +3572,14 @@ var init_screen = __esm({
3490
3572
  pasteActive = false;
3491
3573
  pasteBuffer = "";
3492
3574
  rawStdinHandler;
3575
+ mouseEnabled;
3493
3576
  constructor(opts) {
3494
3577
  this.term = opts.term;
3495
3578
  this.dispatcher = opts.dispatcher;
3496
3579
  this.onKey = opts.onKey;
3497
3580
  this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
3498
3581
  this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
3582
+ this.mouseEnabled = opts.mouse ?? true;
3499
3583
  this.resizeHandler = () => this.repaint();
3500
3584
  this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
3501
3585
  this.mouseHandler = (name) => this.handleMouse(name);
@@ -3512,10 +3596,16 @@ var init_screen = __esm({
3512
3596
  this.lastFrameH = 0;
3513
3597
  this.lastWindowTitle = null;
3514
3598
  process.stdout.write("\x1B[?7l");
3515
- this.term.grabInput({ mouse: "button" });
3599
+ if (this.mouseEnabled) {
3600
+ this.term.grabInput({ mouse: "button" });
3601
+ } else {
3602
+ this.term.grabInput(true);
3603
+ }
3516
3604
  this.term.hideCursor(false);
3517
3605
  this.term.on("key", this.keyHandler);
3518
- this.term.on("mouse", this.mouseHandler);
3606
+ if (this.mouseEnabled) {
3607
+ this.term.on("mouse", this.mouseHandler);
3608
+ }
3519
3609
  this.term.on("resize", this.resizeHandler);
3520
3610
  this.installBracketedPaste();
3521
3611
  this.repaint();
@@ -3527,7 +3617,9 @@ var init_screen = __esm({
3527
3617
  this.started = false;
3528
3618
  this.uninstallBracketedPaste();
3529
3619
  this.term.off("key", this.keyHandler);
3530
- this.term.off("mouse", this.mouseHandler);
3620
+ if (this.mouseEnabled) {
3621
+ this.term.off("mouse", this.mouseHandler);
3622
+ }
3531
3623
  this.term.off("resize", this.resizeHandler);
3532
3624
  this.term.grabInput(false);
3533
3625
  this.term.hideCursor(false);
@@ -3877,6 +3969,7 @@ var init_screen = __esm({
3877
3969
  }
3878
3970
  setQueuedPrompts(texts) {
3879
3971
  this.queuedTexts = [...texts];
3972
+ this.lastQueueEditingIndex = this.dispatcher.state().queueIndex;
3880
3973
  this.repaint();
3881
3974
  }
3882
3975
  // While a permission prompt is active, the prompt area is replaced with
@@ -3929,12 +4022,19 @@ var init_screen = __esm({
3929
4022
  // row count changed (alt+enter added a line, backspace joined two), the
3930
4023
  // separator and scrollback bottom shift, so we need a full repaint;
3931
4024
  // otherwise an in-place prompt redraw is enough. (Queued-zone changes
3932
- // 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.
3933
4028
  refreshPrompt() {
3934
4029
  if (this.promptRows() !== this.lastPromptRows) {
3935
4030
  this.repaint();
3936
4031
  return;
3937
4032
  }
4033
+ const editingIndex = this.dispatcher.state().queueIndex;
4034
+ if (editingIndex !== this.lastQueueEditingIndex) {
4035
+ this.lastQueueEditingIndex = editingIndex;
4036
+ this.drawQueuedZone();
4037
+ }
3938
4038
  this.drawPrompt();
3939
4039
  this.placeCursor();
3940
4040
  }
@@ -3985,6 +4085,14 @@ var init_screen = __esm({
3985
4085
  this.scrollOffset = 0;
3986
4086
  this.repaint();
3987
4087
  }
4088
+ scrollToTop() {
4089
+ const max = this.maxScrollOffset();
4090
+ if (this.scrollOffset === max) {
4091
+ return;
4092
+ }
4093
+ this.scrollOffset = max;
4094
+ this.repaint();
4095
+ }
3988
4096
  scrollPageSize() {
3989
4097
  return Math.max(1, this.scrollbackVisibleRows() - 2);
3990
4098
  }
@@ -4212,19 +4320,26 @@ var init_screen = __esm({
4212
4320
  const separatorRow = this.term.height - promptRows - BANNER_ROWS;
4213
4321
  const queuedBottom = separatorRow - 1;
4214
4322
  const queuedTop = queuedBottom - rows + 1;
4323
+ const editingIndex = this.dispatcher.state().queueIndex;
4215
4324
  for (let i = 0; i < rows; i++) {
4216
4325
  const row = queuedTop + i;
4217
4326
  const text = this.queuedTexts[i];
4218
4327
  const isLast = i === rows - 1 && this.queuedTexts.length > MAX_QUEUED_ROWS;
4219
4328
  const overflow = this.queuedTexts.length - MAX_QUEUED_ROWS;
4220
4329
  const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
4221
- 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}`;
4222
4332
  this.paintRow(row, sig, () => {
4223
4333
  if (text === void 0) {
4224
4334
  return;
4225
4335
  }
4226
- const display = ` \u23F3 ${summary}`;
4227
- 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
+ }
4228
4343
  this.term.bgBlue.brightWhite.noFormat(padded);
4229
4344
  });
4230
4345
  }
@@ -4530,8 +4645,17 @@ var init_input = __esm({
4530
4645
  col = 0;
4531
4646
  planMode = false;
4532
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;
4533
4654
  savedDraft = null;
4534
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 = [];
4535
4659
  turnRunning = false;
4536
4660
  // Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
4537
4661
  // here so ^Y can yank it back. Standard readline keeps a stack; we
@@ -4547,7 +4671,8 @@ var init_input = __esm({
4547
4671
  row: this.row,
4548
4672
  col: this.col,
4549
4673
  planMode: this.planMode,
4550
- historyIndex: this.historyIndex
4674
+ historyIndex: this.historyIndex,
4675
+ queueIndex: this.queueIndex
4551
4676
  };
4552
4677
  }
4553
4678
  setTurnRunning(running) {
@@ -4558,6 +4683,16 @@ var init_input = __esm({
4558
4683
  this.historyIndex = -1;
4559
4684
  this.savedDraft = null;
4560
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
+ }
4561
4696
  // Replace the contents of the first row, leaving subsequent rows alone.
4562
4697
  // Used by slash-command completion: the partial /foo gets swapped for the
4563
4698
  // matched command name. Cursor moves to the end of the replacement.
@@ -4567,6 +4702,15 @@ var init_input = __esm({
4567
4702
  this.col = text.length;
4568
4703
  }
4569
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
+ }
4570
4714
  feed(event) {
4571
4715
  if (event.type === "char") {
4572
4716
  this.insertChar(event.ch);
@@ -4604,14 +4748,16 @@ var init_input = __esm({
4604
4748
  case "right":
4605
4749
  this.moveRight();
4606
4750
  return [];
4607
- case "home":
4608
4751
  case "ctrl-a":
4609
4752
  this.col = 0;
4610
4753
  return [];
4611
- case "end":
4612
4754
  case "ctrl-e":
4613
4755
  this.col = this.currentLine().length;
4614
4756
  return [];
4757
+ case "home":
4758
+ return this.handleHome();
4759
+ case "end":
4760
+ return this.handleEnd();
4615
4761
  case "ctrl-b":
4616
4762
  this.moveLeft();
4617
4763
  return [];
@@ -4634,7 +4780,11 @@ var init_input = __esm({
4634
4780
  case "ctrl-c":
4635
4781
  return this.handleCtrlC();
4636
4782
  case "ctrl-d":
4637
- return this.bufferIsEmpty() ? [{ type: "exit" }] : [];
4783
+ if (this.bufferIsEmpty()) {
4784
+ return [{ type: "exit" }];
4785
+ }
4786
+ this.deleteForward();
4787
+ return [];
4638
4788
  case "ctrl-l":
4639
4789
  return [{ type: "redraw" }];
4640
4790
  case "ctrl-p":
@@ -4649,6 +4799,9 @@ var init_input = __esm({
4649
4799
  this.yank();
4650
4800
  return [];
4651
4801
  case "escape":
4802
+ if (this.turnRunning) {
4803
+ return [{ type: "cancel", prefill: true }];
4804
+ }
4652
4805
  return [];
4653
4806
  }
4654
4807
  }
@@ -4669,6 +4822,7 @@ var init_input = __esm({
4669
4822
  this.row = 0;
4670
4823
  this.col = 0;
4671
4824
  this.historyIndex = -1;
4825
+ this.queueIndex = -1;
4672
4826
  this.savedDraft = null;
4673
4827
  }
4674
4828
  insertChar(ch) {
@@ -4802,50 +4956,92 @@ var init_input = __esm({
4802
4956
  this.col = 0;
4803
4957
  }
4804
4958
  }
4805
- // Up scrolls back through history when the cursor is on the first line of
4806
- // 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.
4807
4963
  handleUp() {
4808
4964
  if (this.row > 0) {
4809
4965
  this.row -= 1;
4810
4966
  this.col = Math.min(this.col, this.currentLine().length);
4811
4967
  return [];
4812
4968
  }
4813
- if (this.history.length === 0) {
4814
- return [];
4815
- }
4816
- 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
+ }
4817
4973
  this.savedDraft = {
4818
4974
  buffer: [...this.buffer],
4819
4975
  row: this.row,
4820
4976
  col: this.col
4821
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;
4822
4997
  this.historyIndex = this.history.length - 1;
4823
- } else if (this.historyIndex > 0) {
4824
- this.historyIndex -= 1;
4825
- } else {
4998
+ this.loadEntry(this.history[this.historyIndex] ?? "");
4826
4999
  return [];
4827
5000
  }
4828
- this.loadHistoryEntry(this.historyIndex);
5001
+ if (this.historyIndex > 0) {
5002
+ this.historyIndex -= 1;
5003
+ this.loadEntry(this.history[this.historyIndex] ?? "");
5004
+ }
4829
5005
  return [];
4830
5006
  }
4831
- // Down advances within history; when we walk off the end, restore the
4832
- // saved draft. When already on a multi-line buffer's middle row, just
4833
- // 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.
4834
5011
  handleDown() {
4835
- if (this.row < this.buffer.length - 1 && this.historyIndex === -1) {
5012
+ if (this.row < this.buffer.length - 1 && this.historyIndex === -1 && this.queueIndex === -1) {
4836
5013
  this.row += 1;
4837
5014
  this.col = Math.min(this.col, this.currentLine().length);
4838
5015
  return [];
4839
5016
  }
4840
- 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();
4841
5030
  return [];
4842
5031
  }
4843
- if (this.historyIndex < this.history.length - 1) {
4844
- this.historyIndex += 1;
4845
- 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();
4846
5040
  return [];
4847
5041
  }
4848
- this.historyIndex = -1;
5042
+ return [];
5043
+ }
5044
+ restoreDraft() {
4849
5045
  if (this.savedDraft) {
4850
5046
  this.buffer = [...this.savedDraft.buffer];
4851
5047
  this.row = this.savedDraft.row;
@@ -4854,11 +5050,9 @@ var init_input = __esm({
4854
5050
  } else {
4855
5051
  this.clearBuffer();
4856
5052
  }
4857
- return [];
4858
5053
  }
4859
- loadHistoryEntry(index) {
4860
- const entry = this.history[index] ?? "";
4861
- this.buffer = entry.split("\n");
5054
+ loadEntry(text) {
5055
+ this.buffer = text.split("\n");
4862
5056
  if (this.buffer.length === 0) {
4863
5057
  this.buffer = [""];
4864
5058
  }
@@ -4867,6 +5061,14 @@ var init_input = __esm({
4867
5061
  }
4868
5062
  send() {
4869
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
+ }
4870
5072
  if (text.trim().length === 0) {
4871
5073
  return [];
4872
5074
  }
@@ -4874,25 +5076,105 @@ var init_input = __esm({
4874
5076
  this.clearBuffer();
4875
5077
  return [{ type: "send", text, planMode }];
4876
5078
  }
4877
- handleCtrlC() {
4878
- if (this.turnRunning) {
4879
- 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 [];
4880
5098
  }
5099
+ return [{ type: "scroll-to-bottom" }];
5100
+ }
5101
+ handleCtrlC() {
4881
5102
  if (!this.bufferIsEmpty()) {
4882
- 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
+ }
4883
5110
  return [];
4884
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
+ }
4885
5120
  return [{ type: "exit" }];
4886
5121
  }
4887
5122
  };
4888
5123
  }
4889
5124
  });
4890
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
+
4891
5170
  // src/tui/render-update.ts
4892
5171
  import stripAnsi from "strip-ansi";
4893
5172
  function sanitizeWireText(text) {
4894
5173
  return stripAnsi(text).replace(STRIP_CONTROLS, "");
4895
5174
  }
5175
+ function sanitizeSingleLine(text) {
5176
+ return sanitizeWireText(text).replace(/[\n\t]+/g, " ").replace(/ +/g, " ").trim();
5177
+ }
4896
5178
  function mapUpdate(update) {
4897
5179
  if (!update || typeof update !== "object") {
4898
5180
  return null;
@@ -4936,7 +5218,7 @@ function mapUpdate(update) {
4936
5218
  }
4937
5219
  function mapSessionInfo(u) {
4938
5220
  const rawTitle = readString(u, "title");
4939
- const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
5221
+ const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
4940
5222
  const meta = u._meta;
4941
5223
  let agentId;
4942
5224
  if (meta && typeof meta === "object" && !Array.isArray(meta)) {
@@ -4960,10 +5242,9 @@ function mapSessionInfo(u) {
4960
5242
  }
4961
5243
  return event;
4962
5244
  }
4963
- function mapAvailableCommands(u) {
4964
- const list = u.availableCommands ?? u.commands;
5245
+ function normalizeAdvertisedCommands(list) {
4965
5246
  if (!Array.isArray(list)) {
4966
- return null;
5247
+ return [];
4967
5248
  }
4968
5249
  const out = [];
4969
5250
  for (const raw of list) {
@@ -4975,13 +5256,20 @@ function mapAvailableCommands(u) {
4975
5256
  continue;
4976
5257
  }
4977
5258
  const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
4978
- const cmd = { name: sanitizeWireText(rawName) };
5259
+ const cmd = { name: sanitizeSingleLine(rawName) };
4979
5260
  if (typeof c.description === "string") {
4980
- cmd.description = sanitizeWireText(c.description);
5261
+ cmd.description = sanitizeSingleLine(c.description);
4981
5262
  }
4982
5263
  out.push(cmd);
4983
5264
  }
4984
- 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) };
4985
5273
  }
4986
5274
  function mapUsage(u) {
4987
5275
  const event = { kind: "usage-update" };
@@ -5043,7 +5331,7 @@ function mapToolCall(u) {
5043
5331
  return null;
5044
5332
  }
5045
5333
  const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
5046
- const title = sanitizeWireText(rawTitle);
5334
+ const title = sanitizeSingleLine(rawTitle);
5047
5335
  const status = readString(u, "status");
5048
5336
  const rawKind = readString(u, "kind");
5049
5337
  const event = { kind: "tool-call", toolCallId, title };
@@ -5061,7 +5349,7 @@ function mapToolCallUpdate(u) {
5061
5349
  return null;
5062
5350
  }
5063
5351
  const rawTitle = readString(u, "title");
5064
- const title = rawTitle !== void 0 ? sanitizeWireText(rawTitle) : void 0;
5352
+ const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
5065
5353
  const status = readString(u, "status");
5066
5354
  const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
5067
5355
  if (!meaningful) {
@@ -5087,7 +5375,7 @@ function mapPlan(u) {
5087
5375
  continue;
5088
5376
  }
5089
5377
  const e = raw;
5090
- const content = typeof e.content === "string" ? sanitizeWireText(e.content) : void 0;
5378
+ const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
5091
5379
  if (!content) {
5092
5380
  continue;
5093
5381
  }
@@ -5107,14 +5395,14 @@ function mapMode(u) {
5107
5395
  if (!mode) {
5108
5396
  return null;
5109
5397
  }
5110
- return { kind: "mode-changed", mode: sanitizeWireText(mode) };
5398
+ return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
5111
5399
  }
5112
5400
  function mapModel(u) {
5113
5401
  const model = readString(u, "currentModel") ?? readString(u, "model");
5114
5402
  if (!model) {
5115
5403
  return null;
5116
5404
  }
5117
- return { kind: "model-changed", model: sanitizeWireText(model) };
5405
+ return { kind: "model-changed", model: sanitizeSingleLine(model) };
5118
5406
  }
5119
5407
  function mapTurnComplete(u) {
5120
5408
  const stopReason = readString(u, "stopReason");
@@ -5420,8 +5708,17 @@ function formatPlan(event) {
5420
5708
  }
5421
5709
  ];
5422
5710
  }
5711
+ const allComplete = event.entries.every(
5712
+ (e) => (e.status ?? "pending") === "completed"
5713
+ );
5714
+ const headerStyle = allComplete ? "plan-done" : "plan";
5423
5715
  const lines = [
5424
- { prefix: "\u25A3 ", prefixStyle: "plan", body: "Plan", bodyStyle: "plan" }
5716
+ {
5717
+ prefix: "\u25A3 ",
5718
+ prefixStyle: headerStyle,
5719
+ body: "Plan",
5720
+ bodyStyle: headerStyle
5721
+ }
5425
5722
  ];
5426
5723
  for (const entry of event.entries) {
5427
5724
  const status = entry.status ?? "pending";
@@ -5594,7 +5891,8 @@ async function runSession(term, config, opts, exitHint) {
5594
5891
  const { update } = params ?? {};
5595
5892
  const event = mapUpdate(update);
5596
5893
  debugLogUpdate(update, event);
5597
- if (event?.kind === "user-text") {
5894
+ const rawTag = update?.sessionUpdate;
5895
+ if (rawTag === "prompt_received") {
5598
5896
  adjustPendingTurns(1);
5599
5897
  } else if (event?.kind === "turn-complete") {
5600
5898
  adjustPendingTurns(-1);
@@ -5665,11 +5963,11 @@ async function runSession(term, config, opts, exitHint) {
5665
5963
  const rawOptions = Array.isArray(p.options) ? p.options : [];
5666
5964
  const options = rawOptions.map((o) => ({
5667
5965
  optionId: o.optionId,
5668
- name: sanitizeWireText(o.name ?? ""),
5966
+ name: sanitizeSingleLine(o.name ?? ""),
5669
5967
  ...o.kind !== void 0 ? { kind: o.kind } : {}
5670
5968
  }));
5671
5969
  const rawTitle = p.toolCall?.title ?? p.toolCall?.name ?? "tool";
5672
- const title = sanitizeWireText(rawTitle);
5970
+ const title = sanitizeSingleLine(rawTitle);
5673
5971
  const toolCallId = p.toolCall?.toolCallId;
5674
5972
  if (options.length === 0) {
5675
5973
  screen.appendLines([
@@ -5718,10 +6016,17 @@ async function runSession(term, config, opts, exitHint) {
5718
6016
  let initialCommands;
5719
6017
  let initialTurnStartedAt;
5720
6018
  if (ctx.sessionId === "__new__") {
6019
+ const hydraNewMeta = {};
6020
+ if (opts.name) {
6021
+ hydraNewMeta.name = opts.name;
6022
+ }
6023
+ if (opts.model) {
6024
+ hydraNewMeta.model = opts.model;
6025
+ }
5721
6026
  const created = await conn.request("session/new", {
5722
6027
  cwd: ctx.cwd,
5723
6028
  ...opts.agentId ? { agentId: opts.agentId } : {},
5724
- ...opts.name ? { _meta: { [HYDRA_META_KEY]: { name: opts.name } } } : {}
6029
+ ...Object.keys(hydraNewMeta).length > 0 ? { _meta: { [HYDRA_META_KEY]: hydraNewMeta } } : {}
5725
6030
  });
5726
6031
  resolvedSessionId = created.sessionId;
5727
6032
  exitHint.sessionId = resolvedSessionId;
@@ -5740,9 +6045,7 @@ async function runSession(term, config, opts, exitHint) {
5740
6045
  initialMode = hydraMeta.currentMode;
5741
6046
  initialTurnStartedAt = hydraMeta.turnStartedAt;
5742
6047
  if (hydraMeta.availableCommands) {
5743
- initialCommands = hydraMeta.availableCommands.map(
5744
- (c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
5745
- );
6048
+ initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
5746
6049
  }
5747
6050
  } else {
5748
6051
  const attached = await conn.request("session/attach", {
@@ -5767,9 +6070,7 @@ async function runSession(term, config, opts, exitHint) {
5767
6070
  initialMode = hydraMeta.currentMode;
5768
6071
  initialTurnStartedAt = hydraMeta.turnStartedAt;
5769
6072
  if (hydraMeta.availableCommands) {
5770
- initialCommands = hydraMeta.availableCommands.map(
5771
- (c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
5772
- );
6073
+ initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
5773
6074
  }
5774
6075
  }
5775
6076
  const historyFile = paths.tuiHistoryFile(resolvedSessionId);
@@ -5780,11 +6081,13 @@ async function runSession(term, config, opts, exitHint) {
5780
6081
  dispatcher.setTurnRunning(true);
5781
6082
  }
5782
6083
  let turnInFlight = null;
6084
+ let pendingPrefill = null;
5783
6085
  const screen = new Screen({
5784
6086
  term,
5785
6087
  dispatcher,
5786
6088
  repaintThrottleMs: config.tui.repaintThrottleMs,
5787
6089
  maxScrollbackLines: config.tui.maxScrollbackLines,
6090
+ mouse: config.tui.mouse,
5788
6091
  onKey: (events) => {
5789
6092
  for (const ev of events) {
5790
6093
  if (pendingPermission && tryHandlePermissionKey(ev)) {
@@ -5811,6 +6114,7 @@ async function runSession(term, config, opts, exitHint) {
5811
6114
  { name: "/quit", description: "Exit the TUI" },
5812
6115
  { name: "/clear", description: "Clear scrollback" },
5813
6116
  { name: "/sessions", description: "List sessions" },
6117
+ { name: "/model", description: "Switch model: /model <model-id>" },
5814
6118
  { name: "/demo-plan", description: "Inject synthetic plan events (UI test)" },
5815
6119
  { name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
5816
6120
  ];
@@ -5845,48 +6149,24 @@ async function runSession(term, config, opts, exitHint) {
5845
6149
  screen.setCompletions(currentCompletions());
5846
6150
  };
5847
6151
  const tryHandleCompletionKey = (ev) => {
5848
- if (ev.type !== "key") {
6152
+ if (ev.type !== "key" || ev.name !== "tab") {
5849
6153
  return false;
5850
6154
  }
5851
- if (ev.name === "tab") {
5852
- const matches = currentCompletions();
5853
- const first = matches[0];
5854
- if (!first) {
5855
- return false;
5856
- }
5857
- const commonPrefix = longestCommonPrefix(matches.map((m) => m.name));
5858
- const buf = dispatcher.state().buffer;
5859
- const firstLine3 = buf[0] ?? "";
5860
- const space = firstLine3.indexOf(" ");
5861
- const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
5862
- const tail = space === -1 ? "" : firstLine3.slice(space);
5863
- let next = commonPrefix;
5864
- if (commonPrefix.length <= typedPrefix.length || matches.length === 1) {
5865
- next = first.name + (tail.startsWith(" ") ? "" : " ");
5866
- }
5867
- 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) {
5868
6165
  return true;
5869
6166
  }
5870
- return false;
6167
+ dispatcher.replaceFirstLine(next);
6168
+ return true;
5871
6169
  };
5872
- function longestCommonPrefix(names) {
5873
- if (names.length === 0) {
5874
- return "";
5875
- }
5876
- let prefix = names[0] ?? "";
5877
- for (let i = 1; i < names.length; i++) {
5878
- const n = names[i] ?? "";
5879
- let j = 0;
5880
- while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
5881
- j += 1;
5882
- }
5883
- prefix = prefix.slice(0, j);
5884
- if (prefix.length === 0) {
5885
- break;
5886
- }
5887
- }
5888
- return prefix;
5889
- }
5890
6170
  const tryHandlePermissionKey = (ev) => {
5891
6171
  if (!pendingPermission) {
5892
6172
  return false;
@@ -6093,22 +6373,45 @@ async function runSession(term, config, opts, exitHint) {
6093
6373
  }
6094
6374
  resume(nextOpts);
6095
6375
  };
6376
+ const queueHeadOffset = () => workerActive ? 1 : 0;
6096
6377
  const handleEffect = (effect) => {
6097
6378
  switch (effect.type) {
6098
6379
  case "send":
6099
6380
  enqueuePrompt(effect.text, effect.planMode);
6100
6381
  return;
6101
- 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
+ }
6102
6408
  if (turnInFlight) {
6103
6409
  turnInFlight.cancel();
6104
6410
  } else if (pendingTurns > 0) {
6105
6411
  cancelRemoteTurn();
6106
6412
  }
6107
- if (promptQueue.length > (workerActive ? 1 : 0)) {
6108
- promptQueue.length = workerActive ? 1 : 0;
6109
- refreshQueueDisplay();
6110
- }
6111
6413
  return;
6414
+ }
6112
6415
  case "exit":
6113
6416
  void requestExit();
6114
6417
  return;
@@ -6121,6 +6424,12 @@ async function runSession(term, config, opts, exitHint) {
6121
6424
  case "redraw":
6122
6425
  screen.fullRedraw();
6123
6426
  return;
6427
+ case "scroll-to-top":
6428
+ screen.scrollToTop();
6429
+ return;
6430
+ case "scroll-to-bottom":
6431
+ screen.scrollToBottom();
6432
+ return;
6124
6433
  case "switch-session":
6125
6434
  void switchSession();
6126
6435
  return;
@@ -6136,6 +6445,7 @@ async function runSession(term, config, opts, exitHint) {
6136
6445
  const waiting = promptQueue.slice(workerActive ? 1 : 0);
6137
6446
  screen.setQueuedPrompts(waiting.map((p) => p.text));
6138
6447
  screen.setBanner({ queued: waiting.length });
6448
+ dispatcher.setQueue(waiting.map((p) => p.text));
6139
6449
  };
6140
6450
  const enqueuePrompt = (text, planMode) => {
6141
6451
  screen.scrollToBottom();
@@ -6264,6 +6574,40 @@ async function runSession(term, config, opts, exitHint) {
6264
6574
  }
6265
6575
  ]);
6266
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
+ }
6267
6611
  default:
6268
6612
  return false;
6269
6613
  }
@@ -6283,6 +6627,15 @@ async function runSession(term, config, opts, exitHint) {
6283
6627
  } finally {
6284
6628
  workerActive = false;
6285
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
+ }
6286
6639
  }
6287
6640
  };
6288
6641
  const processPrompt = async (text, planMode) => {
@@ -6292,6 +6645,7 @@ async function runSession(term, config, opts, exitHint) {
6292
6645
  appendRender({ kind: "user-text", text });
6293
6646
  let cancelled = false;
6294
6647
  turnInFlight = {
6648
+ text,
6295
6649
  cancel: () => {
6296
6650
  if (cancelled) {
6297
6651
  return;
@@ -6388,12 +6742,13 @@ async function runSession(term, config, opts, exitHint) {
6388
6742
  }
6389
6743
  summary = parts.join(" \xB7 ");
6390
6744
  }
6745
+ const pureThinking = total === 0 && inProgress;
6391
6746
  const lines = [
6392
6747
  {
6393
6748
  prefix: "\u2692 ",
6394
- prefixStyle: "tool",
6749
+ prefixStyle: pureThinking ? "tool-status-running" : "tool",
6395
6750
  body: summary,
6396
- bodyStyle: "dim"
6751
+ bodyStyle: pureThinking ? "tool-status-running" : "dim"
6397
6752
  }
6398
6753
  ];
6399
6754
  for (const id of visibleIds) {
@@ -6569,6 +6924,8 @@ async function runSession(term, config, opts, exitHint) {
6569
6924
  }, 1e3);
6570
6925
  }
6571
6926
  startToolsBlock();
6927
+ } else if (initialTurnStartedAt === void 0 && pendingTurns > 0) {
6928
+ adjustPendingTurns(-pendingTurns);
6572
6929
  }
6573
6930
  const resetInFlightUiState = () => {
6574
6931
  if (pendingPermission) {
@@ -6770,6 +7127,7 @@ var init_app = __esm({
6770
7127
  init_picker();
6771
7128
  init_screen();
6772
7129
  init_input();
7130
+ init_completion();
6773
7131
  init_render_update();
6774
7132
  init_format();
6775
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.";
@@ -6856,35 +7214,28 @@ init_config();
6856
7214
  import * as fs2 from "fs/promises";
6857
7215
  async function runInit(flags) {
6858
7216
  await fs2.mkdir(paths.home(), { recursive: true });
6859
- let existing;
6860
- try {
6861
- existing = await loadConfig();
6862
- } catch {
6863
- existing = void 0;
6864
- }
6865
- if (!existing) {
6866
- const config = await writeMinimalInitConfig();
7217
+ const existingToken = await loadAuthToken();
7218
+ if (!existingToken) {
7219
+ const token = generateAuthToken();
7220
+ await writeAuthToken(token);
6867
7221
  process.stdout.write(
6868
- `Initialized ${paths.config()}
6869
- Auth token: ${config.daemon.authToken}
7222
+ `Initialized ${paths.authToken()}
7223
+ Auth token: ${token}
6870
7224
  `
6871
7225
  );
6872
7226
  return;
6873
7227
  }
6874
7228
  if (flagBool(flags, "rotate-token")) {
6875
7229
  const newToken = generateAuthToken();
6876
- await updateConfigField((raw) => {
6877
- const daemon = raw.daemon ??= {};
6878
- daemon.authToken = newToken;
6879
- });
7230
+ await writeAuthToken(newToken);
6880
7231
  process.stdout.write(
6881
- `Rotated token in ${paths.config()}
7232
+ `Rotated token in ${paths.authToken()}
6882
7233
  New token: ${newToken}
6883
7234
  `
6884
7235
  );
6885
7236
  return;
6886
7237
  }
6887
- process.stdout.write(`Config already exists at ${paths.config()}.
7238
+ process.stdout.write(`Auth token already exists at ${paths.authToken()}.
6888
7239
  `);
6889
7240
  process.stdout.write("Pass --rotate-token to generate a new auth token.\n");
6890
7241
  }
@@ -7275,13 +7626,13 @@ function npxPackageBasename(agent) {
7275
7626
  const atIdx = afterSlash.lastIndexOf("@");
7276
7627
  return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
7277
7628
  }
7278
- async function planSpawn(agent, extraArgs = []) {
7629
+ async function planSpawn(agent, callerArgs = []) {
7279
7630
  if (agent.distribution.npx) {
7280
7631
  const npx = agent.distribution.npx;
7281
- const args = ["-y", npx.package, ...npx.args ?? [], ...extraArgs];
7632
+ const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
7282
7633
  return {
7283
7634
  command: "npx",
7284
- args,
7635
+ args: ["-y", npx.package, ...tail],
7285
7636
  env: npx.env ?? {}
7286
7637
  };
7287
7638
  }
@@ -7297,18 +7648,19 @@ async function planSpawn(agent, extraArgs = []) {
7297
7648
  version: agent.version ?? "current",
7298
7649
  target
7299
7650
  });
7651
+ const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
7300
7652
  return {
7301
7653
  command: cmdPath,
7302
- args: [...target.args ?? [], ...extraArgs],
7654
+ args: tail,
7303
7655
  env: target.env ?? {}
7304
7656
  };
7305
7657
  }
7306
7658
  if (agent.distribution.uvx) {
7307
7659
  const uvx = agent.distribution.uvx;
7308
- const args = [uvx.package, ...uvx.args ?? [], ...extraArgs];
7660
+ const tail = callerArgs.length > 0 ? callerArgs : uvx.args ?? [];
7309
7661
  return {
7310
7662
  command: "uvx",
7311
- args,
7663
+ args: [uvx.package, ...tail],
7312
7664
  env: uvx.env ?? {}
7313
7665
  };
7314
7666
  }
@@ -7816,7 +8168,8 @@ var SessionManager = class {
7816
8168
  agentId: params.agentId,
7817
8169
  cwd: params.cwd,
7818
8170
  agentArgs: params.agentArgs,
7819
- mcpServers: params.mcpServers
8171
+ mcpServers: params.mcpServers,
8172
+ model: params.model
7820
8173
  });
7821
8174
  const session = new Session({
7822
8175
  cwd: params.cwd,
@@ -8008,7 +8361,7 @@ var SessionManager = class {
8008
8361
  );
8009
8362
  }
8010
8363
  let initialModel = extractInitialModel(newResult);
8011
- const desired = this.defaultModels[params.agentId];
8364
+ const desired = params.model ?? this.defaultModels[params.agentId];
8012
8365
  if (desired && desired !== initialModel) {
8013
8366
  try {
8014
8367
  await agent.connection.request("session/set_model", {
@@ -9459,7 +9812,8 @@ function registerAcpWsEndpoint(app, deps) {
9459
9812
  agentId: params.agentId ?? deps.defaultAgent,
9460
9813
  mcpServers: params.mcpServers,
9461
9814
  title: hydraMeta.name,
9462
- agentArgs: hydraMeta.agentArgs
9815
+ agentArgs: hydraMeta.agentArgs,
9816
+ model: hydraMeta.model
9463
9817
  });
9464
9818
  const client = bindClientToSession(connection, session, state);
9465
9819
  await session.attach(client, "full");
@@ -10834,6 +11188,9 @@ function wireShim({
10834
11188
  outgoing = injectHydraMeta(outgoing, { name: namingState.name });
10835
11189
  namingState.used = true;
10836
11190
  }
11191
+ if (opts.model) {
11192
+ outgoing = injectHydraMeta(outgoing, { model: opts.model });
11193
+ }
10837
11194
  void upstream.send(outgoing);
10838
11195
  return;
10839
11196
  }
@@ -10976,10 +11333,10 @@ async function main() {
10976
11333
  const positionalAgentId = afterLaunch[0];
10977
11334
  const agentArgs = afterLaunch.slice(1);
10978
11335
  const { flags: flags2 } = parseArgs(beforeLaunch);
10979
- const agentId = positionalAgentId ?? resolveOption(flags2, "agent-id");
11336
+ const agentId = positionalAgentId ?? resolveOption(flags2, "agent");
10980
11337
  if (!agentId) {
10981
11338
  process.stderr.write(
10982
- "Usage: hydra-acp launch <agent-id> [agent-args...]\n"
11339
+ "Usage: hydra-acp launch <agent> [agent-args...]\n"
10983
11340
  );
10984
11341
  process.exit(2);
10985
11342
  return;
@@ -10987,7 +11344,8 @@ async function main() {
10987
11344
  const launchResume = flags2.resume;
10988
11345
  const sessionId2 = typeof launchResume === "string" ? launchResume : resolveOption(flags2, "session-id");
10989
11346
  const name2 = resolveOption(flags2, "name");
10990
- await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2 });
11347
+ const model2 = resolveOption(flags2, "model");
11348
+ await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2, model: model2 });
10991
11349
  return;
10992
11350
  }
10993
11351
  const { positional, flags } = parseArgs(argv);
@@ -11004,22 +11362,24 @@ async function main() {
11004
11362
  const resumeFlag = flags.resume;
11005
11363
  const sessionId = typeof resumeFlag === "string" ? resumeFlag : resolveOption(flags, "session-id");
11006
11364
  const name = resolveOption(flags, "name");
11007
- const agentIdFromFlag = resolveOption(flags, "agent-id");
11365
+ const agentIdFromFlag = resolveOption(flags, "agent");
11366
+ const model = resolveOption(flags, "model");
11008
11367
  if (!subcommand) {
11009
11368
  if (process.stdout.isTTY) {
11010
11369
  await dispatchTui(flags, {
11011
11370
  sessionId,
11012
11371
  agentId: agentIdFromFlag,
11013
- name
11372
+ name,
11373
+ model
11014
11374
  });
11015
11375
  return;
11016
11376
  }
11017
- await runShim({ sessionId, name, agentId: agentIdFromFlag });
11377
+ await runShim({ sessionId, name, agentId: agentIdFromFlag, model });
11018
11378
  return;
11019
11379
  }
11020
11380
  switch (subcommand) {
11021
11381
  case "shim":
11022
- await runShim({ sessionId, name, agentId: agentIdFromFlag });
11382
+ await runShim({ sessionId, name, agentId: agentIdFromFlag, model });
11023
11383
  return;
11024
11384
  case "init":
11025
11385
  await runInit(flags);
@@ -11141,7 +11501,8 @@ async function main() {
11141
11501
  await dispatchTui(flags, {
11142
11502
  sessionId,
11143
11503
  agentId: agentIdFromFlag,
11144
- name
11504
+ name,
11505
+ model
11145
11506
  });
11146
11507
  return;
11147
11508
  default:
@@ -11169,6 +11530,9 @@ async function dispatchTui(flags, base) {
11169
11530
  if (base.name !== void 0) {
11170
11531
  tuiOpts.name = base.name;
11171
11532
  }
11533
+ if (base.model !== void 0) {
11534
+ tuiOpts.model = base.model;
11535
+ }
11172
11536
  await runTui(tuiOpts);
11173
11537
  }
11174
11538
  function readVersion() {
@@ -11191,9 +11555,9 @@ function printHelp() {
11191
11555
  " hydra-acp Auto: TUI when stdout is a TTY, shim otherwise (the editor-spawned case)",
11192
11556
  " hydra-acp shim Run as ACP shim explicitly (forces shim mode regardless of TTY)",
11193
11557
  " hydra-acp tui [opts] Run the terminal UI explicitly (see below for opts)",
11194
- " hydra-acp launch <agent-id> [agent-args...]",
11195
- " Shim mode, force daemon to spawn <agent-id>",
11196
- " from the registry. Args after <agent-id>",
11558
+ " hydra-acp launch <agent> [agent-args...]",
11559
+ " Shim mode, force daemon to spawn <agent>",
11560
+ " from the registry. Args after <agent>",
11197
11561
  " are forwarded to the agent's command.",
11198
11562
  " hydra-acp --resume <id> Attach to an existing session (TUI when in a terminal, shim otherwise)",
11199
11563
  " hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
@@ -11214,14 +11578,15 @@ function printHelp() {
11214
11578
  " hydra-acp extensions logs <name> [-f] [-n N]Tail or follow an extension's log",
11215
11579
  " hydra-acp agents [list] List agents in the cached registry",
11216
11580
  " hydra-acp agents refresh Force a registry re-fetch",
11217
- " hydra-acp tui flags: [--resume [<id>]] [--new] [--agent-id <id>] [--cwd <path>] [--name <label>]",
11581
+ " hydra-acp tui flags: [--resume [<id>]] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
11218
11582
  " --resume <id> attaches to a specific session; bare --resume picks the most-recent",
11219
11583
  " in cwd. Smart default (no flags): picks if any live sessions exist, else new.",
11220
11584
  " hydra-acp --version Print version",
11221
11585
  " hydra-acp --help Show this help",
11222
11586
  "",
11223
11587
  "Config knob flags accept env-var equivalents (flag wins):",
11224
- " --agent-id HYDRA_ACP_AGENT_ID",
11588
+ " --agent HYDRA_ACP_AGENT",
11589
+ " --model HYDRA_ACP_MODEL (one-shot at session/new; ignored on --resume)",
11225
11590
  " --resume / --session-id HYDRA_ACP_SESSION_ID",
11226
11591
  " --name HYDRA_ACP_NAME",
11227
11592
  ""