@hydra-acp/cli 0.1.59 → 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 [];
@@ -9518,20 +9798,6 @@ function formatUsage(usage) {
9518
9798
  }
9519
9799
  return parts.length === 0 ? null : parts.join(" \xB7 ");
9520
9800
  }
9521
- function formatElapsed(ms) {
9522
- const totalSec = Math.floor(ms / 1e3);
9523
- if (totalSec < 60) {
9524
- return `${totalSec}s`;
9525
- }
9526
- const min = Math.floor(totalSec / 60);
9527
- const sec = totalSec % 60;
9528
- if (min < 60) {
9529
- return sec === 0 ? `${min}m` : `${min}m ${sec}s`;
9530
- }
9531
- const hr = Math.floor(min / 60);
9532
- const remMin = min % 60;
9533
- return remMin === 0 ? `${hr}h` : `${hr}h ${remMin}m`;
9534
- }
9535
9801
  function formatTokens(n) {
9536
9802
  if (n >= 1e6) {
9537
9803
  return `${(n / 1e6).toFixed(1)}M`;
@@ -9750,7 +10016,7 @@ function mapCsiUToKeyName(code, mod) {
9750
10016
  }
9751
10017
  return null;
9752
10018
  }
9753
- 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;
9754
10020
  var init_screen = __esm({
9755
10021
  "src/tui/screen.ts"() {
9756
10022
  "use strict";
@@ -9758,6 +10024,7 @@ var init_screen = __esm({
9758
10024
  init_paths();
9759
10025
  init_session();
9760
10026
  init_attachments();
10027
+ init_format();
9761
10028
  init_sync();
9762
10029
  SESSIONBAR_ROWS = 1;
9763
10030
  BANNER_ROWS = 1;
@@ -9765,6 +10032,7 @@ var init_screen = __esm({
9765
10032
  MAX_PROMPT_ROWS = 8;
9766
10033
  MAX_QUEUED_ROWS = 5;
9767
10034
  MAX_PERMISSION_ROWS = 12;
10035
+ MAX_OPTIONS_ROWS = 12;
9768
10036
  MAX_HELP_ROWS = 30;
9769
10037
  MAX_COMPLETION_ROWS = 6;
9770
10038
  MAX_CHIP_ROWS = 4;
@@ -9839,6 +10107,7 @@ var init_screen = __esm({
9839
10107
  lastFrameW = 0;
9840
10108
  lastFrameH = 0;
9841
10109
  permissionPrompt = null;
10110
+ optionsPrompt = null;
9842
10111
  confirmPrompt = null;
9843
10112
  helpPrompt = null;
9844
10113
  completions = [];
@@ -10008,6 +10277,7 @@ var init_screen = __esm({
10008
10277
  this.writeProgressIndicator(0);
10009
10278
  this.started = false;
10010
10279
  if (!opts.keepFullscreen) {
10280
+ emergencyTerminalReset();
10011
10281
  this.term.fullscreen(false);
10012
10282
  this.term("\n");
10013
10283
  }
@@ -10181,6 +10451,11 @@ uncaught: ${err.stack ?? err.message}
10181
10451
  }
10182
10452
  if (text.includes("\n")) {
10183
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
+ }
10184
10459
  for (let i = 0; i < parts.length; i++) {
10185
10460
  if (parts[i].length > 0) {
10186
10461
  this.handleRawStdin(Buffer.from(parts[i], "binary"));
@@ -10708,6 +10983,41 @@ uncaught: ${err.stack ?? err.message}
10708
10983
  clearKey(key) {
10709
10984
  this.keyedBlocks.delete(key);
10710
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
+ }
10711
11021
  // Mark `key` as the sticky-bottom block. While set, whenever new content
10712
11022
  // lands after the block's lines (appendLines / appendStreaming / a new
10713
11023
  // upserted block) the screen floats this block back to the end so it
@@ -10826,6 +11136,15 @@ uncaught: ${err.stack ?? err.message}
10826
11136
  this.permissionPrompt = spec ? { ...spec } : null;
10827
11137
  this.repaint();
10828
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
+ }
10829
11148
  // Two-line confirmation modal that takes over the prompt area. Pass
10830
11149
  // null to dismiss. Currently unused — kept as a generic primitive for
10831
11150
  // any future modal that needs a question + hint footer.
@@ -11316,7 +11635,7 @@ uncaught: ${err.stack ?? err.message}
11316
11635
  this.drawSeparator(h - SESSIONBAR_ROWS);
11317
11636
  this.drawSessionbar();
11318
11637
  this.placeCursor();
11319
- if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
11638
+ if (this.permissionPrompt || this.optionsPrompt || this.confirmPrompt || this.helpPrompt) {
11320
11639
  this.term.hideCursor(false);
11321
11640
  }
11322
11641
  this.lastPromptRows = promptRows;
@@ -11420,7 +11739,7 @@ uncaught: ${err.stack ?? err.message}
11420
11739
  this.repaint();
11421
11740
  }
11422
11741
  completionRows() {
11423
- if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
11742
+ if (this.permissionPrompt || this.optionsPrompt || this.confirmPrompt || this.helpPrompt) {
11424
11743
  return 0;
11425
11744
  }
11426
11745
  return Math.min(MAX_COMPLETION_ROWS, this.completions.length);
@@ -11560,6 +11879,10 @@ uncaught: ${err.stack ?? err.message}
11560
11879
  this.drawPermissionPrompt();
11561
11880
  return;
11562
11881
  }
11882
+ if (this.optionsPrompt) {
11883
+ this.drawOptionsPrompt();
11884
+ return;
11885
+ }
11563
11886
  if (this.confirmPrompt) {
11564
11887
  this.drawConfirmPrompt();
11565
11888
  return;
@@ -11782,6 +12105,14 @@ uncaught: ${err.stack ?? err.message}
11782
12105
  this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
11783
12106
  return;
11784
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
+ }
11785
12116
  if (this.confirmPrompt) {
11786
12117
  const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
11787
12118
  this.term.moveTo(2, top2);
@@ -11820,6 +12151,9 @@ uncaught: ${err.stack ?? err.message}
11820
12151
  if (this.permissionPrompt) {
11821
12152
  return this.permissionRows();
11822
12153
  }
12154
+ if (this.optionsPrompt) {
12155
+ return this.optionsRows();
12156
+ }
11823
12157
  if (this.confirmPrompt) {
11824
12158
  return CONFIRM_PROMPT_ROWS;
11825
12159
  }
@@ -11844,6 +12178,64 @@ uncaught: ${err.stack ?? err.message}
11844
12178
  4 + this.permissionPrompt.options.length
11845
12179
  );
11846
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
+ }
11847
12239
  // Walk this.lines from the tail, accumulating wrapped rows via the
11848
12240
  // wrap cache, until we have at least `needed` rows or run out. Returns
11849
12241
  // the collected rows in original (top-down) order plus an `exhausted`
@@ -12018,7 +12410,7 @@ function drawBox(term, opts) {
12018
12410
  const termW = readTermWidth(term);
12019
12411
  const termH = readTermHeight(term);
12020
12412
  const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
12021
- const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
12413
+ const maxContentW = Math.max(10, termW - 4);
12022
12414
  const contentW = Math.min(desiredContentW, maxContentW);
12023
12415
  const w = contentW + 2;
12024
12416
  const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
@@ -12468,8 +12860,12 @@ async function pickSession(term, opts) {
12468
12860
  return base;
12469
12861
  };
12470
12862
  let visible = applyPrefsFilters(allSessions);
12863
+ const formatOpts = {
12864
+ columns: opts.config.tui.sessionColumns ?? DEFAULT_COLUMNS,
12865
+ cwdMaxWidth: opts.config.tui.cwdColumnMaxWidth
12866
+ };
12471
12867
  let rows = visible.map((s) => toRow(s, Date.now()));
12472
- let widths = computeWidths(rows);
12868
+ let widths = computeWidths(rows, formatOpts);
12473
12869
  let total = 1 + visible.length;
12474
12870
  let selectedIdx = 0;
12475
12871
  let scrollOffset = 0;
@@ -12496,7 +12892,11 @@ async function pickSession(term, opts) {
12496
12892
  let findInFlight = false;
12497
12893
  let renameBuffer = "";
12498
12894
  let transientStatus = null;
12895
+ let currentSessionGone = false;
12499
12896
  const composer = new InputDispatcher({ history: [] });
12897
+ if (opts.initialPrompt) {
12898
+ composer.setBuffer(opts.initialPrompt);
12899
+ }
12500
12900
  const composerHistoryCap = opts.config.tui.promptHistoryMaxEntries;
12501
12901
  loadHistory(paths.globalTuiHistoryFile()).then((entries) => {
12502
12902
  const capped = entries.length > composerHistoryCap ? entries.slice(entries.length - composerHistoryCap) : entries;
@@ -12521,7 +12921,6 @@ async function pickSession(term, opts) {
12521
12921
  let findBoxWindowStart = 0;
12522
12922
  let findBoxCursorVisualRow = 0;
12523
12923
  let findBoxCursorVisualCol = 0;
12524
- const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
12525
12924
  const computeLayout = () => {
12526
12925
  termHeight = readTermHeight2(term);
12527
12926
  termWidth = readTermWidth2(term);
@@ -12543,16 +12942,16 @@ async function pickSession(term, opts) {
12543
12942
  const reserved = 6 + composerRows;
12544
12943
  const maxViewportRows = Math.max(3, termHeight - reserved);
12545
12944
  viewportSize = Math.min(visible.length, maxViewportRows);
12546
- headerLine = formatRow(HEADER, widths, rowMaxWidth, cwdMaxWidth).padEnd(
12945
+ headerLine = formatRow(HEADER, widths, rowMaxWidth, formatOpts).padEnd(
12547
12946
  rowMaxWidth
12548
12947
  );
12549
12948
  sessionLines = rows.map(
12550
- (r) => formatRow(r, widths, rowMaxWidth, cwdMaxWidth).padEnd(rowMaxWidth)
12949
+ (r) => formatRow(r, widths, rowMaxWidth, formatOpts).padEnd(rowMaxWidth)
12551
12950
  );
12552
12951
  };
12553
12952
  const rebuildRows = () => {
12554
12953
  rows = visible.map((s) => toRow(s, Date.now()));
12555
- widths = computeWidths(rows);
12954
+ widths = computeWidths(rows, formatOpts);
12556
12955
  total = 1 + visible.length;
12557
12956
  computeLayout();
12558
12957
  };
@@ -13295,9 +13694,19 @@ async function pickSession(term, opts) {
13295
13694
  term.moveTo(1, indicatorRow() + 1);
13296
13695
  term("\n");
13297
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
+ };
13298
13707
  const renderFingerprint = () => {
13299
13708
  const cells = rows.map(
13300
- (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}`
13301
13710
  ).join("\n");
13302
13711
  return `${selectedIdx}:${scrollOffset}:${transientStatus ?? ""}
13303
13712
  ${cells}`;
@@ -13387,6 +13796,9 @@ ${cells}`;
13387
13796
  }
13388
13797
  mode = "normal";
13389
13798
  pendingAction = null;
13799
+ if (session.sessionId === opts.currentSessionId) {
13800
+ currentSessionGone = true;
13801
+ }
13390
13802
  await refresh(kind === "kill" ? session.sessionId : void 0);
13391
13803
  } catch (err) {
13392
13804
  mode = "normal";
@@ -13733,8 +14145,7 @@ ${cells}`;
13733
14145
  }
13734
14146
  if (selectedIdx === 0 && !searchActive) {
13735
14147
  if (name === "ESCAPE") {
13736
- cleanup();
13737
- resolve8({ kind: "abort" });
14148
+ tryAbort();
13738
14149
  return;
13739
14150
  }
13740
14151
  if (name === "ENTER" || name === "KP_ENTER") {
@@ -13787,8 +14198,7 @@ ${cells}`;
13787
14198
  const after = composer.state();
13788
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;
13789
14200
  if (effects.some((e) => e.type === "exit")) {
13790
- cleanup();
13791
- resolve8({ kind: "abort" });
14201
+ tryAbort();
13792
14202
  return;
13793
14203
  }
13794
14204
  if (unchanged) {
@@ -13861,8 +14271,7 @@ ${cells}`;
13861
14271
  return;
13862
14272
  }
13863
14273
  if (name === "q" || name === "Q") {
13864
- cleanup();
13865
- resolve8({ kind: "abort" });
14274
+ tryAbort();
13866
14275
  return;
13867
14276
  }
13868
14277
  if (name === "o" || name === "O") {
@@ -14038,8 +14447,7 @@ ${cells}`;
14038
14447
  case "ESCAPE":
14039
14448
  case "CTRL_C":
14040
14449
  case "CTRL_D":
14041
- cleanup();
14042
- resolve8({ kind: "abort" });
14450
+ tryAbort();
14043
14451
  return;
14044
14452
  }
14045
14453
  };
@@ -14088,6 +14496,9 @@ function sortSessions(sessions, cwd) {
14088
14496
  return 0;
14089
14497
  }
14090
14498
  const base = s.cwd === cwd ? 2 : 1;
14499
+ if (s.awaitingInput) {
14500
+ return base + 4;
14501
+ }
14091
14502
  return s.busy ? base + 2 : base;
14092
14503
  };
14093
14504
  return [...sessions].sort((a, b) => {
@@ -14404,28 +14815,218 @@ async function promptForImportCwd(term, session, opts = {}) {
14404
14815
  }
14405
14816
  return;
14406
14817
  }
14407
- if (name === "CTRL_U") {
14408
- buffer = "";
14409
- errorLine = null;
14410
- 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();
14411
15007
  return;
14412
15008
  }
14413
- if (name === "CTRL_W") {
14414
- const trimmedRight = buffer.replace(/[/\s]+$/, "");
14415
- const lastSep = Math.max(
14416
- trimmedRight.lastIndexOf("/"),
14417
- trimmedRight.lastIndexOf(" ")
14418
- );
14419
- buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
14420
- errorLine = null;
14421
- repaintInput();
15009
+ if (name === "DOWN" || name === "TAB") {
15010
+ moveDown();
14422
15011
  return;
14423
15012
  }
14424
15013
  if (data?.isCharacter) {
14425
- buffer += name;
14426
- errorLine = null;
14427
- repaintInput();
14428
- 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
+ }
14429
15030
  }
14430
15031
  };
14431
15032
  term.grabInput({});
@@ -14433,7 +15034,7 @@ async function promptForImportCwd(term, session, opts = {}) {
14433
15034
  term.on("resize", onResize);
14434
15035
  });
14435
15036
  }
14436
- function truncate3(s, max) {
15037
+ function truncate4(s, max) {
14437
15038
  if (max <= 1) {
14438
15039
  return "";
14439
15040
  }
@@ -14442,23 +15043,20 @@ function truncate3(s, max) {
14442
15043
  }
14443
15044
  return s.slice(0, Math.max(0, max - 1)) + "\u2026";
14444
15045
  }
14445
- function truncateLeft(s, max) {
14446
- if (max <= 1) {
14447
- return "";
14448
- }
14449
- if (s.length <= max) {
14450
- return s;
15046
+ function padRight2(s, w) {
15047
+ if (s.length >= w) {
15048
+ return s.slice(0, w);
14451
15049
  }
14452
- return "\u2026" + s.slice(s.length - (max - 1));
15050
+ return s + " ".repeat(w - s.length);
14453
15051
  }
14454
- var init_import_cwd_prompt = __esm({
14455
- "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"() {
14456
15055
  "use strict";
14457
- init_paths();
14458
- init_session();
14459
- init_cwd();
14460
- init_completion();
14461
15056
  init_prompt_utils();
15057
+ PREFERRED_DEFAULT = "opencode";
15058
+ MAX_VISIBLE_ROWS = 20;
15059
+ PREFERRED_CONTENT_WIDTH = 88;
14462
15060
  }
14463
15061
  });
14464
15062
 
@@ -14796,7 +15394,12 @@ async function runTuiApp(opts) {
14796
15394
  const term = termkit.terminal;
14797
15395
  const exitHint = {};
14798
15396
  const viewPrefs = {
14799
- 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
14800
15403
  };
14801
15404
  const pickerPrefs = createPickerPrefs();
14802
15405
  let altScreenEngaged = false;
@@ -14974,7 +15577,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14974
15577
  "current_mode_update",
14975
15578
  "available_commands_update",
14976
15579
  "available_modes_update",
14977
- "usage_update"
15580
+ "usage_update",
15581
+ "config_option_update"
14978
15582
  ]);
14979
15583
  const handleSessionUpdate = (params) => {
14980
15584
  const { update } = params ?? {};
@@ -15319,6 +15923,10 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15319
15923
  historyPolicy: "full",
15320
15924
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
15321
15925
  ...opts.readonly === true ? { readonly: true } : {},
15926
+ ...opts.drip === true ? {
15927
+ replayMode: "drip",
15928
+ ...opts.dripSpeed !== void 0 ? { dripSpeed: opts.dripSpeed } : {}
15929
+ } : {},
15322
15930
  // Forward the user-chosen cwd via a full resume hint. An empty
15323
15931
  // upstreamSessionId routes through doResurrectFromImport
15324
15932
  // (first-launch imports); a real one takes the normal session/load
@@ -15410,7 +16018,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15410
16018
  dispatcher,
15411
16019
  repaintThrottleMs: config.tui.repaintThrottleMs,
15412
16020
  maxScrollbackLines: config.tui.maxScrollbackLines,
15413
- mouse: config.tui.mouse,
16021
+ mouse: viewPrefs.mouseEnabled,
15414
16022
  progressIndicator: config.tui.progressIndicator,
15415
16023
  readonly: opts.readonly === true,
15416
16024
  onKey: (events) => {
@@ -15421,6 +16029,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15421
16029
  if (tryHandleHelpKey(ev)) {
15422
16030
  continue;
15423
16031
  }
16032
+ if (tryHandleOptionsKey(ev)) {
16033
+ continue;
16034
+ }
15424
16035
  if (tryHandleScrollbackSearchKey(ev)) {
15425
16036
  continue;
15426
16037
  }
@@ -15664,7 +16275,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15664
16275
  const buildHelpEntries = () => {
15665
16276
  const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
15666
16277
  const amendDesc = "amend the in-flight turn (cancel + replace)";
15667
- const head = config.tui.defaultEnterAction === "amend" ? [
16278
+ const head = viewPrefs.defaultEnterAction === "amend" ? [
15668
16279
  ["Enter", amendDesc],
15669
16280
  ["Ctrl+Enter / Shift+Enter / ^S", enqueueDesc]
15670
16281
  ] : [
@@ -15695,6 +16306,190 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15695
16306
  screen.setHelpPrompt(null);
15696
16307
  return true;
15697
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
+ };
15698
16493
  const teardown = () => {
15699
16494
  teardownStarted = true;
15700
16495
  process.off("SIGINT", sigintHandler);
@@ -15893,14 +16688,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15893
16688
  const handleEffect = (effect) => {
15894
16689
  switch (effect.type) {
15895
16690
  case "send":
15896
- if (config.tui.defaultEnterAction === "amend") {
16691
+ if (viewPrefs.defaultEnterAction === "amend") {
15897
16692
  amendPrompt(effect.text, effect.attachments, effect.displayText);
15898
16693
  } else {
15899
16694
  enqueuePrompt(effect.text, effect.attachments, effect.displayText);
15900
16695
  }
15901
16696
  return;
15902
16697
  case "amend":
15903
- if (config.tui.defaultEnterAction === "amend") {
16698
+ if (viewPrefs.defaultEnterAction === "amend") {
15904
16699
  enqueuePrompt(effect.text, effect.attachments, effect.displayText);
15905
16700
  } else {
15906
16701
  amendPrompt(effect.text, effect.attachments, effect.displayText);
@@ -15993,9 +16788,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15993
16788
  case "next-live-session":
15994
16789
  void cycleLiveSession();
15995
16790
  return;
15996
- case "toggle-tools":
15997
- toolsExpanded = !toolsExpanded;
15998
- renderToolsBlock();
16791
+ case "toggle-options":
16792
+ toggleOptionsModal();
15999
16793
  return;
16000
16794
  case "toggle-thoughts":
16001
16795
  viewPrefs.showThoughts = !viewPrefs.showThoughts;
@@ -16007,6 +16801,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16007
16801
  case "toggle-mouse": {
16008
16802
  const next = !screen.isMouseEnabled();
16009
16803
  screen.setMouseEnabled(next);
16804
+ viewPrefs.mouseEnabled = next;
16010
16805
  screen.notify(
16011
16806
  next ? "mouse capture on \u2014 wheel scrolls; shift+drag to select text" : "mouse capture off \u2014 click-drag selects text; PgUp/PgDn scrolls"
16012
16807
  );
@@ -16277,9 +17072,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16277
17072
  toolsBlockStartedAt = null;
16278
17073
  toolsBlockEndedAt = null;
16279
17074
  toolsBlockStopReason = null;
16280
- toolsExpanded = false;
16281
17075
  lastEditMarkPath = null;
16282
17076
  turnHasShownProse = false;
17077
+ renderedEditDiffs.clear();
16283
17078
  screen.clearScrollback();
16284
17079
  return true;
16285
17080
  case "/demo-plan": {
@@ -16460,16 +17255,27 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16460
17255
  }
16461
17256
  };
16462
17257
  const toolStates = /* @__PURE__ */ new Map();
17258
+ const renderedEditDiffs = /* @__PURE__ */ new Map();
16463
17259
  const exitPlanStates = /* @__PURE__ */ new Map();
16464
17260
  const toolCallOrder = [];
16465
- let toolsExpanded = false;
16466
17261
  let toolsBlockStartedAt = null;
16467
17262
  let toolsBlockEndedAt = null;
16468
17263
  let toolsBlockStopReason = null;
16469
17264
  let lastPlanEvent = null;
16470
17265
  const TOOLS_COLLAPSED_LIMIT = config.tui.maxToolItems;
16471
17266
  const PLAN_VISIBLE_LIMIT2 = config.tui.maxPlanItems;
16472
- 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
+ };
16473
17279
  let agentBuffer = "";
16474
17280
  let agentKey = null;
16475
17281
  let agentSeq = 0;
@@ -16537,7 +17343,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16537
17343
  }
16538
17344
  const total = toolCallOrder.length;
16539
17345
  const capped = TOOLS_COLLAPSED_LIMIT > 0;
16540
- 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));
16541
17347
  const hidden = total - visibleIds.length;
16542
17348
  const inProgress = toolsBlockEndedAt === null;
16543
17349
  const end = toolsBlockEndedAt ?? Date.now();
@@ -16556,12 +17362,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16556
17362
  const noun = total === 1 ? "tool" : "tools";
16557
17363
  const timing = stoppedReason !== null ? stoppedLabel : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
16558
17364
  const parts = [`${total} ${noun}`, timing];
16559
- if (inProgress && capped) {
16560
- if (hidden > 0) {
16561
- parts.push(`${hidden} hidden \u2014 ^O expand`);
16562
- } else if (toolsExpanded && total > TOOLS_COLLAPSED_LIMIT) {
16563
- parts.push("^O collapse");
16564
- }
17365
+ if (inProgress && capped && hidden > 0) {
17366
+ parts.push(`${hidden} hidden`);
16565
17367
  }
16566
17368
  summary = parts.join(" \xB7 ");
16567
17369
  }
@@ -16580,7 +17382,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16580
17382
  for (const id of visibleIds) {
16581
17383
  const state = toolStates.get(id);
16582
17384
  if (state) {
16583
- lines.push(...formatToolLine2(state));
17385
+ lines.push(...formatToolLine2(state, end));
16584
17386
  }
16585
17387
  }
16586
17388
  screen.upsertLines("tools", lines);
@@ -16597,7 +17399,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16597
17399
  const state = existing ?? {
16598
17400
  initialTitle: title ?? "tool",
16599
17401
  latestTitle: title ?? "tool",
16600
- status: status ?? "pending"
17402
+ status: status ?? "pending",
17403
+ startedAt: Date.now()
16601
17404
  };
16602
17405
  if (existing && title !== void 0) {
16603
17406
  state.latestTitle = title;
@@ -16608,6 +17411,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16608
17411
  if (!existing) {
16609
17412
  state.status = status ?? "pending";
16610
17413
  }
17414
+ if (state.endedAt === void 0 && isTerminalToolStatus(state.status)) {
17415
+ state.endedAt = Date.now();
17416
+ }
16611
17417
  if (errorText !== void 0) {
16612
17418
  state.errorText = errorText;
16613
17419
  }
@@ -16627,35 +17433,60 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16627
17433
  let lastEditMarkPath = null;
16628
17434
  let turnHasShownProse = false;
16629
17435
  const maybeRenderEditDiff = (toolCallId) => {
16630
- const mode = config.tui.showFileUpdates;
16631
- if (mode === "none") {
16632
- return;
16633
- }
16634
- const state = toolStates.get(toolCallId);
16635
- if (!state?.editDiff || state.status !== "completed") {
16636
- return;
16637
- }
16638
- if (mode === "diff") {
16639
- const lines2 = formatEditDiffBlock(state.editDiff, "diff");
16640
- if (lines2.length > 0) {
16641
- 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;
16642
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);
16643
17468
  return;
16644
17469
  }
16645
- if (!turnHasShownProse) {
16646
- return;
16647
- }
16648
- const diff = state.editDiff;
16649
- if (diff.path && diff.path === lastEditMarkPath) {
16650
- return;
16651
- }
16652
- const lines = formatEditDiffBlock(diff, "edit");
16653
- if (lines.length === 0) {
16654
- return;
17470
+ const diff = toolStates.get(toolCallId)?.editDiff;
17471
+ if (diff) {
17472
+ renderedEditDiffs.set(toolCallId, diff);
16655
17473
  }
16656
- screen.upsertLines(`editdiff:${toolCallId}`, lines);
16657
- if (diff.path) {
16658
- 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
+ }
16659
17490
  }
16660
17491
  };
16661
17492
  applyRenderEvent = (event) => {
@@ -16727,7 +17558,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16727
17558
  toolStates.clear();
16728
17559
  exitPlanStates.clear();
16729
17560
  toolCallOrder.length = 0;
16730
- toolsExpanded = false;
16731
17561
  toolsBlockEndedAt = null;
16732
17562
  lastEditMarkPath = null;
16733
17563
  turnHasShownProse = false;
@@ -16789,7 +17619,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16789
17619
  closeAgentText();
16790
17620
  closeThought();
16791
17621
  lastPlanEvent = event;
16792
- const lines = formatEvent(event, formatOptions);
17622
+ const lines = formatEvent(event, planFormatOptions());
16793
17623
  if (lines.length > 0) {
16794
17624
  screen.upsertLines("plan", [{ body: "" }, ...lines]);
16795
17625
  }
@@ -16838,7 +17668,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16838
17668
  stopped: true,
16839
17669
  amended: event.amended === true
16840
17670
  },
16841
- formatOptions
17671
+ planFormatOptions()
16842
17672
  );
16843
17673
  if (lines.length > 0) {
16844
17674
  screen.upsertLines("plan", [{ body: "" }, ...lines]);
@@ -16868,7 +17698,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16868
17698
  toolsBlockStartedAt = null;
16869
17699
  toolsBlockEndedAt = null;
16870
17700
  toolsBlockStopReason = null;
16871
- toolsExpanded = false;
16872
17701
  upstreamInterruptedSeen = false;
16873
17702
  lastEditMarkPath = null;
16874
17703
  turnHasShownProse = false;
@@ -16956,7 +17785,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16956
17785
  toolsBlockStartedAt = null;
16957
17786
  toolsBlockEndedAt = null;
16958
17787
  toolsBlockStopReason = null;
16959
- toolsExpanded = false;
16960
17788
  lastEditMarkPath = null;
16961
17789
  turnHasShownProse = false;
16962
17790
  };
@@ -17114,6 +17942,10 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17114
17942
  return ctx;
17115
17943
  }
17116
17944
  if (opts.forceNew) {
17945
+ const agentStep = await ensureAgentForNew(term, target, opts);
17946
+ if (agentStep !== "ok") {
17947
+ return null;
17948
+ }
17117
17949
  return newCtx(opts, cwd, config);
17118
17950
  }
17119
17951
  if (opts.resume) {
@@ -17137,7 +17969,8 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17137
17969
  sessions,
17138
17970
  config,
17139
17971
  target,
17140
- prefs: pickerPrefs
17972
+ prefs: pickerPrefs,
17973
+ ...opts.initialPrompt !== void 0 ? { initialPrompt: opts.initialPrompt } : {}
17141
17974
  });
17142
17975
  if (choice.kind === "abort") {
17143
17976
  return null;
@@ -17146,6 +17979,13 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17146
17979
  if (choice.prompt !== void 0) {
17147
17980
  opts.initialPrompt = choice.prompt;
17148
17981
  }
17982
+ const agentStep = await ensureAgentForNew(term, target, opts);
17983
+ if (agentStep === "cancel") {
17984
+ return null;
17985
+ }
17986
+ if (agentStep === "back") {
17987
+ continue;
17988
+ }
17149
17989
  return newCtx(opts, cwd, config);
17150
17990
  }
17151
17991
  if (choice.kind === "fork") {
@@ -17305,6 +18145,38 @@ function newCtx(opts, cwd, config) {
17305
18145
  cwd
17306
18146
  };
17307
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
+ }
17308
18180
  function debugLogUpdate(update, event) {
17309
18181
  writeDebugLine({
17310
18182
  src: "session/update",
@@ -17450,6 +18322,7 @@ var init_app = __esm({
17450
18322
  init_picker();
17451
18323
  init_import_cwd_prompt();
17452
18324
  init_import_action_prompt();
18325
+ init_agent_prompt();
17453
18326
  init_screen();
17454
18327
  init_input();
17455
18328
  init_attachments();
@@ -17475,7 +18348,7 @@ var init_app = __esm({
17475
18348
  ["Alt+N / Alt+Tab", "next live session"],
17476
18349
  ["^T", "show / hide thoughts"],
17477
18350
  ["^V", "paste image from clipboard"],
17478
- ["^O", "expand / collapse tools block"],
18351
+ ["^O", "session options (tools \xB7 plan \xB7 thoughts \xB7 diffs \xB7 mouse \xB7 enter)"],
17479
18352
  null,
17480
18353
  ["^R", "history reverse search (^S walks forward once engaged)"],
17481
18354
  ["PgUp / PgDn", "scroll scrollback"],
@@ -17516,6 +18389,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17516
18389
  "detach",
17517
18390
  "diff",
17518
18391
  "disabled",
18392
+ "drip",
17519
18393
  "fold",
17520
18394
  "follow",
17521
18395
  "force",
@@ -17538,8 +18412,10 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17538
18412
  var KNOWN_VALUE_FLAGS = /* @__PURE__ */ new Set([
17539
18413
  "agent",
17540
18414
  "args",
18415
+ "columns",
17541
18416
  "command",
17542
18417
  "cwd",
18418
+ "drip-speed",
17543
18419
  "env",
17544
18420
  "host",
17545
18421
  "model",
@@ -20001,7 +20877,14 @@ var SessionManager = class {
20001
20877
  histories: this.histories,
20002
20878
  synopsisAgent: this.synopsisAgent,
20003
20879
  synopsisModel: this.synopsisModel,
20004
- 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
+ },
20005
20888
  persistSynopsis: (id, synopsis, through) => this.persistSynopsis(id, synopsis, through),
20006
20889
  logger: this.logger,
20007
20890
  npmRegistry: this.npmRegistry
@@ -20354,27 +21237,27 @@ var SessionManager = class {
20354
21237
  return false;
20355
21238
  }
20356
21239
  }
20357
- // When the last client detaches from a session that resolves to
20358
- // non-interactive e.g. a `hydra cat` run, born interactive:undefined
20359
- // with originatingClient hydra-acp-cat, whose every prompt is ancillary
20360
- // close it so its agent process doesn't linger until the (default 1h)
20361
- // idle timeout fires. The cold record is kept, so the rare refine-in-TUI
20362
- // still works via the resurrect/reseed path. Sessions promoted to
20363
- // interactive (driven by a real, non-ancillary prompt) resolve to true
20364
- // 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.
20365
21255
  async reapIfOrphanedNonInteractive(sessionId) {
20366
21256
  const session = this.sessions.get(sessionId);
20367
21257
  if (!session || session.attachedCount > 0) {
20368
21258
  return;
20369
21259
  }
20370
- const interactive = effectiveInteractive(
20371
- {
20372
- interactive: session.interactive,
20373
- ...session.originatingClient ? { originatingClient: session.originatingClient } : {}
20374
- },
20375
- true
20376
- );
20377
- if (interactive !== false) {
21260
+ if (session.interactive === true) {
20378
21261
  return;
20379
21262
  }
20380
21263
  this.logger?.info(
@@ -20872,7 +21755,8 @@ var SessionManager = class {
20872
21755
  updatedAt: used,
20873
21756
  attachedClients: session.attachedCount,
20874
21757
  status: "live",
20875
- busy: session.turnStartedAt !== void 0
21758
+ busy: session.turnStartedAt !== void 0,
21759
+ awaitingInput: session.awaitingInput
20876
21760
  });
20877
21761
  }
20878
21762
  const records = await this.store.list().catch(() => []);
@@ -20910,7 +21794,8 @@ var SessionManager = class {
20910
21794
  updatedAt: used,
20911
21795
  attachedClients: 0,
20912
21796
  status: "cold",
20913
- busy: false
21797
+ busy: false,
21798
+ awaitingInput: false
20914
21799
  });
20915
21800
  }
20916
21801
  entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
@@ -24814,10 +25699,11 @@ function registerAcpWsEndpoint(app, deps) {
24814
25699
  params.clientInfo,
24815
25700
  params.clientId
24816
25701
  );
25702
+ const drip = params.replayMode === "drip";
24817
25703
  const { entries: replay, appliedPolicy } = await session.attach(
24818
25704
  client,
24819
25705
  params.historyPolicy,
24820
- { afterMessageId: params.afterMessageId }
25706
+ { afterMessageId: params.afterMessageId, raw: drip }
24821
25707
  );
24822
25708
  state.attached.set(session.sessionId, {
24823
25709
  sessionId: session.sessionId,
@@ -24825,10 +25711,35 @@ function registerAcpWsEndpoint(app, deps) {
24825
25711
  readonly
24826
25712
  });
24827
25713
  app.log.info(
24828
- `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" : ""}`
24829
25715
  );
24830
- for (const note of replay) {
24831
- 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
+ }
24832
25743
  }
24833
25744
  session.replayPendingPermissions(client);
24834
25745
  const modesPayload = buildModesPayload(session);
@@ -26689,12 +27600,15 @@ async function runSessionsList(opts = {}) {
26689
27600
  }
26690
27601
  const now = Date.now();
26691
27602
  const rows = visible.map((s) => toRow(s, now));
26692
- 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);
26693
27608
  const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
26694
- const cwdMax = config.tui.cwdColumnMaxWidth;
26695
- process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
27609
+ process.stdout.write(formatRow(HEADER, widths, maxWidth, formatOpts) + "\n");
26696
27610
  for (const r of rows) {
26697
- process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
27611
+ process.stdout.write(formatRow(r, widths, maxWidth, formatOpts) + "\n");
26698
27612
  }
26699
27613
  if (truncated > 0) {
26700
27614
  process.stdout.write(
@@ -26958,10 +27872,14 @@ function printBundleInfo(raw, cwdColumnMaxWidth) {
26958
27872
  }
26959
27873
  const summary = bundleToSummary(parsed);
26960
27874
  const row = toRow(summary);
26961
- const widths = computeWidths([row]);
27875
+ const formatOpts = {
27876
+ columns: ALL_COLUMNS,
27877
+ cwdMaxWidth: cwdColumnMaxWidth
27878
+ };
27879
+ const widths = computeWidths([row], formatOpts);
26962
27880
  const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
26963
- process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
26964
- 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");
26965
27883
  const originUpstream = parsed.session.upstreamSessionId ?? "-";
26966
27884
  process.stdout.write(
26967
27885
  `
@@ -27537,6 +28455,7 @@ function aggregate(bundle, status) {
27537
28455
  const durationMs = Number.isFinite(createdMs) && Number.isFinite(updatedMs) ? updatedMs - createdMs : null;
27538
28456
  return {
27539
28457
  sessionId: r.sessionId,
28458
+ ...r.upstreamSessionId !== void 0 ? { upstreamSessionId: r.upstreamSessionId } : {},
27540
28459
  ...r.title !== void 0 ? { title: r.title } : {},
27541
28460
  cwd: r.cwd,
27542
28461
  agentId: r.agentId,
@@ -27564,6 +28483,9 @@ function formatSummary(d, verbose) {
27564
28483
  const lines = [];
27565
28484
  const pad = (label) => label.padEnd(14);
27566
28485
  lines.push(`${pad("Session:")}${d.sessionId}`);
28486
+ if (d.upstreamSessionId) {
28487
+ lines.push(`${pad("Upstream:")}${d.upstreamSessionId}`);
28488
+ }
27567
28489
  if (d.title) {
27568
28490
  lines.push(`${pad("Title:")}${d.title}`);
27569
28491
  }
@@ -27688,6 +28610,9 @@ function formatDuration(ms) {
27688
28610
  return parts.join(" ");
27689
28611
  }
27690
28612
 
28613
+ // src/cli.ts
28614
+ init_session_row();
28615
+
27691
28616
  // src/cli/commands/extensions.ts
27692
28617
  init_config();
27693
28618
  init_service_token();
@@ -28704,6 +29629,32 @@ function formatAge(ms) {
28704
29629
  const day = Math.floor(hour / 24);
28705
29630
  return `${day} day${day === 1 ? "" : "s"}`;
28706
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
+ }
28707
29658
  async function runAgentsInstall(agentId) {
28708
29659
  if (!agentId) {
28709
29660
  process.stderr.write("Usage: hydra-acp agent install <agent-id>\n");
@@ -28875,16 +29826,7 @@ async function runAgentsSet(agentId, modelId) {
28875
29826
  process.exit(1);
28876
29827
  return;
28877
29828
  }
28878
- const raw = await readRawConfig3();
28879
- if (modelId === void 0) {
28880
- raw.defaultAgent = agentId;
28881
- await writeRawConfig3(raw);
28882
- } else {
28883
- const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
28884
- models[agentId] = modelId;
28885
- raw.defaultModels = models;
28886
- await writeRawConfig3(raw);
28887
- }
29829
+ await setDefaultAgent(agentId, modelId);
28888
29830
  const disk = readAgentDefaults(await readRawConfig3());
28889
29831
  if (modelId !== void 0 && agentId !== disk.agent) {
28890
29832
  process.stdout.write(
@@ -28936,13 +29878,6 @@ async function readRawConfig3() {
28936
29878
  const raw = await fsp12.readFile(paths.config(), "utf8");
28937
29879
  return JSON.parse(raw);
28938
29880
  }
28939
- async function writeRawConfig3(raw) {
28940
- await fsp12.writeFile(
28941
- paths.config(),
28942
- JSON.stringify(raw, null, 2) + "\n",
28943
- { encoding: "utf8", mode: 384 }
28944
- );
28945
- }
28946
29881
  async function runAgentsRefresh() {
28947
29882
  const config = await loadConfig();
28948
29883
  const serviceToken = await loadServiceToken();
@@ -30458,6 +31393,9 @@ async function main() {
30458
31393
  if (streamBufferBytes !== void 0) {
30459
31394
  catOpts.streamBufferBytes = streamBufferBytes;
30460
31395
  }
31396
+ if (agentIdFromFlag !== void 0) {
31397
+ await assertKnownAgent(agentIdFromFlag);
31398
+ }
30461
31399
  suppressUpdateNotice = true;
30462
31400
  await runCat(catOpts);
30463
31401
  return;
@@ -30498,11 +31436,24 @@ async function main() {
30498
31436
  case "sessions": {
30499
31437
  const sub = positional[1];
30500
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
+ }
30501
31451
  await runSessionsList({
30502
31452
  all: flags.all === true,
30503
31453
  json: flags.json === true,
30504
31454
  host: typeof flags.host === "string" ? flags.host : void 0,
30505
- includeNonInteractive: flags["include-non-interactive"] === true
31455
+ includeNonInteractive: flags["include-non-interactive"] === true,
31456
+ columns
30506
31457
  });
30507
31458
  return;
30508
31459
  }
@@ -30737,6 +31688,9 @@ async function dispatchTui(flags, base) {
30737
31688
  );
30738
31689
  process.exit(2);
30739
31690
  }
31691
+ if (base.agentId !== void 0) {
31692
+ await assertKnownAgent(base.agentId);
31693
+ }
30740
31694
  setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
30741
31695
  const { runTui } = await Promise.resolve().then(() => (init_tui(), tui_exports));
30742
31696
  const tuiOpts = { resume, forceNew, readonly };
@@ -30761,6 +31715,19 @@ async function dispatchTui(flags, base) {
30761
31715
  if (base.dangerouslySkipPermissions === true) {
30762
31716
  tuiOpts.dangerouslySkipPermissions = true;
30763
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
+ }
30764
31731
  await runTui(tuiOpts);
30765
31732
  }
30766
31733
  function parseNumericFlag(flags, name) {
@@ -30875,10 +31842,11 @@ function printHelp() {
30875
31842
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
30876
31843
  " hydra-acp daemon stop|restart",
30877
31844
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
30878
- " 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>]",
30879
31846
  " List sessions (live + 20 most-recent cold; --all lifts the cold cap AND surfaces non-interactive sessions; --json emits JSON for scripts).",
30880
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.",
30881
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.",
30882
31850
  " hydra-acp session info <id> [--verbose] [--json] [--diff] [--fold] [--no-color] [--no-pager]",
30883
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).",
30884
31852
  " hydra-acp session diff <id> [--json] [--no-color] [--no-pager] [--fold]",