@hydra-acp/cli 0.1.58 → 0.1.60

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
@@ -303,6 +303,34 @@ async function loadConfig() {
303
303
  await migrateLegacyAuthToken();
304
304
  return HydraConfig.parse(await readConfigFile());
305
305
  }
306
+ async function updateRawConfig(mutate) {
307
+ await migrateLegacyAuthToken();
308
+ const raw = await readConfigFile();
309
+ mutate(raw);
310
+ HydraConfig.parse(raw);
311
+ await writeJsonAtomic(paths.config(), raw, { mode: 384 });
312
+ }
313
+ async function setTuiConfigValue(key, value) {
314
+ await updateRawConfig((raw) => {
315
+ const tui = raw.tui && typeof raw.tui === "object" && !Array.isArray(raw.tui) ? raw.tui : {};
316
+ tui[key] = value;
317
+ raw.tui = tui;
318
+ });
319
+ }
320
+ async function setDefaultAgent(agentId, modelId) {
321
+ await updateRawConfig((raw) => {
322
+ raw.defaultAgent = agentId;
323
+ if (modelId !== void 0) {
324
+ const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
325
+ models[agentId] = modelId;
326
+ raw.defaultModels = models;
327
+ }
328
+ });
329
+ }
330
+ async function hasConfiguredDefaultAgent() {
331
+ const raw = await readConfigFile();
332
+ return typeof raw.defaultAgent === "string" && raw.defaultAgent.length > 0;
333
+ }
306
334
  function expandHome(p) {
307
335
  if (p === "~" || p === "$HOME") {
308
336
  return homedir2();
@@ -385,7 +413,7 @@ var init_config = __esm({
385
413
  // Width cap on the cwd column in the `sessions list` output and the
386
414
  // TUI picker. Set higher if you keep deeply-nested working directories
387
415
  // and want them visible; the elastic title column shrinks to make room.
388
- cwdColumnMaxWidth: z.number().int().positive().default(24),
416
+ cwdColumnMaxWidth: z.number().int().positive().default(32),
389
417
  // When true (default), emit OSC 9;4 progress-bar control codes so the
390
418
  // host terminal can show an indeterminate busy indicator (taskbar pulse
391
419
  // on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
@@ -436,7 +464,28 @@ var init_config = __esm({
436
464
  // The diff payload is extracted from the ACP wire (content[]
437
465
  // type:"diff" entries, falling back to rawInput shapes), so any agent
438
466
  // that emits one of those shapes gets the treatment.
439
- showFileUpdates: z.enum(["none", "edit", "diff"]).default("edit")
467
+ showFileUpdates: z.enum(["none", "edit", "diff"]).default("edit"),
468
+ // Columns shown in the `sessions list` output and the TUI picker, in
469
+ // the given order — so this controls both which columns appear and
470
+ // their left-to-right order. Valid names: session, upstream, host,
471
+ // state, agent, model, age, cwd, title, cost. Omit to use the built-in
472
+ // default (session, state, agent, age, cwd, title, cost — UPSTREAM,
473
+ // HOST, and MODEL hidden). The CLI's `--columns` flag overrides this
474
+ // per-invocation. Duplicate or unknown names are rejected.
475
+ sessionColumns: z.array(
476
+ z.enum([
477
+ "session",
478
+ "upstream",
479
+ "host",
480
+ "state",
481
+ "agent",
482
+ "model",
483
+ "age",
484
+ "cwd",
485
+ "title",
486
+ "cost"
487
+ ])
488
+ ).nonempty().optional()
440
489
  });
441
490
  ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
442
491
  ExtensionBody = z.object({
@@ -457,7 +506,7 @@ var init_config = __esm({
457
506
  HydraConfig = z.object({
458
507
  daemon: DaemonConfig.default({}),
459
508
  registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
460
- defaultAgent: z.string().default("claude-acp"),
509
+ defaultAgent: z.string().default("opencode"),
461
510
  // Optional per-agent default model id. When a brand-new agent process
462
511
  // is spawned (session/new path), hydra issues session/set_model with
463
512
  // the matching entry so the user lands on their preferred model from
@@ -505,7 +554,7 @@ var init_config = __esm({
505
554
  maxScrollbackLines: 1e4,
506
555
  mouse: false,
507
556
  logMaxBytes: 5 * 1024 * 1024,
508
- cwdColumnMaxWidth: 24,
557
+ cwdColumnMaxWidth: 32,
509
558
  progressIndicator: true,
510
559
  defaultEnterAction: "amend",
511
560
  showThoughts: true,
@@ -1122,7 +1171,8 @@ function sessionListEntryToWire(entry) {
1122
1171
  const hydraMeta = {
1123
1172
  attachedClients: entry.attachedClients,
1124
1173
  status: entry.status,
1125
- busy: entry.busy
1174
+ busy: entry.busy,
1175
+ awaitingInput: entry.awaitingInput
1126
1176
  };
1127
1177
  if (entry.agentId !== void 0) {
1128
1178
  hydraMeta.agentId = entry.agentId;
@@ -1239,6 +1289,17 @@ var init_types = __esm({
1239
1289
  // to a cold session does not resurrect or spawn an agent — just
1240
1290
  // streams history from disk. Used by the TUI's view-only mode.
1241
1291
  readonly: z3.boolean().optional(),
1292
+ // Debug-only replay pacing. When "drip", the daemon skips chunk
1293
+ // coalescing and re-emits each recorded session/update individually,
1294
+ // spacing them by their original recordedAt deltas (scaled by
1295
+ // dripSpeed, with a per-gap cap) so a session's streaming render can
1296
+ // be reproduced at its real granularity for flicker investigation.
1297
+ // Omitted/"instant" preserves the normal coalesced, as-fast-as-possible
1298
+ // replay.
1299
+ replayMode: z3.enum(["instant", "drip"]).optional(),
1300
+ // Multiplier applied to original inter-entry gaps in drip mode. >1
1301
+ // compresses time (faster), <1 stretches it. Defaults to 1.
1302
+ dripSpeed: z3.number().positive().optional(),
1242
1303
  _meta: z3.record(z3.unknown()).optional()
1243
1304
  });
1244
1305
  HYDRA_META_KEY = "hydra-acp";
@@ -1294,6 +1355,11 @@ var init_types = __esm({
1294
1355
  // Always false for cold sessions. Lets pickers render a busy dot
1295
1356
  // without having to attach.
1296
1357
  busy: z3.boolean().default(false),
1358
+ // True when the agent is blocked on the user (an outstanding
1359
+ // session/request_permission, which also covers agent-posed
1360
+ // questions). Always false for cold sessions. Lets pickers render a
1361
+ // distinct "waiting on you" glyph instead of the busy dot.
1362
+ awaitingInput: z3.boolean().default(false),
1297
1363
  _meta: z3.record(z3.unknown()).optional()
1298
1364
  });
1299
1365
  SessionListEntryWire = z3.object({
@@ -3064,6 +3130,14 @@ var init_session = __esm({
3064
3130
  get turnStartedAt() {
3065
3131
  return this.promptStartedAt;
3066
3132
  }
3133
+ // True when the agent is blocked on the user: an outstanding
3134
+ // session/request_permission (which also carries agent-posed
3135
+ // questions — there's no separate "ask the user" request in ACP).
3136
+ // Lets pickers show a "waiting on you" glyph distinct from the
3137
+ // "actively working" one without having to attach.
3138
+ get awaitingInput() {
3139
+ return this.inFlightPermissions.size > 0;
3140
+ }
3067
3141
  // Read the persisted history from disk. Returns [] if no history
3068
3142
  // file exists (fresh session, never prompted). Used by attach() and
3069
3143
  // the HTTP /history endpoint.
@@ -3120,23 +3194,24 @@ var init_session = __esm({
3120
3194
  return this.loadReplay(historyPolicy, opts);
3121
3195
  }
3122
3196
  async loadReplay(historyPolicy, opts) {
3197
+ const maybeCoalesce = (entries) => opts.raw ? entries : coalesceReplay(entries);
3123
3198
  const raw = await this.getHistorySnapshot();
3124
3199
  const state = this.buildStateSnapshotReplay();
3125
3200
  if (historyPolicy === "after_message") {
3126
3201
  const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
3127
3202
  if (cutoff < 0) {
3128
3203
  return {
3129
- entries: [...state, ...coalesceReplay(raw)],
3204
+ entries: [...state, ...maybeCoalesce(raw)],
3130
3205
  appliedPolicy: "full"
3131
3206
  };
3132
3207
  }
3133
3208
  return {
3134
- entries: [...state, ...coalesceReplay(raw.slice(cutoff + 1))],
3209
+ entries: [...state, ...maybeCoalesce(raw.slice(cutoff + 1))],
3135
3210
  appliedPolicy: "after_message"
3136
3211
  };
3137
3212
  }
3138
3213
  return {
3139
- entries: [...state, ...coalesceReplay(raw)],
3214
+ entries: [...state, ...maybeCoalesce(raw)],
3140
3215
  appliedPolicy: "full"
3141
3216
  };
3142
3217
  }
@@ -4403,6 +4478,7 @@ var init_session = __esm({
4403
4478
  const out = [
4404
4479
  { name: "hydra", description: "Hydra session command (kill, restart, title, agent <agent>)" },
4405
4480
  { name: "model", description: "Switch model; omit arg to list available models" },
4481
+ { name: "mode", description: "Switch mode; omit arg to list available modes" },
4406
4482
  { name: "sessions", description: "List all sessions" },
4407
4483
  { name: "help", description: "Show available commands" }
4408
4484
  ];
@@ -4673,6 +4749,60 @@ ${body}
4673
4749
  });
4674
4750
  return { stopReason: "end_turn" };
4675
4751
  }
4752
+ // /mode — the text-command twin of session/set_mode, so clients that
4753
+ // can only send prompts (e.g. Slack) can switch modes. With no arg it
4754
+ // lists advertised modes; with an arg it forwards session/set_mode to
4755
+ // the agent and then applies the change locally. The forward +
4756
+ // applyModeChange pair MUST stay identical to the daemon's
4757
+ // session/set_mode handler (see acp-ws.ts) — applyModeChange owns the
4758
+ // persistence + synthetic current_mode_update broadcast for both paths.
4759
+ async handleModeCommand(text) {
4760
+ const arg = text.slice("/mode".length).trim();
4761
+ if (arg === "") {
4762
+ const modes2 = this.agentAdvertisedModes;
4763
+ const current = this.currentMode;
4764
+ let body;
4765
+ if (modes2.length === 0) {
4766
+ body = current ? `Current mode: ${current}` : "_(no modes advertised yet)_";
4767
+ } else {
4768
+ const inList = current ? modes2.some((m) => m.id === current) : true;
4769
+ const lines = modes2.map((m) => {
4770
+ const marker = m.id === current ? "\u25B6 " : " ";
4771
+ const desc = m.name && m.name !== m.id ? ` ${m.name}` : "";
4772
+ return `${marker}${m.id}${desc}`;
4773
+ });
4774
+ if (!inList && current) {
4775
+ lines.unshift(`\u25B6 ${current}`);
4776
+ }
4777
+ body = lines.join("\n");
4778
+ }
4779
+ this.recordAndBroadcast("session/update", {
4780
+ sessionId: this.upstreamSessionId,
4781
+ update: {
4782
+ sessionUpdate: "agent_message_chunk",
4783
+ content: { type: "text", text: `
4784
+ ${body}
4785
+ ` },
4786
+ _meta: { "hydra-acp": { synthetic: true } }
4787
+ }
4788
+ });
4789
+ return { stopReason: "end_turn" };
4790
+ }
4791
+ const modes = this.agentAdvertisedModes;
4792
+ if (modes.length > 0 && !modes.some((m) => m.id === arg)) {
4793
+ const known = modes.map((m) => m.id).join(", ");
4794
+ throw withCode(
4795
+ new Error(`unknown mode: ${arg} (known: ${known})`),
4796
+ JsonRpcErrorCodes.InvalidParams
4797
+ );
4798
+ }
4799
+ await this.forwardRequest("session/set_mode", {
4800
+ sessionId: this.sessionId,
4801
+ modeId: arg
4802
+ });
4803
+ this.applyModeChange(arg);
4804
+ return { stopReason: "end_turn" };
4805
+ }
4676
4806
  // /hydra title. With an arg, sets the title directly (synchronous,
4677
4807
  // broadcasts session_info_update). Without an arg, asks the manager
4678
4808
  // to schedule a background synopsis via the ephemeral-agent path —
@@ -5498,12 +5628,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
5498
5628
  }
5499
5629
  this.broadcastPromptReceived(entry);
5500
5630
  const promptText = extractPromptText(entry.prompt).trim();
5501
- if (promptText === "/model" || promptText.startsWith("/model ") || promptText === "/sessions" || promptText === "/help") {
5631
+ if (promptText === "/model" || promptText.startsWith("/model ") || promptText === "/mode" || promptText.startsWith("/mode ") || promptText === "/sessions" || promptText === "/help") {
5502
5632
  let result;
5503
5633
  if (promptText === "/sessions") {
5504
5634
  result = await this.handleSessionsCommand();
5505
5635
  } else if (promptText === "/help") {
5506
5636
  result = await this.handleHelpCommand();
5637
+ } else if (promptText === "/mode" || promptText.startsWith("/mode ")) {
5638
+ result = await this.handleModeCommand(promptText);
5507
5639
  } else {
5508
5640
  result = await this.handleModelCommand(promptText);
5509
5641
  }
@@ -5561,7 +5693,12 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
5561
5693
  "current_mode_update",
5562
5694
  "available_commands_update",
5563
5695
  "available_modes_update",
5564
- "usage_update"
5696
+ "usage_update",
5697
+ // opencode's non-spec carrier for model/mode/config state. Its canonical
5698
+ // form is harvested into meta.json and re-synthesized on attach, so
5699
+ // recording it would just give never-prompted sessions a non-empty
5700
+ // history and make them look interactive in the picker.
5701
+ "config_option_update"
5565
5702
  ]);
5566
5703
  }
5567
5704
  });
@@ -6338,10 +6475,28 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
6338
6475
  forkedFromSessionId: s.forkedFromSessionId,
6339
6476
  forkedFromMessageId: s.forkedFromMessageId,
6340
6477
  busy: s.busy,
6478
+ awaitingInput: s.awaitingInput,
6341
6479
  originatingClient: s.originatingClient,
6342
6480
  interactive: s.interactive
6343
6481
  }));
6344
6482
  }
6483
+ async function listAgents(target, fetchImpl = fetch) {
6484
+ const response = await fetchImpl(`${target.baseUrl}/v1/agents`, {
6485
+ headers: { Authorization: `Bearer ${target.token}` }
6486
+ });
6487
+ if (!response.ok) {
6488
+ throw new Error(`daemon returned HTTP ${response.status}`);
6489
+ }
6490
+ const body = await response.json();
6491
+ if (!Array.isArray(body.agents)) {
6492
+ return [];
6493
+ }
6494
+ return body.agents.map((a) => ({
6495
+ id: a.id,
6496
+ name: a.name,
6497
+ description: a.description
6498
+ }));
6499
+ }
6345
6500
  async function forkSession(target, id, opts = {}, fetchImpl = fetch) {
6346
6501
  const response = await fetchImpl(
6347
6502
  `${target.baseUrl}/v1/sessions/${id}/fork`,
@@ -6469,30 +6624,24 @@ function formatAgentWithModel(agentId, model) {
6469
6624
  }
6470
6625
  return `${agent}${AGENT_MODEL_SEP}${short}`;
6471
6626
  }
6472
- function formatAgentCell(agentId, usage) {
6473
- const base = agentId ?? "?";
6627
+ function formatAgentCell(agentId) {
6628
+ return agentId ?? "?";
6629
+ }
6630
+ function formatCostCell(usage) {
6474
6631
  if (!usage || typeof usage.costAmount !== "number") {
6475
- return base;
6632
+ return "";
6476
6633
  }
6477
- const compact = formatCostCompact(usage.costAmount, usage.costCurrency);
6478
- if (compact === null) {
6479
- return base;
6634
+ const { costAmount, costCurrency } = usage;
6635
+ if (costCurrency === void 0 || costCurrency === "USD") {
6636
+ return `$${Math.round(costAmount)}`;
6480
6637
  }
6481
- return `${base} ${compact}`;
6638
+ return formatCost(costAmount, costCurrency);
6482
6639
  }
6483
6640
  function formatCost(amount, currency) {
6484
6641
  const sign = currency === "USD" || currency === void 0 ? "$" : "";
6485
6642
  const decimals = 2;
6486
6643
  return `${sign}${amount.toFixed(decimals)}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
6487
6644
  }
6488
- function formatCostCompact(amount, currency) {
6489
- const whole = Math.round(amount);
6490
- if (whole === 0) {
6491
- return null;
6492
- }
6493
- const sign = currency === "USD" || currency === void 0 ? "$" : "";
6494
- return `${sign}${whole}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
6495
- }
6496
6645
  var AGENT_MODEL_SEP;
6497
6646
  var init_agent_display = __esm({
6498
6647
  "src/core/agent-display.ts"() {
@@ -6502,15 +6651,39 @@ var init_agent_display = __esm({
6502
6651
  });
6503
6652
 
6504
6653
  // src/cli/session-row.ts
6654
+ function parseColumns(raw) {
6655
+ const parts = raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
6656
+ if (parts.length === 0) {
6657
+ throw new Error("--columns: no column names given");
6658
+ }
6659
+ const seen = /* @__PURE__ */ new Set();
6660
+ const out = [];
6661
+ for (const name of parts) {
6662
+ if (!ALL_COLUMNS.includes(name)) {
6663
+ throw new Error(
6664
+ `--columns: unknown column "${name}" (valid: ${ALL_COLUMNS.join(", ")})`
6665
+ );
6666
+ }
6667
+ if (seen.has(name)) {
6668
+ throw new Error(`--columns: duplicate column "${name}"`);
6669
+ }
6670
+ seen.add(name);
6671
+ out.push(name);
6672
+ }
6673
+ return out;
6674
+ }
6505
6675
  function toRow(s, now = Date.now()) {
6506
6676
  return {
6507
6677
  session: stripHydraSessionPrefix(s.sessionId),
6508
6678
  upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
6509
- state: formatState(s.status, s.busy),
6510
- agent: formatAgentCell(s.agentId, s.currentUsage),
6679
+ host: s.importedFromMachine ?? "-",
6680
+ state: formatState(s.status, s.busy, s.awaitingInput),
6681
+ agent: formatAgentCell(s.agentId),
6682
+ model: shortenModel(s.currentModel) ?? "-",
6511
6683
  age: formatRelativeAge(s.updatedAt, now),
6512
6684
  title: s.title ?? "-",
6513
- cwd: shortenHomePath(s.cwd)
6685
+ cwd: shortenHomePath(s.cwd),
6686
+ cost: formatCostCell(s.currentUsage)
6514
6687
  };
6515
6688
  }
6516
6689
  function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
@@ -6522,22 +6695,33 @@ function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
6522
6695
  }
6523
6696
  return "-";
6524
6697
  }
6525
- function formatState(status, busy) {
6698
+ function formatState(status, busy, awaitingInput) {
6526
6699
  if (status === "cold") {
6527
6700
  return "COLD";
6528
6701
  }
6702
+ if (awaitingInput) {
6703
+ return "LIVE\u25E6";
6704
+ }
6529
6705
  return busy ? "LIVE\u2022" : "LIVE";
6530
6706
  }
6531
- function computeWidths(rows) {
6532
- return {
6533
- session: maxLen(HEADER.session, rows.map((r) => r.session)),
6534
- upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
6535
- state: maxLen(HEADER.state, rows.map((r) => r.state)),
6536
- agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
6537
- age: maxLen(HEADER.age, rows.map((r) => r.age)),
6538
- cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
6539
- title: maxLen(HEADER.title, rows.map((r) => r.title))
6707
+ function computeWidths(rows, opts = {}) {
6708
+ const columns = opts.columns ?? DEFAULT_COLUMNS;
6709
+ const w = {
6710
+ session: 0,
6711
+ upstream: 0,
6712
+ host: 0,
6713
+ state: 0,
6714
+ agent: 0,
6715
+ model: 0,
6716
+ age: 0,
6717
+ cwd: 0,
6718
+ title: 0,
6719
+ cost: 0
6540
6720
  };
6721
+ for (const col of columns) {
6722
+ w[col] = maxLen(HEADER[col], rows.map((r) => r[col]));
6723
+ }
6724
+ return w;
6541
6725
  }
6542
6726
  function formatRelativeAge(iso, now) {
6543
6727
  if (!iso) {
@@ -6584,27 +6768,56 @@ function maxLen(headerCell, values) {
6584
6768
  }
6585
6769
  return max;
6586
6770
  }
6587
- function formatRow(r, w, maxWidth, cwdMaxWidth = DEFAULT_CWD_MAX_WIDTH) {
6588
- const fixed = [
6589
- r.session.padEnd(w.session),
6590
- r.upstream.padEnd(w.upstream),
6591
- r.state.padEnd(w.state),
6592
- r.agent.padEnd(w.agent),
6593
- r.age.padStart(w.age)
6594
- ].join(SEP);
6771
+ function formatRow(r, w, maxWidth, opts = {}) {
6772
+ const columns = opts.columns ?? DEFAULT_COLUMNS;
6773
+ const cwdMaxWidth = opts.cwdMaxWidth ?? DEFAULT_CWD_MAX_WIDTH;
6774
+ const naturalCell = (col) => col === "age" ? r[col].padStart(w[col]) : r[col].padEnd(w[col]);
6595
6775
  if (maxWidth === void 0) {
6596
- return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
6597
- }
6598
- const budget = maxWidth - fixed.length - SEP.length;
6599
- if (budget <= 0) {
6600
- return fixed.slice(0, maxWidth);
6776
+ const cells2 = columns.map(
6777
+ (col, i) => i === columns.length - 1 ? r[col] : naturalCell(col)
6778
+ );
6779
+ return cells2.join(SEP);
6780
+ }
6781
+ const elasticIdx = columns.map((col, i) => ({ col, i })).filter(({ col }) => ELASTIC_COLUMNS.has(col));
6782
+ const lastElastic = elasticIdx.length > 0 ? elasticIdx[elasticIdx.length - 1].i : -1;
6783
+ if (lastElastic === -1) {
6784
+ const out2 = columns.map(naturalCell).join(SEP);
6785
+ return out2.length > maxWidth ? out2.slice(0, maxWidth) : out2;
6786
+ }
6787
+ const fixedWidth = columns.filter((col) => !ELASTIC_COLUMNS.has(col)).reduce((sum, col) => sum + w[col], 0);
6788
+ const sepCount = Math.max(0, columns.length - 1);
6789
+ let budget = maxWidth - fixedWidth - sepCount * SEP.length;
6790
+ if (budget < 0) {
6791
+ budget = 0;
6792
+ }
6793
+ const elasticAlloc = /* @__PURE__ */ new Map();
6794
+ let remaining = budget;
6795
+ for (const { col, i } of elasticIdx) {
6796
+ if (i === lastElastic) {
6797
+ continue;
6798
+ }
6799
+ const natural = col === "cwd" ? Math.min(w[col], cwdMaxWidth) : w[col];
6800
+ const alloc = Math.min(natural, Math.max(0, remaining - 1));
6801
+ elasticAlloc.set(i, alloc);
6802
+ remaining = Math.max(0, remaining - alloc);
6601
6803
  }
6602
- const cwdCap = Math.min(w.cwd, cwdMaxWidth);
6603
- const cwdAlloc = Math.min(cwdCap, Math.max(0, budget - SEP.length - 1));
6604
- const cwdCell = truncateMiddle(r.cwd, cwdAlloc).padEnd(cwdAlloc);
6605
- const titleBudget = Math.max(0, budget - cwdAlloc - SEP.length);
6606
- const titleCell = truncateRight(r.title, titleBudget);
6607
- return [fixed, cwdCell, titleCell].join(SEP);
6804
+ elasticAlloc.set(lastElastic, Math.max(0, remaining));
6805
+ const lastCol = columns.length - 1;
6806
+ const renderElastic = (col, width, isLast) => {
6807
+ if (col === "cwd") {
6808
+ return truncateMiddle(r[col], width).padEnd(width);
6809
+ }
6810
+ const cell = truncateRight(r[col], width);
6811
+ return isLast ? cell : cell.padEnd(width);
6812
+ };
6813
+ const cells = columns.map((col, i) => {
6814
+ if (ELASTIC_COLUMNS.has(col)) {
6815
+ return renderElastic(col, elasticAlloc.get(i) ?? 0, i === lastCol);
6816
+ }
6817
+ return naturalCell(col);
6818
+ });
6819
+ const out = cells.join(SEP);
6820
+ return out.length > maxWidth ? out.slice(0, maxWidth) : out;
6608
6821
  }
6609
6822
  function truncateRight(s, max) {
6610
6823
  if (max <= 0) {
@@ -6632,24 +6845,49 @@ function truncateMiddle(s, max) {
6632
6845
  const tail = max - 1 - head;
6633
6846
  return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
6634
6847
  }
6635
- var HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
6848
+ var ALL_COLUMNS, DEFAULT_COLUMNS, ELASTIC_COLUMNS, HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
6636
6849
  var init_session_row = __esm({
6637
6850
  "src/cli/session-row.ts"() {
6638
6851
  "use strict";
6639
6852
  init_agent_display();
6640
6853
  init_paths();
6641
6854
  init_session();
6855
+ ALL_COLUMNS = [
6856
+ "session",
6857
+ "upstream",
6858
+ "host",
6859
+ "state",
6860
+ "agent",
6861
+ "model",
6862
+ "age",
6863
+ "cwd",
6864
+ "title",
6865
+ "cost"
6866
+ ];
6867
+ DEFAULT_COLUMNS = [
6868
+ "session",
6869
+ "state",
6870
+ "agent",
6871
+ "age",
6872
+ "cwd",
6873
+ "title",
6874
+ "cost"
6875
+ ];
6876
+ ELASTIC_COLUMNS = /* @__PURE__ */ new Set(["cwd", "title"]);
6642
6877
  HEADER = {
6643
6878
  session: "SESSION",
6644
6879
  upstream: "UPSTREAM",
6880
+ host: "HOST",
6645
6881
  state: "STATE",
6646
6882
  agent: "AGENT",
6883
+ model: "MODEL",
6647
6884
  age: "AGE",
6648
6885
  title: "TITLE",
6649
- cwd: "CWD"
6886
+ cwd: "CWD",
6887
+ cost: "COST"
6650
6888
  };
6651
6889
  SEP = " ";
6652
- DEFAULT_CWD_MAX_WIDTH = 24;
6890
+ DEFAULT_CWD_MAX_WIDTH = 32;
6653
6891
  }
6654
6892
  });
6655
6893
 
@@ -7202,7 +7440,21 @@ function formatBlock(text, prefix, bodyStyle, prefixStyle, sentBy, fillRow) {
7202
7440
  }
7203
7441
  return out;
7204
7442
  }
7205
- function formatToolLine2(state) {
7443
+ function formatElapsed(ms) {
7444
+ const totalSec = Math.floor(ms / 1e3);
7445
+ if (totalSec < 60) {
7446
+ return `${totalSec}s`;
7447
+ }
7448
+ const min = Math.floor(totalSec / 60);
7449
+ const sec = totalSec % 60;
7450
+ if (min < 60) {
7451
+ return sec === 0 ? `${min}m` : `${min}m ${sec}s`;
7452
+ }
7453
+ const hr = Math.floor(min / 60);
7454
+ const remMin = min % 60;
7455
+ return remMin === 0 ? `${hr}h` : `${hr}h ${remMin}m`;
7456
+ }
7457
+ function formatToolLine2(state, now = Date.now()) {
7206
7458
  const initial = state.initialTitle;
7207
7459
  const latest = state.latestTitle;
7208
7460
  const initialLc = initial.toLowerCase();
@@ -7215,6 +7467,10 @@ function formatToolLine2(state) {
7215
7467
  } else {
7216
7468
  title = `${initial} \xB7 ${latest}`;
7217
7469
  }
7470
+ if (state.startedAt !== void 0) {
7471
+ const end = state.endedAt ?? now;
7472
+ title = `${title} \xB7 ${formatElapsed(end - state.startedAt)}`;
7473
+ }
7218
7474
  const lines = [
7219
7475
  {
7220
7476
  prefix: ` ${toolStatusIcon(state.status)} `,
@@ -7234,22 +7490,32 @@ function formatToolLine2(state) {
7234
7490
  }
7235
7491
  function formatEditDiffBlock(diff, mode) {
7236
7492
  const lines = [];
7237
- if (diff.path) {
7238
- lines.push({
7239
- prefix: " ",
7240
- body: `\u25B8 Edited ${sanitizeSingleLine(shortenHomePath(diff.path))}`,
7241
- bodyStyle: "dim"
7242
- });
7243
- }
7493
+ const header = (open2) => ({
7494
+ prefix: " ",
7495
+ body: `${open2 ? "\u25BE" : "\u25B8"} Edited ${sanitizeSingleLine(shortenHomePath(diff.path))}`,
7496
+ bodyStyle: "dim"
7497
+ });
7244
7498
  if (mode === "edit") {
7499
+ if (diff.path) {
7500
+ lines.push(header(false));
7501
+ }
7245
7502
  return lines;
7246
7503
  }
7247
- const body = buildUnifiedDiff(diff);
7504
+ const body = buildUnifiedDiff(diff, { maxLines: Infinity });
7248
7505
  if (body.length === 0) {
7506
+ if (diff.path) {
7507
+ lines.push(header(false));
7508
+ }
7249
7509
  return lines;
7250
7510
  }
7511
+ if (diff.path) {
7512
+ lines.push(header(true));
7513
+ }
7251
7514
  const fenced = "```diff\n" + body + "\n```";
7252
7515
  lines.push(...parseAgentMarkdown(fenced));
7516
+ if (lines.length > 0) {
7517
+ lines.unshift({ body: "" });
7518
+ }
7253
7519
  return lines;
7254
7520
  }
7255
7521
  function buildUnifiedDiff(diff, opts = {}) {
@@ -7324,6 +7590,20 @@ function diffLines(a, b) {
7324
7590
  }
7325
7591
  return out;
7326
7592
  }
7593
+ function isTerminalToolStatus(status) {
7594
+ switch (status) {
7595
+ case "completed":
7596
+ case "succeeded":
7597
+ case "ok":
7598
+ case "failed":
7599
+ case "error":
7600
+ case "rejected":
7601
+ case "cancelled":
7602
+ return true;
7603
+ default:
7604
+ return false;
7605
+ }
7606
+ }
7327
7607
  function toolStatusIcon(status) {
7328
7608
  switch (status) {
7329
7609
  case "completed":
@@ -8243,7 +8523,7 @@ var init_input = __esm({
8243
8523
  case "ctrl-n":
8244
8524
  return this.handleDown();
8245
8525
  case "ctrl-o":
8246
- return [{ type: "toggle-tools" }];
8526
+ return [{ type: "toggle-options" }];
8247
8527
  case "backspace":
8248
8528
  this.backspace();
8249
8529
  return [];
@@ -8256,11 +8536,6 @@ var init_input = __esm({
8256
8536
  if (this.bufferIsEmpty()) {
8257
8537
  return [{ type: "exit" }];
8258
8538
  }
8259
- const lastLine = this.buffer[this.buffer.length - 1] ?? "";
8260
- const atEndOfBuffer = this.row === this.buffer.length - 1 && this.col >= lastLine.length;
8261
- if (atEndOfBuffer) {
8262
- return [{ type: "exit" }];
8263
- }
8264
8539
  this.deleteForward();
8265
8540
  return [];
8266
8541
  }
@@ -9523,20 +9798,6 @@ function formatUsage(usage) {
9523
9798
  }
9524
9799
  return parts.length === 0 ? null : parts.join(" \xB7 ");
9525
9800
  }
9526
- function formatElapsed(ms) {
9527
- const totalSec = Math.floor(ms / 1e3);
9528
- if (totalSec < 60) {
9529
- return `${totalSec}s`;
9530
- }
9531
- const min = Math.floor(totalSec / 60);
9532
- const sec = totalSec % 60;
9533
- if (min < 60) {
9534
- return sec === 0 ? `${min}m` : `${min}m ${sec}s`;
9535
- }
9536
- const hr = Math.floor(min / 60);
9537
- const remMin = min % 60;
9538
- return remMin === 0 ? `${hr}h` : `${hr}h ${remMin}m`;
9539
- }
9540
9801
  function formatTokens(n) {
9541
9802
  if (n >= 1e6) {
9542
9803
  return `${(n / 1e6).toFixed(1)}M`;
@@ -9755,7 +10016,7 @@ function mapCsiUToKeyName(code, mod) {
9755
10016
  }
9756
10017
  return null;
9757
10018
  }
9758
- var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_HELP_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
10019
+ var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_OPTIONS_ROWS, MAX_HELP_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
9759
10020
  var init_screen = __esm({
9760
10021
  "src/tui/screen.ts"() {
9761
10022
  "use strict";
@@ -9763,6 +10024,7 @@ var init_screen = __esm({
9763
10024
  init_paths();
9764
10025
  init_session();
9765
10026
  init_attachments();
10027
+ init_format();
9766
10028
  init_sync();
9767
10029
  SESSIONBAR_ROWS = 1;
9768
10030
  BANNER_ROWS = 1;
@@ -9770,6 +10032,7 @@ var init_screen = __esm({
9770
10032
  MAX_PROMPT_ROWS = 8;
9771
10033
  MAX_QUEUED_ROWS = 5;
9772
10034
  MAX_PERMISSION_ROWS = 12;
10035
+ MAX_OPTIONS_ROWS = 12;
9773
10036
  MAX_HELP_ROWS = 30;
9774
10037
  MAX_COMPLETION_ROWS = 6;
9775
10038
  MAX_CHIP_ROWS = 4;
@@ -9844,6 +10107,7 @@ var init_screen = __esm({
9844
10107
  lastFrameW = 0;
9845
10108
  lastFrameH = 0;
9846
10109
  permissionPrompt = null;
10110
+ optionsPrompt = null;
9847
10111
  confirmPrompt = null;
9848
10112
  helpPrompt = null;
9849
10113
  completions = [];
@@ -10013,6 +10277,7 @@ var init_screen = __esm({
10013
10277
  this.writeProgressIndicator(0);
10014
10278
  this.started = false;
10015
10279
  if (!opts.keepFullscreen) {
10280
+ emergencyTerminalReset();
10016
10281
  this.term.fullscreen(false);
10017
10282
  this.term("\n");
10018
10283
  }
@@ -10186,6 +10451,11 @@ uncaught: ${err.stack ?? err.message}
10186
10451
  }
10187
10452
  if (text.includes("\n")) {
10188
10453
  const parts = text.split("\n");
10454
+ const nonEmpty = parts.filter((p) => p.length > 0).length;
10455
+ if (nonEmpty > 1) {
10456
+ this.onKey([{ type: "paste", text: text.replace(/\r/g, "") }]);
10457
+ return;
10458
+ }
10189
10459
  for (let i = 0; i < parts.length; i++) {
10190
10460
  if (parts[i].length > 0) {
10191
10461
  this.handleRawStdin(Buffer.from(parts[i], "binary"));
@@ -10713,6 +10983,41 @@ uncaught: ${err.stack ?? err.message}
10713
10983
  clearKey(key) {
10714
10984
  this.keyedBlocks.delete(key);
10715
10985
  }
10986
+ // Whether a keyed block currently exists in scrollback.
10987
+ hasKey(key) {
10988
+ return this.keyedBlocks.has(key);
10989
+ }
10990
+ // Splice a keyed block's lines out of scrollback entirely (unlike
10991
+ // clearKey, which only forgets the key but leaves the lines painted).
10992
+ // Later blocks' start indices shift up by the removed count so they
10993
+ // stay aligned with the lines array. No-op if the key is unknown.
10994
+ removeKey(key) {
10995
+ const block = this.keyedBlocks.get(key);
10996
+ if (!block) {
10997
+ return;
10998
+ }
10999
+ const end = block.start + block.count;
11000
+ const touchesEnd = end >= this.lines.length;
11001
+ const removedRows = this.wrappedRowsOfMany(
11002
+ this.lines.slice(block.start, end)
11003
+ );
11004
+ const removed = this.lines.splice(block.start, block.count);
11005
+ for (const line of removed) {
11006
+ this.forgetLine(line);
11007
+ }
11008
+ this.keyedBlocks.delete(key);
11009
+ for (const [k, range] of this.keyedBlocks) {
11010
+ if (k !== key && range.start > block.start) {
11011
+ range.start -= block.count;
11012
+ }
11013
+ }
11014
+ if (touchesEnd) {
11015
+ this.streamingActive = false;
11016
+ }
11017
+ this.adjustScrollForRowChange(-removedRows);
11018
+ this.moveStickyToEnd();
11019
+ this.scheduleRepaint();
11020
+ }
10716
11021
  // Mark `key` as the sticky-bottom block. While set, whenever new content
10717
11022
  // lands after the block's lines (appendLines / appendStreaming / a new
10718
11023
  // upserted block) the screen floats this block back to the end so it
@@ -10831,6 +11136,15 @@ uncaught: ${err.stack ?? err.message}
10831
11136
  this.permissionPrompt = spec ? { ...spec } : null;
10832
11137
  this.repaint();
10833
11138
  }
11139
+ // Interactive session-options modal (^O). Takes over the prompt area
11140
+ // like the permission modal. Pass null to dismiss.
11141
+ setOptionsPrompt(spec) {
11142
+ this.optionsPrompt = spec ? { ...spec, options: spec.options.map((o) => ({ ...o })) } : null;
11143
+ this.repaint();
11144
+ }
11145
+ isOptionsPromptActive() {
11146
+ return this.optionsPrompt !== null;
11147
+ }
10834
11148
  // Two-line confirmation modal that takes over the prompt area. Pass
10835
11149
  // null to dismiss. Currently unused — kept as a generic primitive for
10836
11150
  // any future modal that needs a question + hint footer.
@@ -11321,7 +11635,7 @@ uncaught: ${err.stack ?? err.message}
11321
11635
  this.drawSeparator(h - SESSIONBAR_ROWS);
11322
11636
  this.drawSessionbar();
11323
11637
  this.placeCursor();
11324
- if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
11638
+ if (this.permissionPrompt || this.optionsPrompt || this.confirmPrompt || this.helpPrompt) {
11325
11639
  this.term.hideCursor(false);
11326
11640
  }
11327
11641
  this.lastPromptRows = promptRows;
@@ -11425,7 +11739,7 @@ uncaught: ${err.stack ?? err.message}
11425
11739
  this.repaint();
11426
11740
  }
11427
11741
  completionRows() {
11428
- if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
11742
+ if (this.permissionPrompt || this.optionsPrompt || this.confirmPrompt || this.helpPrompt) {
11429
11743
  return 0;
11430
11744
  }
11431
11745
  return Math.min(MAX_COMPLETION_ROWS, this.completions.length);
@@ -11565,6 +11879,10 @@ uncaught: ${err.stack ?? err.message}
11565
11879
  this.drawPermissionPrompt();
11566
11880
  return;
11567
11881
  }
11882
+ if (this.optionsPrompt) {
11883
+ this.drawOptionsPrompt();
11884
+ return;
11885
+ }
11568
11886
  if (this.confirmPrompt) {
11569
11887
  this.drawConfirmPrompt();
11570
11888
  return;
@@ -11787,6 +12105,14 @@ uncaught: ${err.stack ?? err.message}
11787
12105
  this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
11788
12106
  return;
11789
12107
  }
12108
+ if (this.optionsPrompt) {
12109
+ const rows = this.optionsRows();
12110
+ const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
12111
+ const optionRow = top2 + 1 + this.optionsPrompt.selectedIndex;
12112
+ const lastUsableRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
12113
+ this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
12114
+ return;
12115
+ }
11790
12116
  if (this.confirmPrompt) {
11791
12117
  const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
11792
12118
  this.term.moveTo(2, top2);
@@ -11825,6 +12151,9 @@ uncaught: ${err.stack ?? err.message}
11825
12151
  if (this.permissionPrompt) {
11826
12152
  return this.permissionRows();
11827
12153
  }
12154
+ if (this.optionsPrompt) {
12155
+ return this.optionsRows();
12156
+ }
11828
12157
  if (this.confirmPrompt) {
11829
12158
  return CONFIRM_PROMPT_ROWS;
11830
12159
  }
@@ -11849,6 +12178,64 @@ uncaught: ${err.stack ?? err.message}
11849
12178
  4 + this.permissionPrompt.options.length
11850
12179
  );
11851
12180
  }
12181
+ optionsRows() {
12182
+ if (!this.optionsPrompt) {
12183
+ return 0;
12184
+ }
12185
+ return Math.min(MAX_OPTIONS_ROWS, 2 + this.optionsPrompt.options.length);
12186
+ }
12187
+ drawOptionsPrompt() {
12188
+ const spec = this.optionsPrompt;
12189
+ if (!spec) {
12190
+ return;
12191
+ }
12192
+ const w = this.term.width;
12193
+ const rows = this.optionsRows();
12194
+ const top = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
12195
+ let row = top;
12196
+ const writeRow = (sig, paint) => {
12197
+ if (row >= top + rows) {
12198
+ return;
12199
+ }
12200
+ this.paintRow(row, sig, paint);
12201
+ row += 1;
12202
+ };
12203
+ writeRow(`opts|t|${w}|${spec.title}`, () => {
12204
+ this.term.brightYellow(` \u2699 ${truncate(spec.title, w - 5)}`);
12205
+ });
12206
+ const labelWidth = Math.max(
12207
+ ...spec.options.map((o) => o.label.length),
12208
+ 0
12209
+ );
12210
+ for (let i = 0; i < spec.options.length; i++) {
12211
+ if (row >= top + rows - 1) {
12212
+ break;
12213
+ }
12214
+ const opt = spec.options[i];
12215
+ if (!opt) {
12216
+ continue;
12217
+ }
12218
+ const isSel = i === spec.selectedIndex;
12219
+ const marker = isSel ? "\u276F" : " ";
12220
+ const prefix = ` ${marker} ${i + 1}. `;
12221
+ const paddedLabel = opt.label.padEnd(labelWidth);
12222
+ const room = w - prefix.length - 3;
12223
+ const body = `${prefix}${truncate(`${paddedLabel} ${opt.value}`, room)}`;
12224
+ writeRow(
12225
+ `opts|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.value}|${opt.label}`,
12226
+ () => {
12227
+ if (isSel) {
12228
+ this.term.brightYellow(body);
12229
+ } else {
12230
+ this.term.dim(body);
12231
+ }
12232
+ }
12233
+ );
12234
+ }
12235
+ writeRow(`opts|hint|${w}`, () => {
12236
+ this.term.dim(" \u2191/\u2193 choose \xB7 Enter this session \xB7 s save default \xB7 Esc close");
12237
+ });
12238
+ }
11852
12239
  // Walk this.lines from the tail, accumulating wrapped rows via the
11853
12240
  // wrap cache, until we have at least `needed` rows or run out. Returns
11854
12241
  // the collected rows in original (top-down) order plus an `exhausted`
@@ -12023,7 +12410,7 @@ function drawBox(term, opts) {
12023
12410
  const termW = readTermWidth(term);
12024
12411
  const termH = readTermHeight(term);
12025
12412
  const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
12026
- const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
12413
+ const maxContentW = Math.max(10, termW - 4);
12027
12414
  const contentW = Math.min(desiredContentW, maxContentW);
12028
12415
  const w = contentW + 2;
12029
12416
  const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
@@ -12473,8 +12860,12 @@ async function pickSession(term, opts) {
12473
12860
  return base;
12474
12861
  };
12475
12862
  let visible = applyPrefsFilters(allSessions);
12863
+ const formatOpts = {
12864
+ columns: opts.config.tui.sessionColumns ?? DEFAULT_COLUMNS,
12865
+ cwdMaxWidth: opts.config.tui.cwdColumnMaxWidth
12866
+ };
12476
12867
  let rows = visible.map((s) => toRow(s, Date.now()));
12477
- let widths = computeWidths(rows);
12868
+ let widths = computeWidths(rows, formatOpts);
12478
12869
  let total = 1 + visible.length;
12479
12870
  let selectedIdx = 0;
12480
12871
  let scrollOffset = 0;
@@ -12501,7 +12892,11 @@ async function pickSession(term, opts) {
12501
12892
  let findInFlight = false;
12502
12893
  let renameBuffer = "";
12503
12894
  let transientStatus = null;
12895
+ let currentSessionGone = false;
12504
12896
  const composer = new InputDispatcher({ history: [] });
12897
+ if (opts.initialPrompt) {
12898
+ composer.setBuffer(opts.initialPrompt);
12899
+ }
12505
12900
  const composerHistoryCap = opts.config.tui.promptHistoryMaxEntries;
12506
12901
  loadHistory(paths.globalTuiHistoryFile()).then((entries) => {
12507
12902
  const capped = entries.length > composerHistoryCap ? entries.slice(entries.length - composerHistoryCap) : entries;
@@ -12526,7 +12921,6 @@ async function pickSession(term, opts) {
12526
12921
  let findBoxWindowStart = 0;
12527
12922
  let findBoxCursorVisualRow = 0;
12528
12923
  let findBoxCursorVisualCol = 0;
12529
- const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
12530
12924
  const computeLayout = () => {
12531
12925
  termHeight = readTermHeight2(term);
12532
12926
  termWidth = readTermWidth2(term);
@@ -12548,16 +12942,16 @@ async function pickSession(term, opts) {
12548
12942
  const reserved = 6 + composerRows;
12549
12943
  const maxViewportRows = Math.max(3, termHeight - reserved);
12550
12944
  viewportSize = Math.min(visible.length, maxViewportRows);
12551
- headerLine = formatRow(HEADER, widths, rowMaxWidth, cwdMaxWidth).padEnd(
12945
+ headerLine = formatRow(HEADER, widths, rowMaxWidth, formatOpts).padEnd(
12552
12946
  rowMaxWidth
12553
12947
  );
12554
12948
  sessionLines = rows.map(
12555
- (r) => formatRow(r, widths, rowMaxWidth, cwdMaxWidth).padEnd(rowMaxWidth)
12949
+ (r) => formatRow(r, widths, rowMaxWidth, formatOpts).padEnd(rowMaxWidth)
12556
12950
  );
12557
12951
  };
12558
12952
  const rebuildRows = () => {
12559
12953
  rows = visible.map((s) => toRow(s, Date.now()));
12560
- widths = computeWidths(rows);
12954
+ widths = computeWidths(rows, formatOpts);
12561
12955
  total = 1 + visible.length;
12562
12956
  computeLayout();
12563
12957
  };
@@ -13300,9 +13694,19 @@ async function pickSession(term, opts) {
13300
13694
  term.moveTo(1, indicatorRow() + 1);
13301
13695
  term("\n");
13302
13696
  };
13697
+ const tryAbort = () => {
13698
+ if (currentSessionGone) {
13699
+ transientStatus = "current session ended \u2014 pick a session or start a new one";
13700
+ paintIndicator();
13701
+ return false;
13702
+ }
13703
+ cleanup();
13704
+ resolve8({ kind: "abort" });
13705
+ return true;
13706
+ };
13303
13707
  const renderFingerprint = () => {
13304
13708
  const cells = rows.map(
13305
- (r) => `${r.session}|${r.upstream}|${r.state}|${r.agent}|${r.age}|${r.title}|${r.cwd}`
13709
+ (r) => `${r.session}|${r.upstream}|${r.host}|${r.state}|${r.agent}|${r.model}|${r.age}|${r.title}|${r.cwd}|${r.cost}`
13306
13710
  ).join("\n");
13307
13711
  return `${selectedIdx}:${scrollOffset}:${transientStatus ?? ""}
13308
13712
  ${cells}`;
@@ -13392,6 +13796,9 @@ ${cells}`;
13392
13796
  }
13393
13797
  mode = "normal";
13394
13798
  pendingAction = null;
13799
+ if (session.sessionId === opts.currentSessionId) {
13800
+ currentSessionGone = true;
13801
+ }
13395
13802
  await refresh(kind === "kill" ? session.sessionId : void 0);
13396
13803
  } catch (err) {
13397
13804
  mode = "normal";
@@ -13738,8 +14145,7 @@ ${cells}`;
13738
14145
  }
13739
14146
  if (selectedIdx === 0 && !searchActive) {
13740
14147
  if (name === "ESCAPE") {
13741
- cleanup();
13742
- resolve8({ kind: "abort" });
14148
+ tryAbort();
13743
14149
  return;
13744
14150
  }
13745
14151
  if (name === "ENTER" || name === "KP_ENTER") {
@@ -13792,8 +14198,7 @@ ${cells}`;
13792
14198
  const after = composer.state();
13793
14199
  const unchanged = before.buffer.length === after.buffer.length && before.buffer.every((line, i) => line === after.buffer[i]) && before.row === after.row && before.col === after.col;
13794
14200
  if (effects.some((e) => e.type === "exit")) {
13795
- cleanup();
13796
- resolve8({ kind: "abort" });
14201
+ tryAbort();
13797
14202
  return;
13798
14203
  }
13799
14204
  if (unchanged) {
@@ -13866,8 +14271,7 @@ ${cells}`;
13866
14271
  return;
13867
14272
  }
13868
14273
  if (name === "q" || name === "Q") {
13869
- cleanup();
13870
- resolve8({ kind: "abort" });
14274
+ tryAbort();
13871
14275
  return;
13872
14276
  }
13873
14277
  if (name === "o" || name === "O") {
@@ -14043,8 +14447,7 @@ ${cells}`;
14043
14447
  case "ESCAPE":
14044
14448
  case "CTRL_C":
14045
14449
  case "CTRL_D":
14046
- cleanup();
14047
- resolve8({ kind: "abort" });
14450
+ tryAbort();
14048
14451
  return;
14049
14452
  }
14050
14453
  };
@@ -14093,6 +14496,9 @@ function sortSessions(sessions, cwd) {
14093
14496
  return 0;
14094
14497
  }
14095
14498
  const base = s.cwd === cwd ? 2 : 1;
14499
+ if (s.awaitingInput) {
14500
+ return base + 4;
14501
+ }
14096
14502
  return s.busy ? base + 2 : base;
14097
14503
  };
14098
14504
  return [...sessions].sort((a, b) => {
@@ -14409,28 +14815,218 @@ async function promptForImportCwd(term, session, opts = {}) {
14409
14815
  }
14410
14816
  return;
14411
14817
  }
14412
- if (name === "CTRL_U") {
14413
- buffer = "";
14414
- errorLine = null;
14415
- repaintInput();
14818
+ if (name === "CTRL_U") {
14819
+ buffer = "";
14820
+ errorLine = null;
14821
+ repaintInput();
14822
+ return;
14823
+ }
14824
+ if (name === "CTRL_W") {
14825
+ const trimmedRight = buffer.replace(/[/\s]+$/, "");
14826
+ const lastSep = Math.max(
14827
+ trimmedRight.lastIndexOf("/"),
14828
+ trimmedRight.lastIndexOf(" ")
14829
+ );
14830
+ buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
14831
+ errorLine = null;
14832
+ repaintInput();
14833
+ return;
14834
+ }
14835
+ if (data?.isCharacter) {
14836
+ buffer += name;
14837
+ errorLine = null;
14838
+ repaintInput();
14839
+ return;
14840
+ }
14841
+ };
14842
+ term.grabInput({});
14843
+ term.on("key", onKey);
14844
+ term.on("resize", onResize);
14845
+ });
14846
+ }
14847
+ function truncate3(s, max) {
14848
+ if (max <= 1) {
14849
+ return "";
14850
+ }
14851
+ if (s.length <= max) {
14852
+ return s;
14853
+ }
14854
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
14855
+ }
14856
+ function truncateLeft(s, max) {
14857
+ if (max <= 1) {
14858
+ return "";
14859
+ }
14860
+ if (s.length <= max) {
14861
+ return s;
14862
+ }
14863
+ return "\u2026" + s.slice(s.length - (max - 1));
14864
+ }
14865
+ var init_import_cwd_prompt = __esm({
14866
+ "src/tui/import-cwd-prompt.ts"() {
14867
+ "use strict";
14868
+ init_paths();
14869
+ init_session();
14870
+ init_cwd();
14871
+ init_completion();
14872
+ init_prompt_utils();
14873
+ }
14874
+ });
14875
+
14876
+ // src/tui/agent-prompt.ts
14877
+ function initialIndex(agents) {
14878
+ const idx = agents.findIndex((a) => a.id === PREFERRED_DEFAULT);
14879
+ return idx === -1 ? 0 : idx;
14880
+ }
14881
+ async function promptForAgent(term, agents) {
14882
+ resetTerminalModes();
14883
+ let selected = initialIndex(agents);
14884
+ let windowStart = 0;
14885
+ const visibleRows = () => {
14886
+ const termBudget = readTermHeight(term) - 8;
14887
+ return Math.max(1, Math.min(MAX_VISIBLE_ROWS, agents.length, termBudget));
14888
+ };
14889
+ const reclamp = () => {
14890
+ const rows = visibleRows();
14891
+ if (selected < windowStart) {
14892
+ windowStart = selected;
14893
+ } else if (selected >= windowStart + rows) {
14894
+ windowStart = selected - rows + 1;
14895
+ }
14896
+ const maxStart = Math.max(0, agents.length - rows);
14897
+ if (windowStart > maxStart) {
14898
+ windowStart = maxStart;
14899
+ }
14900
+ if (windowStart < 0) {
14901
+ windowStart = 0;
14902
+ }
14903
+ };
14904
+ const render = () => {
14905
+ reclamp();
14906
+ const rows = visibleRows();
14907
+ const contentHeight = rows + 4;
14908
+ const contentWidth = Math.min(
14909
+ PREFERRED_CONTENT_WIDTH,
14910
+ Math.max(40, readTermWidth(term) - 8)
14911
+ );
14912
+ const layout = drawBox(term, {
14913
+ contentHeight,
14914
+ contentWidth,
14915
+ title: "Select agent"
14916
+ });
14917
+ const innerW = layout.contentW;
14918
+ let row = 0;
14919
+ term.moveTo(layout.contentX, layout.contentY + row);
14920
+ term.noFormat(" Which agent should this session use?");
14921
+ row += 2;
14922
+ const end = Math.min(agents.length, windowStart + rows);
14923
+ for (let i = windowStart; i < end; i++) {
14924
+ const agent = agents[i];
14925
+ if (!agent) {
14926
+ continue;
14927
+ }
14928
+ const pointer = i === selected ? "\u276F" : " ";
14929
+ const desc = agent.description ?? agent.name;
14930
+ const idPart = ` ${pointer} ${agent.id}`;
14931
+ term.moveTo(layout.contentX, layout.contentY + row);
14932
+ if (i === selected) {
14933
+ const line = `${idPart} ${desc}`;
14934
+ term.brightWhite.bgBlue.noFormat(padRight2(truncate4(line, innerW), innerW));
14935
+ } else {
14936
+ term.noFormat(idPart);
14937
+ const room = innerW - idPart.length - 2;
14938
+ if (room > 1) {
14939
+ term.dim.noFormat(` ${truncate4(desc, room)}`);
14940
+ }
14941
+ }
14942
+ row++;
14943
+ }
14944
+ row++;
14945
+ term.moveTo(layout.contentX, layout.contentY + row);
14946
+ const more = agents.length > rows ? ` (${selected + 1}/${agents.length})` : "";
14947
+ term.dim.noFormat(
14948
+ ` \u2191/\u2193 navigate \xB7 Enter this session \xB7 s set default \xB7 Esc back${more}`
14949
+ );
14950
+ return layout;
14951
+ };
14952
+ render();
14953
+ term.hideCursor();
14954
+ return await new Promise((resolve8) => {
14955
+ let resolved = false;
14956
+ const cleanup = () => {
14957
+ if (resolved) {
14958
+ return;
14959
+ }
14960
+ resolved = true;
14961
+ term.off("key", onKey);
14962
+ term.off("resize", onResize);
14963
+ term.grabInput(false);
14964
+ term.hideCursor(false);
14965
+ term.moveTo(1, 1).eraseDisplayBelow();
14966
+ };
14967
+ const finish = (value) => {
14968
+ cleanup();
14969
+ resolve8(value);
14970
+ };
14971
+ const onResize = () => {
14972
+ if (resolved) {
14973
+ return;
14974
+ }
14975
+ render();
14976
+ };
14977
+ const moveDown = () => {
14978
+ if (selected < agents.length - 1) {
14979
+ selected++;
14980
+ render();
14981
+ }
14982
+ };
14983
+ const moveUp = () => {
14984
+ if (selected > 0) {
14985
+ selected--;
14986
+ render();
14987
+ }
14988
+ };
14989
+ const onKey = (name, _m, data) => {
14990
+ if (name === "CTRL_C" || name === "CTRL_D") {
14991
+ finish({ kind: "cancel" });
14992
+ return;
14993
+ }
14994
+ if (name === "ESCAPE") {
14995
+ finish({ kind: "back" });
14996
+ return;
14997
+ }
14998
+ if (name === "ENTER" || name === "KP_ENTER") {
14999
+ const agent = agents[selected];
15000
+ if (agent) {
15001
+ finish({ kind: "select", agentId: agent.id, persist: false });
15002
+ }
15003
+ return;
15004
+ }
15005
+ if (name === "UP" || name === "SHIFT_TAB") {
15006
+ moveUp();
14416
15007
  return;
14417
15008
  }
14418
- if (name === "CTRL_W") {
14419
- const trimmedRight = buffer.replace(/[/\s]+$/, "");
14420
- const lastSep = Math.max(
14421
- trimmedRight.lastIndexOf("/"),
14422
- trimmedRight.lastIndexOf(" ")
14423
- );
14424
- buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
14425
- errorLine = null;
14426
- repaintInput();
15009
+ if (name === "DOWN" || name === "TAB") {
15010
+ moveDown();
14427
15011
  return;
14428
15012
  }
14429
15013
  if (data?.isCharacter) {
14430
- buffer += name;
14431
- errorLine = null;
14432
- repaintInput();
14433
- return;
15014
+ const lower = name.toLowerCase();
15015
+ if (lower === "s") {
15016
+ const agent = agents[selected];
15017
+ if (agent) {
15018
+ finish({ kind: "select", agentId: agent.id, persist: true });
15019
+ }
15020
+ return;
15021
+ }
15022
+ if (lower === "j") {
15023
+ moveDown();
15024
+ return;
15025
+ }
15026
+ if (lower === "k") {
15027
+ moveUp();
15028
+ return;
15029
+ }
14434
15030
  }
14435
15031
  };
14436
15032
  term.grabInput({});
@@ -14438,7 +15034,7 @@ async function promptForImportCwd(term, session, opts = {}) {
14438
15034
  term.on("resize", onResize);
14439
15035
  });
14440
15036
  }
14441
- function truncate3(s, max) {
15037
+ function truncate4(s, max) {
14442
15038
  if (max <= 1) {
14443
15039
  return "";
14444
15040
  }
@@ -14447,23 +15043,20 @@ function truncate3(s, max) {
14447
15043
  }
14448
15044
  return s.slice(0, Math.max(0, max - 1)) + "\u2026";
14449
15045
  }
14450
- function truncateLeft(s, max) {
14451
- if (max <= 1) {
14452
- return "";
14453
- }
14454
- if (s.length <= max) {
14455
- return s;
15046
+ function padRight2(s, w) {
15047
+ if (s.length >= w) {
15048
+ return s.slice(0, w);
14456
15049
  }
14457
- return "\u2026" + s.slice(s.length - (max - 1));
15050
+ return s + " ".repeat(w - s.length);
14458
15051
  }
14459
- var init_import_cwd_prompt = __esm({
14460
- "src/tui/import-cwd-prompt.ts"() {
15052
+ var PREFERRED_DEFAULT, MAX_VISIBLE_ROWS, PREFERRED_CONTENT_WIDTH;
15053
+ var init_agent_prompt = __esm({
15054
+ "src/tui/agent-prompt.ts"() {
14461
15055
  "use strict";
14462
- init_paths();
14463
- init_session();
14464
- init_cwd();
14465
- init_completion();
14466
15056
  init_prompt_utils();
15057
+ PREFERRED_DEFAULT = "opencode";
15058
+ MAX_VISIBLE_ROWS = 20;
15059
+ PREFERRED_CONTENT_WIDTH = 88;
14467
15060
  }
14468
15061
  });
14469
15062
 
@@ -14801,7 +15394,12 @@ async function runTuiApp(opts) {
14801
15394
  const term = termkit.terminal;
14802
15395
  const exitHint = {};
14803
15396
  const viewPrefs = {
14804
- showThoughts: config.tui.showThoughts
15397
+ showThoughts: config.tui.showThoughts,
15398
+ toolsExpanded: false,
15399
+ planExpanded: false,
15400
+ showFileUpdates: config.tui.showFileUpdates,
15401
+ mouseEnabled: config.tui.mouse,
15402
+ defaultEnterAction: config.tui.defaultEnterAction
14805
15403
  };
14806
15404
  const pickerPrefs = createPickerPrefs();
14807
15405
  let altScreenEngaged = false;
@@ -14979,7 +15577,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14979
15577
  "current_mode_update",
14980
15578
  "available_commands_update",
14981
15579
  "available_modes_update",
14982
- "usage_update"
15580
+ "usage_update",
15581
+ "config_option_update"
14983
15582
  ]);
14984
15583
  const handleSessionUpdate = (params) => {
14985
15584
  const { update } = params ?? {};
@@ -15324,6 +15923,10 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15324
15923
  historyPolicy: "full",
15325
15924
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
15326
15925
  ...opts.readonly === true ? { readonly: true } : {},
15926
+ ...opts.drip === true ? {
15927
+ replayMode: "drip",
15928
+ ...opts.dripSpeed !== void 0 ? { dripSpeed: opts.dripSpeed } : {}
15929
+ } : {},
15327
15930
  // Forward the user-chosen cwd via a full resume hint. An empty
15328
15931
  // upstreamSessionId routes through doResurrectFromImport
15329
15932
  // (first-launch imports); a real one takes the normal session/load
@@ -15415,7 +16018,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15415
16018
  dispatcher,
15416
16019
  repaintThrottleMs: config.tui.repaintThrottleMs,
15417
16020
  maxScrollbackLines: config.tui.maxScrollbackLines,
15418
- mouse: config.tui.mouse,
16021
+ mouse: viewPrefs.mouseEnabled,
15419
16022
  progressIndicator: config.tui.progressIndicator,
15420
16023
  readonly: opts.readonly === true,
15421
16024
  onKey: (events) => {
@@ -15426,6 +16029,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15426
16029
  if (tryHandleHelpKey(ev)) {
15427
16030
  continue;
15428
16031
  }
16032
+ if (tryHandleOptionsKey(ev)) {
16033
+ continue;
16034
+ }
15429
16035
  if (tryHandleScrollbackSearchKey(ev)) {
15430
16036
  continue;
15431
16037
  }
@@ -15669,7 +16275,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15669
16275
  const buildHelpEntries = () => {
15670
16276
  const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
15671
16277
  const amendDesc = "amend the in-flight turn (cancel + replace)";
15672
- const head = config.tui.defaultEnterAction === "amend" ? [
16278
+ const head = viewPrefs.defaultEnterAction === "amend" ? [
15673
16279
  ["Enter", amendDesc],
15674
16280
  ["Ctrl+Enter / Shift+Enter / ^S", enqueueDesc]
15675
16281
  ] : [
@@ -15700,6 +16306,190 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15700
16306
  screen.setHelpPrompt(null);
15701
16307
  return true;
15702
16308
  };
16309
+ const OPTION_IDS = [
16310
+ "tools",
16311
+ "plan",
16312
+ "thoughts",
16313
+ "diffs",
16314
+ "mouse",
16315
+ "enter"
16316
+ ];
16317
+ let optionsSelectedIndex = 0;
16318
+ const optionValue = (id) => {
16319
+ switch (id) {
16320
+ case "tools":
16321
+ return viewPrefs.toolsExpanded ? "expanded" : "collapsed";
16322
+ case "plan":
16323
+ return viewPrefs.planExpanded ? "expanded" : "collapsed";
16324
+ case "thoughts":
16325
+ return viewPrefs.showThoughts ? "shown" : "hidden";
16326
+ case "diffs":
16327
+ return viewPrefs.showFileUpdates;
16328
+ case "mouse":
16329
+ return viewPrefs.mouseEnabled ? "on" : "off";
16330
+ case "enter":
16331
+ return viewPrefs.defaultEnterAction;
16332
+ }
16333
+ };
16334
+ const optionLabel = (id) => {
16335
+ switch (id) {
16336
+ case "tools":
16337
+ return "Tools";
16338
+ case "plan":
16339
+ return "Plan";
16340
+ case "thoughts":
16341
+ return "Thoughts";
16342
+ case "diffs":
16343
+ return "File updates";
16344
+ case "mouse":
16345
+ return "Mouse capture";
16346
+ case "enter":
16347
+ return "Enter key";
16348
+ }
16349
+ };
16350
+ const buildOptionsSpec = () => ({
16351
+ title: "Session options",
16352
+ options: OPTION_IDS.map((id) => ({
16353
+ label: optionLabel(id),
16354
+ value: optionValue(id)
16355
+ })),
16356
+ selectedIndex: optionsSelectedIndex
16357
+ });
16358
+ const refreshOptionsPrompt = () => {
16359
+ if (!screen.isOptionsPromptActive()) {
16360
+ return;
16361
+ }
16362
+ screen.setOptionsPrompt(buildOptionsSpec());
16363
+ };
16364
+ const toggleOptionsModal = () => {
16365
+ if (screen.isOptionsPromptActive()) {
16366
+ screen.setOptionsPrompt(null);
16367
+ return;
16368
+ }
16369
+ optionsSelectedIndex = 0;
16370
+ screen.setOptionsPrompt(buildOptionsSpec());
16371
+ };
16372
+ const applyOptionToggle = (id) => {
16373
+ switch (id) {
16374
+ case "tools":
16375
+ viewPrefs.toolsExpanded = !viewPrefs.toolsExpanded;
16376
+ renderToolsBlock();
16377
+ break;
16378
+ case "plan":
16379
+ viewPrefs.planExpanded = !viewPrefs.planExpanded;
16380
+ rerenderPlan();
16381
+ break;
16382
+ case "thoughts":
16383
+ viewPrefs.showThoughts = !viewPrefs.showThoughts;
16384
+ screen.setHideThoughts(!viewPrefs.showThoughts);
16385
+ break;
16386
+ case "diffs":
16387
+ viewPrefs.showFileUpdates = viewPrefs.showFileUpdates === "diff" ? "edit" : "diff";
16388
+ reRenderAllEditDiffs();
16389
+ break;
16390
+ case "mouse": {
16391
+ const next = !screen.isMouseEnabled();
16392
+ screen.setMouseEnabled(next);
16393
+ viewPrefs.mouseEnabled = next;
16394
+ break;
16395
+ }
16396
+ case "enter":
16397
+ viewPrefs.defaultEnterAction = viewPrefs.defaultEnterAction === "amend" ? "enqueue" : "amend";
16398
+ break;
16399
+ }
16400
+ refreshOptionsPrompt();
16401
+ };
16402
+ const saveOption = (id) => {
16403
+ void (async () => {
16404
+ try {
16405
+ switch (id) {
16406
+ case "tools":
16407
+ case "plan":
16408
+ screen.notify(`${optionLabel(id)} is session-only \u2014 not saved`);
16409
+ return;
16410
+ case "thoughts":
16411
+ await setTuiConfigValue("showThoughts", viewPrefs.showThoughts);
16412
+ break;
16413
+ case "diffs":
16414
+ await setTuiConfigValue(
16415
+ "showFileUpdates",
16416
+ viewPrefs.showFileUpdates
16417
+ );
16418
+ break;
16419
+ case "mouse":
16420
+ await setTuiConfigValue("mouse", viewPrefs.mouseEnabled);
16421
+ break;
16422
+ case "enter":
16423
+ await setTuiConfigValue(
16424
+ "defaultEnterAction",
16425
+ viewPrefs.defaultEnterAction
16426
+ );
16427
+ break;
16428
+ }
16429
+ screen.notify(`saved default: ${optionLabel(id)} ${optionValue(id)}`);
16430
+ } catch (err) {
16431
+ screen.notify(
16432
+ `save failed: ${err instanceof Error ? err.message : String(err)}`
16433
+ );
16434
+ }
16435
+ })();
16436
+ };
16437
+ const tryHandleOptionsKey = (ev) => {
16438
+ if (!screen.isOptionsPromptActive()) {
16439
+ return false;
16440
+ }
16441
+ if (ev.type === "key") {
16442
+ switch (ev.name) {
16443
+ case "up":
16444
+ optionsSelectedIndex = Math.max(0, optionsSelectedIndex - 1);
16445
+ refreshOptionsPrompt();
16446
+ return true;
16447
+ case "down":
16448
+ optionsSelectedIndex = Math.min(
16449
+ OPTION_IDS.length - 1,
16450
+ optionsSelectedIndex + 1
16451
+ );
16452
+ refreshOptionsPrompt();
16453
+ return true;
16454
+ case "enter": {
16455
+ const id = OPTION_IDS[optionsSelectedIndex];
16456
+ if (id) {
16457
+ applyOptionToggle(id);
16458
+ }
16459
+ return true;
16460
+ }
16461
+ case "ctrl-o":
16462
+ case "escape":
16463
+ case "ctrl-c":
16464
+ screen.setOptionsPrompt(null);
16465
+ return true;
16466
+ case "ctrl-d":
16467
+ screen.setOptionsPrompt(null);
16468
+ return false;
16469
+ default:
16470
+ return true;
16471
+ }
16472
+ }
16473
+ if (ev.type === "char") {
16474
+ if (/^[1-9]$/.test(ev.ch)) {
16475
+ const idx = parseInt(ev.ch, 10) - 1;
16476
+ const id = OPTION_IDS[idx];
16477
+ if (id) {
16478
+ optionsSelectedIndex = idx;
16479
+ applyOptionToggle(id);
16480
+ }
16481
+ return true;
16482
+ }
16483
+ if (ev.ch === "s" || ev.ch === "S") {
16484
+ const id = OPTION_IDS[optionsSelectedIndex];
16485
+ if (id) {
16486
+ saveOption(id);
16487
+ }
16488
+ return true;
16489
+ }
16490
+ }
16491
+ return true;
16492
+ };
15703
16493
  const teardown = () => {
15704
16494
  teardownStarted = true;
15705
16495
  process.off("SIGINT", sigintHandler);
@@ -15898,14 +16688,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15898
16688
  const handleEffect = (effect) => {
15899
16689
  switch (effect.type) {
15900
16690
  case "send":
15901
- if (config.tui.defaultEnterAction === "amend") {
16691
+ if (viewPrefs.defaultEnterAction === "amend") {
15902
16692
  amendPrompt(effect.text, effect.attachments, effect.displayText);
15903
16693
  } else {
15904
16694
  enqueuePrompt(effect.text, effect.attachments, effect.displayText);
15905
16695
  }
15906
16696
  return;
15907
16697
  case "amend":
15908
- if (config.tui.defaultEnterAction === "amend") {
16698
+ if (viewPrefs.defaultEnterAction === "amend") {
15909
16699
  enqueuePrompt(effect.text, effect.attachments, effect.displayText);
15910
16700
  } else {
15911
16701
  amendPrompt(effect.text, effect.attachments, effect.displayText);
@@ -15998,9 +16788,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15998
16788
  case "next-live-session":
15999
16789
  void cycleLiveSession();
16000
16790
  return;
16001
- case "toggle-tools":
16002
- toolsExpanded = !toolsExpanded;
16003
- renderToolsBlock();
16791
+ case "toggle-options":
16792
+ toggleOptionsModal();
16004
16793
  return;
16005
16794
  case "toggle-thoughts":
16006
16795
  viewPrefs.showThoughts = !viewPrefs.showThoughts;
@@ -16012,6 +16801,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16012
16801
  case "toggle-mouse": {
16013
16802
  const next = !screen.isMouseEnabled();
16014
16803
  screen.setMouseEnabled(next);
16804
+ viewPrefs.mouseEnabled = next;
16015
16805
  screen.notify(
16016
16806
  next ? "mouse capture on \u2014 wheel scrolls; shift+drag to select text" : "mouse capture off \u2014 click-drag selects text; PgUp/PgDn scrolls"
16017
16807
  );
@@ -16282,9 +17072,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16282
17072
  toolsBlockStartedAt = null;
16283
17073
  toolsBlockEndedAt = null;
16284
17074
  toolsBlockStopReason = null;
16285
- toolsExpanded = false;
16286
17075
  lastEditMarkPath = null;
16287
17076
  turnHasShownProse = false;
17077
+ renderedEditDiffs.clear();
16288
17078
  screen.clearScrollback();
16289
17079
  return true;
16290
17080
  case "/demo-plan": {
@@ -16465,16 +17255,27 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16465
17255
  }
16466
17256
  };
16467
17257
  const toolStates = /* @__PURE__ */ new Map();
17258
+ const renderedEditDiffs = /* @__PURE__ */ new Map();
16468
17259
  const exitPlanStates = /* @__PURE__ */ new Map();
16469
17260
  const toolCallOrder = [];
16470
- let toolsExpanded = false;
16471
17261
  let toolsBlockStartedAt = null;
16472
17262
  let toolsBlockEndedAt = null;
16473
17263
  let toolsBlockStopReason = null;
16474
17264
  let lastPlanEvent = null;
16475
17265
  const TOOLS_COLLAPSED_LIMIT = config.tui.maxToolItems;
16476
17266
  const PLAN_VISIBLE_LIMIT2 = config.tui.maxPlanItems;
16477
- const formatOptions = { maxPlanItems: PLAN_VISIBLE_LIMIT2 };
17267
+ const planFormatOptions = () => ({
17268
+ maxPlanItems: viewPrefs.planExpanded ? Infinity : PLAN_VISIBLE_LIMIT2
17269
+ });
17270
+ const rerenderPlan = () => {
17271
+ if (lastPlanEvent === null) {
17272
+ return;
17273
+ }
17274
+ const lines = formatEvent(lastPlanEvent, planFormatOptions());
17275
+ if (lines.length > 0) {
17276
+ screen.upsertLines("plan", [{ body: "" }, ...lines]);
17277
+ }
17278
+ };
16478
17279
  let agentBuffer = "";
16479
17280
  let agentKey = null;
16480
17281
  let agentSeq = 0;
@@ -16542,7 +17343,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16542
17343
  }
16543
17344
  const total = toolCallOrder.length;
16544
17345
  const capped = TOOLS_COLLAPSED_LIMIT > 0;
16545
- const visibleIds = !capped || toolsExpanded ? toolCallOrder : toolCallOrder.slice(Math.max(0, total - TOOLS_COLLAPSED_LIMIT));
17346
+ const visibleIds = !capped || viewPrefs.toolsExpanded ? toolCallOrder : toolCallOrder.slice(Math.max(0, total - TOOLS_COLLAPSED_LIMIT));
16546
17347
  const hidden = total - visibleIds.length;
16547
17348
  const inProgress = toolsBlockEndedAt === null;
16548
17349
  const end = toolsBlockEndedAt ?? Date.now();
@@ -16561,12 +17362,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16561
17362
  const noun = total === 1 ? "tool" : "tools";
16562
17363
  const timing = stoppedReason !== null ? stoppedLabel : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
16563
17364
  const parts = [`${total} ${noun}`, timing];
16564
- if (inProgress && capped) {
16565
- if (hidden > 0) {
16566
- parts.push(`${hidden} hidden \u2014 ^O expand`);
16567
- } else if (toolsExpanded && total > TOOLS_COLLAPSED_LIMIT) {
16568
- parts.push("^O collapse");
16569
- }
17365
+ if (inProgress && capped && hidden > 0) {
17366
+ parts.push(`${hidden} hidden`);
16570
17367
  }
16571
17368
  summary = parts.join(" \xB7 ");
16572
17369
  }
@@ -16585,7 +17382,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16585
17382
  for (const id of visibleIds) {
16586
17383
  const state = toolStates.get(id);
16587
17384
  if (state) {
16588
- lines.push(...formatToolLine2(state));
17385
+ lines.push(...formatToolLine2(state, end));
16589
17386
  }
16590
17387
  }
16591
17388
  screen.upsertLines("tools", lines);
@@ -16602,7 +17399,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16602
17399
  const state = existing ?? {
16603
17400
  initialTitle: title ?? "tool",
16604
17401
  latestTitle: title ?? "tool",
16605
- status: status ?? "pending"
17402
+ status: status ?? "pending",
17403
+ startedAt: Date.now()
16606
17404
  };
16607
17405
  if (existing && title !== void 0) {
16608
17406
  state.latestTitle = title;
@@ -16613,6 +17411,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16613
17411
  if (!existing) {
16614
17412
  state.status = status ?? "pending";
16615
17413
  }
17414
+ if (state.endedAt === void 0 && isTerminalToolStatus(state.status)) {
17415
+ state.endedAt = Date.now();
17416
+ }
16616
17417
  if (errorText !== void 0) {
16617
17418
  state.errorText = errorText;
16618
17419
  }
@@ -16632,35 +17433,60 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16632
17433
  let lastEditMarkPath = null;
16633
17434
  let turnHasShownProse = false;
16634
17435
  const maybeRenderEditDiff = (toolCallId) => {
16635
- const mode = config.tui.showFileUpdates;
16636
- if (mode === "none") {
16637
- return;
16638
- }
16639
- const state = toolStates.get(toolCallId);
16640
- if (!state?.editDiff || state.status !== "completed") {
16641
- return;
16642
- }
16643
- if (mode === "diff") {
16644
- const lines2 = formatEditDiffBlock(state.editDiff, "diff");
16645
- if (lines2.length > 0) {
16646
- screen.upsertLines(`editdiff:${toolCallId}`, lines2);
17436
+ const key = `editdiff:${toolCallId}`;
17437
+ const lines = (() => {
17438
+ const mode = viewPrefs.showFileUpdates;
17439
+ if (mode === "none") {
17440
+ return null;
16647
17441
  }
17442
+ const state = toolStates.get(toolCallId);
17443
+ if (!state?.editDiff || state.status !== "completed") {
17444
+ return null;
17445
+ }
17446
+ if (mode === "diff") {
17447
+ const out2 = formatEditDiffBlock(state.editDiff, "diff");
17448
+ return out2.length > 0 ? out2 : null;
17449
+ }
17450
+ if (!turnHasShownProse) {
17451
+ return null;
17452
+ }
17453
+ const diff2 = state.editDiff;
17454
+ if (diff2.path && diff2.path === lastEditMarkPath && !screen.hasKey(key)) {
17455
+ return null;
17456
+ }
17457
+ const out = formatEditDiffBlock(diff2, "edit");
17458
+ if (out.length === 0) {
17459
+ return null;
17460
+ }
17461
+ if (diff2.path) {
17462
+ lastEditMarkPath = diff2.path;
17463
+ }
17464
+ return out;
17465
+ })();
17466
+ if (lines === null) {
17467
+ screen.removeKey(key);
16648
17468
  return;
16649
17469
  }
16650
- if (!turnHasShownProse) {
16651
- return;
16652
- }
16653
- const diff = state.editDiff;
16654
- if (diff.path && diff.path === lastEditMarkPath) {
16655
- return;
16656
- }
16657
- const lines = formatEditDiffBlock(diff, "edit");
16658
- if (lines.length === 0) {
16659
- return;
17470
+ const diff = toolStates.get(toolCallId)?.editDiff;
17471
+ if (diff) {
17472
+ renderedEditDiffs.set(toolCallId, diff);
16660
17473
  }
16661
- screen.upsertLines(`editdiff:${toolCallId}`, lines);
16662
- if (diff.path) {
16663
- lastEditMarkPath = diff.path;
17474
+ screen.upsertLines(key, lines);
17475
+ };
17476
+ const reRenderAllEditDiffs = () => {
17477
+ const mode = viewPrefs.showFileUpdates;
17478
+ for (const [toolCallId, diff] of renderedEditDiffs) {
17479
+ const key = `editdiff:${toolCallId}`;
17480
+ if (mode === "none") {
17481
+ screen.removeKey(key);
17482
+ continue;
17483
+ }
17484
+ const out = formatEditDiffBlock(diff, mode);
17485
+ if (out.length === 0) {
17486
+ screen.removeKey(key);
17487
+ } else {
17488
+ screen.upsertLines(key, out);
17489
+ }
16664
17490
  }
16665
17491
  };
16666
17492
  applyRenderEvent = (event) => {
@@ -16732,7 +17558,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16732
17558
  toolStates.clear();
16733
17559
  exitPlanStates.clear();
16734
17560
  toolCallOrder.length = 0;
16735
- toolsExpanded = false;
16736
17561
  toolsBlockEndedAt = null;
16737
17562
  lastEditMarkPath = null;
16738
17563
  turnHasShownProse = false;
@@ -16794,7 +17619,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16794
17619
  closeAgentText();
16795
17620
  closeThought();
16796
17621
  lastPlanEvent = event;
16797
- const lines = formatEvent(event, formatOptions);
17622
+ const lines = formatEvent(event, planFormatOptions());
16798
17623
  if (lines.length > 0) {
16799
17624
  screen.upsertLines("plan", [{ body: "" }, ...lines]);
16800
17625
  }
@@ -16843,7 +17668,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16843
17668
  stopped: true,
16844
17669
  amended: event.amended === true
16845
17670
  },
16846
- formatOptions
17671
+ planFormatOptions()
16847
17672
  );
16848
17673
  if (lines.length > 0) {
16849
17674
  screen.upsertLines("plan", [{ body: "" }, ...lines]);
@@ -16873,7 +17698,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16873
17698
  toolsBlockStartedAt = null;
16874
17699
  toolsBlockEndedAt = null;
16875
17700
  toolsBlockStopReason = null;
16876
- toolsExpanded = false;
16877
17701
  upstreamInterruptedSeen = false;
16878
17702
  lastEditMarkPath = null;
16879
17703
  turnHasShownProse = false;
@@ -16961,7 +17785,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16961
17785
  toolsBlockStartedAt = null;
16962
17786
  toolsBlockEndedAt = null;
16963
17787
  toolsBlockStopReason = null;
16964
- toolsExpanded = false;
16965
17788
  lastEditMarkPath = null;
16966
17789
  turnHasShownProse = false;
16967
17790
  };
@@ -17119,6 +17942,10 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17119
17942
  return ctx;
17120
17943
  }
17121
17944
  if (opts.forceNew) {
17945
+ const agentStep = await ensureAgentForNew(term, target, opts);
17946
+ if (agentStep !== "ok") {
17947
+ return null;
17948
+ }
17122
17949
  return newCtx(opts, cwd, config);
17123
17950
  }
17124
17951
  if (opts.resume) {
@@ -17142,7 +17969,8 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17142
17969
  sessions,
17143
17970
  config,
17144
17971
  target,
17145
- prefs: pickerPrefs
17972
+ prefs: pickerPrefs,
17973
+ ...opts.initialPrompt !== void 0 ? { initialPrompt: opts.initialPrompt } : {}
17146
17974
  });
17147
17975
  if (choice.kind === "abort") {
17148
17976
  return null;
@@ -17151,6 +17979,13 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17151
17979
  if (choice.prompt !== void 0) {
17152
17980
  opts.initialPrompt = choice.prompt;
17153
17981
  }
17982
+ const agentStep = await ensureAgentForNew(term, target, opts);
17983
+ if (agentStep === "cancel") {
17984
+ return null;
17985
+ }
17986
+ if (agentStep === "back") {
17987
+ continue;
17988
+ }
17154
17989
  return newCtx(opts, cwd, config);
17155
17990
  }
17156
17991
  if (choice.kind === "fork") {
@@ -17310,6 +18145,38 @@ function newCtx(opts, cwd, config) {
17310
18145
  cwd
17311
18146
  };
17312
18147
  }
18148
+ async function ensureAgentForNew(term, target, opts) {
18149
+ if (opts.agentId) {
18150
+ return "ok";
18151
+ }
18152
+ if (await hasConfiguredDefaultAgent()) {
18153
+ return "ok";
18154
+ }
18155
+ let agents;
18156
+ try {
18157
+ agents = await listAgents(target);
18158
+ } catch {
18159
+ return "ok";
18160
+ }
18161
+ if (agents.length === 0) {
18162
+ return "ok";
18163
+ }
18164
+ const result = await promptForAgent(term, agents);
18165
+ if (result.kind === "cancel") {
18166
+ return "cancel";
18167
+ }
18168
+ if (result.kind === "back") {
18169
+ return "back";
18170
+ }
18171
+ opts.agentId = result.agentId;
18172
+ if (result.persist) {
18173
+ try {
18174
+ await setDefaultAgent(result.agentId);
18175
+ } catch {
18176
+ }
18177
+ }
18178
+ return "ok";
18179
+ }
17313
18180
  function debugLogUpdate(update, event) {
17314
18181
  writeDebugLine({
17315
18182
  src: "session/update",
@@ -17455,6 +18322,7 @@ var init_app = __esm({
17455
18322
  init_picker();
17456
18323
  init_import_cwd_prompt();
17457
18324
  init_import_action_prompt();
18325
+ init_agent_prompt();
17458
18326
  init_screen();
17459
18327
  init_input();
17460
18328
  init_attachments();
@@ -17480,7 +18348,7 @@ var init_app = __esm({
17480
18348
  ["Alt+N / Alt+Tab", "next live session"],
17481
18349
  ["^T", "show / hide thoughts"],
17482
18350
  ["^V", "paste image from clipboard"],
17483
- ["^O", "expand / collapse tools block"],
18351
+ ["^O", "session options (tools \xB7 plan \xB7 thoughts \xB7 diffs \xB7 mouse \xB7 enter)"],
17484
18352
  null,
17485
18353
  ["^R", "history reverse search (^S walks forward once engaged)"],
17486
18354
  ["PgUp / PgDn", "scroll scrollback"],
@@ -17521,6 +18389,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17521
18389
  "detach",
17522
18390
  "diff",
17523
18391
  "disabled",
18392
+ "drip",
17524
18393
  "fold",
17525
18394
  "follow",
17526
18395
  "force",
@@ -17543,8 +18412,10 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17543
18412
  var KNOWN_VALUE_FLAGS = /* @__PURE__ */ new Set([
17544
18413
  "agent",
17545
18414
  "args",
18415
+ "columns",
17546
18416
  "command",
17547
18417
  "cwd",
18418
+ "drip-speed",
17548
18419
  "env",
17549
18420
  "host",
17550
18421
  "model",
@@ -20006,7 +20877,14 @@ var SessionManager = class {
20006
20877
  histories: this.histories,
20007
20878
  synopsisAgent: this.synopsisAgent,
20008
20879
  synopsisModel: this.synopsisModel,
20009
- persistTitle: (id, title) => this.persistTitle(id, title),
20880
+ persistTitle: async (id, title) => {
20881
+ const live = this.get(id);
20882
+ if (live) {
20883
+ await live.retitle(title);
20884
+ return;
20885
+ }
20886
+ await this.persistTitle(id, title);
20887
+ },
20010
20888
  persistSynopsis: (id, synopsis, through) => this.persistSynopsis(id, synopsis, through),
20011
20889
  logger: this.logger,
20012
20890
  npmRegistry: this.npmRegistry
@@ -20359,27 +21237,27 @@ var SessionManager = class {
20359
21237
  return false;
20360
21238
  }
20361
21239
  }
20362
- // When the last client detaches from a session that resolves to
20363
- // non-interactive e.g. a `hydra cat` run, born interactive:undefined
20364
- // with originatingClient hydra-acp-cat, whose every prompt is ancillary
20365
- // close it so its agent process doesn't linger until the (default 1h)
20366
- // idle timeout fires. The cold record is kept, so the rare refine-in-TUI
20367
- // still works via the resurrect/reseed path. Sessions promoted to
20368
- // interactive (driven by a real, non-ancillary prompt) resolve to true
20369
- // and are left running.
21240
+ // When the last client detaches from a session that was never promoted
21241
+ // to interactive, close it so its agent process doesn't linger until the
21242
+ // (default 1h) idle timeout fires. This covers both `hydra cat` runs
21243
+ // (born interactive:undefined with originatingClient hydra-acp-cat, every
21244
+ // prompt ancillary) and any other client that opened a session but never
21245
+ // sent a real, non-ancillary prompt. Promotion to interactive is
21246
+ // synchronous on the first real prompt (Session.prompt sets _interactive
21247
+ // = true before enqueuing), so a session that ever saw a genuine turn
21248
+ // resolves to true here and is left running. The cold record is kept, so
21249
+ // re-attaching resurrects via the reseed path.
21250
+ //
21251
+ // Note: this only fires from the explicit session/detach handler — raw WS
21252
+ // close deliberately does NOT reap (see acp-ws.ts), so an abrupt
21253
+ // disconnect of a never-prompted session falls through to the idle
21254
+ // timeout rather than being torn down.
20370
21255
  async reapIfOrphanedNonInteractive(sessionId) {
20371
21256
  const session = this.sessions.get(sessionId);
20372
21257
  if (!session || session.attachedCount > 0) {
20373
21258
  return;
20374
21259
  }
20375
- const interactive = effectiveInteractive(
20376
- {
20377
- interactive: session.interactive,
20378
- ...session.originatingClient ? { originatingClient: session.originatingClient } : {}
20379
- },
20380
- true
20381
- );
20382
- if (interactive !== false) {
21260
+ if (session.interactive === true) {
20383
21261
  return;
20384
21262
  }
20385
21263
  this.logger?.info(
@@ -20877,7 +21755,8 @@ var SessionManager = class {
20877
21755
  updatedAt: used,
20878
21756
  attachedClients: session.attachedCount,
20879
21757
  status: "live",
20880
- busy: session.turnStartedAt !== void 0
21758
+ busy: session.turnStartedAt !== void 0,
21759
+ awaitingInput: session.awaitingInput
20881
21760
  });
20882
21761
  }
20883
21762
  const records = await this.store.list().catch(() => []);
@@ -20915,7 +21794,8 @@ var SessionManager = class {
20915
21794
  updatedAt: used,
20916
21795
  attachedClients: 0,
20917
21796
  status: "cold",
20918
- busy: false
21797
+ busy: false,
21798
+ awaitingInput: false
20919
21799
  });
20920
21800
  }
20921
21801
  entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
@@ -24819,10 +25699,11 @@ function registerAcpWsEndpoint(app, deps) {
24819
25699
  params.clientInfo,
24820
25700
  params.clientId
24821
25701
  );
25702
+ const drip = params.replayMode === "drip";
24822
25703
  const { entries: replay, appliedPolicy } = await session.attach(
24823
25704
  client,
24824
25705
  params.historyPolicy,
24825
- { afterMessageId: params.afterMessageId }
25706
+ { afterMessageId: params.afterMessageId, raw: drip }
24826
25707
  );
24827
25708
  state.attached.set(session.sessionId, {
24828
25709
  sessionId: session.sessionId,
@@ -24830,10 +25711,35 @@ function registerAcpWsEndpoint(app, deps) {
24830
25711
  readonly
24831
25712
  });
24832
25713
  app.log.info(
24833
- `session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}`
25714
+ `session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}${drip ? " replayMode=drip" : ""}`
24834
25715
  );
24835
- for (const note of replay) {
24836
- await connection.notify(note.method, note.params);
25716
+ if (drip) {
25717
+ const speed = params.dripSpeed && params.dripSpeed > 0 ? params.dripSpeed : 1;
25718
+ const MAX_GAP_MS = 750;
25719
+ void (async () => {
25720
+ let prev = null;
25721
+ for (const note of replay) {
25722
+ const at = typeof note.recordedAt === "number" ? note.recordedAt : null;
25723
+ if (prev !== null && at !== null) {
25724
+ const gap = Math.min(MAX_GAP_MS, Math.max(0, (at - prev) / speed));
25725
+ if (gap > 0) {
25726
+ await new Promise((r) => setTimeout(r, gap));
25727
+ }
25728
+ }
25729
+ if (at !== null) {
25730
+ prev = at;
25731
+ }
25732
+ try {
25733
+ await connection.notify(note.method, note.params);
25734
+ } catch {
25735
+ return;
25736
+ }
25737
+ }
25738
+ })();
25739
+ } else {
25740
+ for (const note of replay) {
25741
+ await connection.notify(note.method, note.params);
25742
+ }
24837
25743
  }
24838
25744
  session.replayPendingPermissions(client);
24839
25745
  const modesPayload = buildModesPayload(session);
@@ -26694,12 +27600,15 @@ async function runSessionsList(opts = {}) {
26694
27600
  }
26695
27601
  const now = Date.now();
26696
27602
  const rows = visible.map((s) => toRow(s, now));
26697
- const widths = computeWidths(rows);
27603
+ const formatOpts = {
27604
+ columns: opts.columns ?? config.tui.sessionColumns ?? DEFAULT_COLUMNS,
27605
+ cwdMaxWidth: config.tui.cwdColumnMaxWidth
27606
+ };
27607
+ const widths = computeWidths(rows, formatOpts);
26698
27608
  const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
26699
- const cwdMax = config.tui.cwdColumnMaxWidth;
26700
- process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
27609
+ process.stdout.write(formatRow(HEADER, widths, maxWidth, formatOpts) + "\n");
26701
27610
  for (const r of rows) {
26702
- process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
27611
+ process.stdout.write(formatRow(r, widths, maxWidth, formatOpts) + "\n");
26703
27612
  }
26704
27613
  if (truncated > 0) {
26705
27614
  process.stdout.write(
@@ -26963,10 +27872,14 @@ function printBundleInfo(raw, cwdColumnMaxWidth) {
26963
27872
  }
26964
27873
  const summary = bundleToSummary(parsed);
26965
27874
  const row = toRow(summary);
26966
- const widths = computeWidths([row]);
27875
+ const formatOpts = {
27876
+ columns: ALL_COLUMNS,
27877
+ cwdMaxWidth: cwdColumnMaxWidth
27878
+ };
27879
+ const widths = computeWidths([row], formatOpts);
26967
27880
  const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
26968
- process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
26969
- process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
27881
+ process.stdout.write(formatRow(HEADER, widths, maxWidth, formatOpts) + "\n");
27882
+ process.stdout.write(formatRow(row, widths, maxWidth, formatOpts) + "\n");
26970
27883
  const originUpstream = parsed.session.upstreamSessionId ?? "-";
26971
27884
  process.stdout.write(
26972
27885
  `
@@ -27542,6 +28455,7 @@ function aggregate(bundle, status) {
27542
28455
  const durationMs = Number.isFinite(createdMs) && Number.isFinite(updatedMs) ? updatedMs - createdMs : null;
27543
28456
  return {
27544
28457
  sessionId: r.sessionId,
28458
+ ...r.upstreamSessionId !== void 0 ? { upstreamSessionId: r.upstreamSessionId } : {},
27545
28459
  ...r.title !== void 0 ? { title: r.title } : {},
27546
28460
  cwd: r.cwd,
27547
28461
  agentId: r.agentId,
@@ -27569,6 +28483,9 @@ function formatSummary(d, verbose) {
27569
28483
  const lines = [];
27570
28484
  const pad = (label) => label.padEnd(14);
27571
28485
  lines.push(`${pad("Session:")}${d.sessionId}`);
28486
+ if (d.upstreamSessionId) {
28487
+ lines.push(`${pad("Upstream:")}${d.upstreamSessionId}`);
28488
+ }
27572
28489
  if (d.title) {
27573
28490
  lines.push(`${pad("Title:")}${d.title}`);
27574
28491
  }
@@ -27693,6 +28610,9 @@ function formatDuration(ms) {
27693
28610
  return parts.join(" ");
27694
28611
  }
27695
28612
 
28613
+ // src/cli.ts
28614
+ init_session_row();
28615
+
27696
28616
  // src/cli/commands/extensions.ts
27697
28617
  init_config();
27698
28618
  init_service_token();
@@ -28709,6 +29629,32 @@ function formatAge(ms) {
28709
29629
  const day = Math.floor(hour / 24);
28710
29630
  return `${day} day${day === 1 ? "" : "s"}`;
28711
29631
  }
29632
+ async function assertKnownAgent(agentId) {
29633
+ const config = await loadConfig();
29634
+ const serviceToken = await loadServiceToken();
29635
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
29636
+ let known;
29637
+ try {
29638
+ const r = await fetch(`${baseUrl}/v1/agents`, {
29639
+ headers: { Authorization: `Bearer ${serviceToken}` }
29640
+ });
29641
+ if (!r.ok) {
29642
+ return;
29643
+ }
29644
+ const body = await r.json();
29645
+ known = body.agents.map((a) => a.id);
29646
+ } catch {
29647
+ return;
29648
+ }
29649
+ if (known.includes(agentId)) {
29650
+ return;
29651
+ }
29652
+ process.stderr.write(
29653
+ `hydra-acp: unknown agent '${agentId}'. Run 'hydra-acp agent list' to see available agents.
29654
+ `
29655
+ );
29656
+ process.exit(2);
29657
+ }
28712
29658
  async function runAgentsInstall(agentId) {
28713
29659
  if (!agentId) {
28714
29660
  process.stderr.write("Usage: hydra-acp agent install <agent-id>\n");
@@ -28880,16 +29826,7 @@ async function runAgentsSet(agentId, modelId) {
28880
29826
  process.exit(1);
28881
29827
  return;
28882
29828
  }
28883
- const raw = await readRawConfig3();
28884
- if (modelId === void 0) {
28885
- raw.defaultAgent = agentId;
28886
- await writeRawConfig3(raw);
28887
- } else {
28888
- const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
28889
- models[agentId] = modelId;
28890
- raw.defaultModels = models;
28891
- await writeRawConfig3(raw);
28892
- }
29829
+ await setDefaultAgent(agentId, modelId);
28893
29830
  const disk = readAgentDefaults(await readRawConfig3());
28894
29831
  if (modelId !== void 0 && agentId !== disk.agent) {
28895
29832
  process.stdout.write(
@@ -28941,13 +29878,6 @@ async function readRawConfig3() {
28941
29878
  const raw = await fsp12.readFile(paths.config(), "utf8");
28942
29879
  return JSON.parse(raw);
28943
29880
  }
28944
- async function writeRawConfig3(raw) {
28945
- await fsp12.writeFile(
28946
- paths.config(),
28947
- JSON.stringify(raw, null, 2) + "\n",
28948
- { encoding: "utf8", mode: 384 }
28949
- );
28950
- }
28951
29881
  async function runAgentsRefresh() {
28952
29882
  const config = await loadConfig();
28953
29883
  const serviceToken = await loadServiceToken();
@@ -30463,6 +31393,9 @@ async function main() {
30463
31393
  if (streamBufferBytes !== void 0) {
30464
31394
  catOpts.streamBufferBytes = streamBufferBytes;
30465
31395
  }
31396
+ if (agentIdFromFlag !== void 0) {
31397
+ await assertKnownAgent(agentIdFromFlag);
31398
+ }
30466
31399
  suppressUpdateNotice = true;
30467
31400
  await runCat(catOpts);
30468
31401
  return;
@@ -30503,11 +31436,24 @@ async function main() {
30503
31436
  case "sessions": {
30504
31437
  const sub = positional[1];
30505
31438
  if (sub === void 0 || sub === "list") {
31439
+ const columnsRaw = resolveOption(flags, "columns");
31440
+ let columns;
31441
+ if (columnsRaw !== void 0) {
31442
+ try {
31443
+ columns = parseColumns(columnsRaw);
31444
+ } catch (err) {
31445
+ process.stderr.write(`${err.message}
31446
+ `);
31447
+ process.exit(2);
31448
+ return;
31449
+ }
31450
+ }
30506
31451
  await runSessionsList({
30507
31452
  all: flags.all === true,
30508
31453
  json: flags.json === true,
30509
31454
  host: typeof flags.host === "string" ? flags.host : void 0,
30510
- includeNonInteractive: flags["include-non-interactive"] === true
31455
+ includeNonInteractive: flags["include-non-interactive"] === true,
31456
+ columns
30511
31457
  });
30512
31458
  return;
30513
31459
  }
@@ -30742,6 +31688,9 @@ async function dispatchTui(flags, base) {
30742
31688
  );
30743
31689
  process.exit(2);
30744
31690
  }
31691
+ if (base.agentId !== void 0) {
31692
+ await assertKnownAgent(base.agentId);
31693
+ }
30745
31694
  setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
30746
31695
  const { runTui } = await Promise.resolve().then(() => (init_tui(), tui_exports));
30747
31696
  const tuiOpts = { resume, forceNew, readonly };
@@ -30766,6 +31715,19 @@ async function dispatchTui(flags, base) {
30766
31715
  if (base.dangerouslySkipPermissions === true) {
30767
31716
  tuiOpts.dangerouslySkipPermissions = true;
30768
31717
  }
31718
+ if (flags.drip === true) {
31719
+ if (base.sessionId === void 0) {
31720
+ process.stderr.write(
31721
+ "hydra-acp: --drip requires a session id. Pass --session <id-or-url> --drip.\n"
31722
+ );
31723
+ process.exit(2);
31724
+ }
31725
+ tuiOpts.drip = true;
31726
+ const dripSpeed = parseNumericFlag(flags, "drip-speed");
31727
+ if (dripSpeed !== void 0 && dripSpeed > 0) {
31728
+ tuiOpts.dripSpeed = dripSpeed;
31729
+ }
31730
+ }
30769
31731
  await runTui(tuiOpts);
30770
31732
  }
30771
31733
  function parseNumericFlag(flags, name) {
@@ -30880,10 +31842,11 @@ function printHelp() {
30880
31842
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
30881
31843
  " hydra-acp daemon stop|restart",
30882
31844
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
30883
- " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-non-interactive]",
31845
+ " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-non-interactive] [--columns=<list>]",
30884
31846
  " List sessions (live + 20 most-recent cold; --all lifts the cold cap AND surfaces non-interactive sessions; --json emits JSON for scripts).",
30885
31847
  " --host filters by origin machine: 'local' (default) shows only sessions created here, 'all' shows everything, or pass a hostname (e.g. machine-b) to show only imports from that peer.",
30886
31848
  " --include-non-interactive surfaces ancillary (e.g. `hydra cat`) or never-prompted sessions while keeping the cold cap (a narrower --all).",
31849
+ " --columns picks which columns show and their order, e.g. --columns=session,state,title,cost (valid: session,upstream,host,state,agent,model,age,cwd,title,cost). UPSTREAM/HOST/MODEL hidden by default; COST is the trailing column; overrides config.tui.sessionColumns.",
30887
31850
  " hydra-acp session info <id> [--verbose] [--json] [--diff] [--fold] [--no-color] [--no-pager]",
30888
31851
  " Aggregate one session: turn count, tool histogram, files touched, cost/duration, synopsis. --diff appends the session diff under the summary and pages the whole thing on a TTY (inherits --fold).",
30889
31852
  " hydra-acp session diff <id> [--json] [--no-color] [--no-pager] [--fold]",