@hydra-acp/cli 0.1.59 → 0.1.61

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
+ "age",
6871
+ "cwd",
6872
+ "title",
6873
+ "agent",
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;
@@ -12481,8 +12877,12 @@ async function pickSession(term, opts) {
12481
12877
  }
12482
12878
  let searchActive = false;
12483
12879
  let searchTerm = "";
12880
+ const UP_REPEAT_GAP_MS = 120;
12881
+ let upGuardArmed = false;
12882
+ let lastUpAt = 0;
12484
12883
  let mode = "normal";
12485
12884
  let pendingAction = null;
12885
+ let findLayerActive = false;
12486
12886
  let findSubMode = "input";
12487
12887
  let findComposer = new InputDispatcher({
12488
12888
  history: [],
@@ -12496,7 +12896,11 @@ async function pickSession(term, opts) {
12496
12896
  let findInFlight = false;
12497
12897
  let renameBuffer = "";
12498
12898
  let transientStatus = null;
12899
+ let currentSessionGone = false;
12499
12900
  const composer = new InputDispatcher({ history: [] });
12901
+ if (opts.initialPrompt) {
12902
+ composer.setBuffer(opts.initialPrompt);
12903
+ }
12500
12904
  const composerHistoryCap = opts.config.tui.promptHistoryMaxEntries;
12501
12905
  loadHistory(paths.globalTuiHistoryFile()).then((entries) => {
12502
12906
  const capped = entries.length > composerHistoryCap ? entries.slice(entries.length - composerHistoryCap) : entries;
@@ -12521,7 +12925,6 @@ async function pickSession(term, opts) {
12521
12925
  let findBoxWindowStart = 0;
12522
12926
  let findBoxCursorVisualRow = 0;
12523
12927
  let findBoxCursorVisualCol = 0;
12524
- const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
12525
12928
  const computeLayout = () => {
12526
12929
  termHeight = readTermHeight2(term);
12527
12930
  termWidth = readTermWidth2(term);
@@ -12543,16 +12946,16 @@ async function pickSession(term, opts) {
12543
12946
  const reserved = 6 + composerRows;
12544
12947
  const maxViewportRows = Math.max(3, termHeight - reserved);
12545
12948
  viewportSize = Math.min(visible.length, maxViewportRows);
12546
- headerLine = formatRow(HEADER, widths, rowMaxWidth, cwdMaxWidth).padEnd(
12949
+ headerLine = formatRow(HEADER, widths, rowMaxWidth, formatOpts).padEnd(
12547
12950
  rowMaxWidth
12548
12951
  );
12549
12952
  sessionLines = rows.map(
12550
- (r) => formatRow(r, widths, rowMaxWidth, cwdMaxWidth).padEnd(rowMaxWidth)
12953
+ (r) => formatRow(r, widths, rowMaxWidth, formatOpts).padEnd(rowMaxWidth)
12551
12954
  );
12552
12955
  };
12553
12956
  const rebuildRows = () => {
12554
12957
  rows = visible.map((s) => toRow(s, Date.now()));
12555
- widths = computeWidths(rows);
12958
+ widths = computeWidths(rows, formatOpts);
12556
12959
  total = 1 + visible.length;
12557
12960
  computeLayout();
12558
12961
  };
@@ -13055,6 +13458,16 @@ async function pickSession(term, opts) {
13055
13458
  });
13056
13459
  };
13057
13460
  const findQueryText = () => findComposer.state().buffer.join("\n");
13461
+ const feedFindPaste = (text) => {
13462
+ const prevRows = findBoxRows;
13463
+ findComposer.feed({ type: "paste", text });
13464
+ computeFindBoxLayout();
13465
+ if (findBoxRows !== prevRows) {
13466
+ renderFind();
13467
+ } else {
13468
+ repaintFindBoxBodyRows();
13469
+ }
13470
+ };
13058
13471
  const runFind = async () => {
13059
13472
  const query = findQueryText().trim();
13060
13473
  if (query.length === 0) {
@@ -13197,7 +13610,11 @@ async function pickSession(term, opts) {
13197
13610
  const pasted = Buffer.from(pasteBuffer, "binary").toString("utf-8").replace(/\r\n?/g, "\n");
13198
13611
  pasteBuffer = "";
13199
13612
  const remaining = text.slice(endIdx + PASTE_END.length);
13200
- if (selectedIdx === 0 && !searchActive) {
13613
+ if (findLayerActive) {
13614
+ if (findSubMode === "input" && !findInFlight) {
13615
+ feedFindPaste(pasted);
13616
+ }
13617
+ } else if (selectedIdx === 0 && !searchActive) {
13201
13618
  composer.feed({ type: "paste", text: pasted });
13202
13619
  const after = composer.state();
13203
13620
  const newVr = computePromptVisualRows(after.buffer, composerRoom);
@@ -13260,6 +13677,7 @@ async function pickSession(term, opts) {
13260
13677
  findError = null;
13261
13678
  findInFlight = false;
13262
13679
  findSubMode = "input";
13680
+ findLayerActive = false;
13263
13681
  popLayer();
13264
13682
  };
13265
13683
  const dispatch = (name, _matches, data) => {
@@ -13295,9 +13713,19 @@ async function pickSession(term, opts) {
13295
13713
  term.moveTo(1, indicatorRow() + 1);
13296
13714
  term("\n");
13297
13715
  };
13716
+ const tryAbort = () => {
13717
+ if (currentSessionGone) {
13718
+ transientStatus = "current session ended \u2014 pick a session or start a new one";
13719
+ paintIndicator();
13720
+ return false;
13721
+ }
13722
+ cleanup();
13723
+ resolve8({ kind: "abort" });
13724
+ return true;
13725
+ };
13298
13726
  const renderFingerprint = () => {
13299
13727
  const cells = rows.map(
13300
- (r) => `${r.session}|${r.upstream}|${r.state}|${r.agent}|${r.age}|${r.title}|${r.cwd}`
13728
+ (r) => `${r.session}|${r.upstream}|${r.host}|${r.state}|${r.agent}|${r.model}|${r.age}|${r.title}|${r.cwd}|${r.cost}`
13301
13729
  ).join("\n");
13302
13730
  return `${selectedIdx}:${scrollOffset}:${transientStatus ?? ""}
13303
13731
  ${cells}`;
@@ -13387,6 +13815,9 @@ ${cells}`;
13387
13815
  }
13388
13816
  mode = "normal";
13389
13817
  pendingAction = null;
13818
+ if (session.sessionId === opts.currentSessionId) {
13819
+ currentSessionGone = true;
13820
+ }
13390
13821
  await refresh(kind === "kill" ? session.sessionId : void 0);
13391
13822
  } catch (err) {
13392
13823
  mode = "normal";
@@ -13459,7 +13890,10 @@ ${cells}`;
13459
13890
  paintIndicator();
13460
13891
  return;
13461
13892
  }
13462
- findComposer = new InputDispatcher({ history: [] });
13893
+ findComposer = new InputDispatcher({
13894
+ history: [],
13895
+ collapsePastes: false
13896
+ });
13463
13897
  findResults = [];
13464
13898
  findTruncated = false;
13465
13899
  findSelectedIdx = 0;
@@ -13468,6 +13902,7 @@ ${cells}`;
13468
13902
  findError = null;
13469
13903
  findInFlight = false;
13470
13904
  findSubMode = "input";
13905
+ findLayerActive = true;
13471
13906
  computeFindBoxLayout();
13472
13907
  renderFind();
13473
13908
  const findOnKey = (name, _matches, data) => {
@@ -13733,8 +14168,7 @@ ${cells}`;
13733
14168
  }
13734
14169
  if (selectedIdx === 0 && !searchActive) {
13735
14170
  if (name === "ESCAPE") {
13736
- cleanup();
13737
- resolve8({ kind: "abort" });
14171
+ tryAbort();
13738
14172
  return;
13739
14173
  }
13740
14174
  if (name === "ENTER" || name === "KP_ENTER") {
@@ -13747,6 +14181,16 @@ ${cells}`;
13747
14181
  }
13748
14182
  return;
13749
14183
  }
14184
+ if (name === "UP" && upGuardArmed) {
14185
+ const now = Date.now();
14186
+ const gap = now - lastUpAt;
14187
+ lastUpAt = now;
14188
+ if (gap < UP_REPEAT_GAP_MS) {
14189
+ placeComposerCursor();
14190
+ return;
14191
+ }
14192
+ upGuardArmed = false;
14193
+ }
13750
14194
  if (name === "DOWN") {
13751
14195
  const cs = composer.state();
13752
14196
  const inWalk = cs.historyIndex !== -1 || cs.queueIndex !== -1;
@@ -13769,6 +14213,7 @@ ${cells}`;
13769
14213
  }
13770
14214
  return;
13771
14215
  }
14216
+ upGuardArmed = false;
13772
14217
  const before = composer.state();
13773
14218
  let event = null;
13774
14219
  if (data?.isCharacter) {
@@ -13787,8 +14232,7 @@ ${cells}`;
13787
14232
  const after = composer.state();
13788
14233
  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
14234
  if (effects.some((e) => e.type === "exit")) {
13790
- cleanup();
13791
- resolve8({ kind: "abort" });
14235
+ tryAbort();
13792
14236
  return;
13793
14237
  }
13794
14238
  if (unchanged) {
@@ -13861,8 +14305,7 @@ ${cells}`;
13861
14305
  return;
13862
14306
  }
13863
14307
  if (name === "q" || name === "Q") {
13864
- cleanup();
13865
- resolve8({ kind: "abort" });
14308
+ tryAbort();
13866
14309
  return;
13867
14310
  }
13868
14311
  if (name === "o" || name === "O") {
@@ -13994,6 +14437,12 @@ ${cells}`;
13994
14437
  case "UP":
13995
14438
  case "SHIFT_TAB":
13996
14439
  case "CTRL_P":
14440
+ if (name === "UP") {
14441
+ if (selectedIdx === 1) {
14442
+ upGuardArmed = true;
14443
+ }
14444
+ lastUpAt = Date.now();
14445
+ }
13997
14446
  move(-1);
13998
14447
  return;
13999
14448
  case "DOWN":
@@ -14038,8 +14487,7 @@ ${cells}`;
14038
14487
  case "ESCAPE":
14039
14488
  case "CTRL_C":
14040
14489
  case "CTRL_D":
14041
- cleanup();
14042
- resolve8({ kind: "abort" });
14490
+ tryAbort();
14043
14491
  return;
14044
14492
  }
14045
14493
  };
@@ -14088,6 +14536,9 @@ function sortSessions(sessions, cwd) {
14088
14536
  return 0;
14089
14537
  }
14090
14538
  const base = s.cwd === cwd ? 2 : 1;
14539
+ if (s.awaitingInput) {
14540
+ return base + 4;
14541
+ }
14091
14542
  return s.busy ? base + 2 : base;
14092
14543
  };
14093
14544
  return [...sessions].sort((a, b) => {
@@ -14404,28 +14855,218 @@ async function promptForImportCwd(term, session, opts = {}) {
14404
14855
  }
14405
14856
  return;
14406
14857
  }
14407
- if (name === "CTRL_U") {
14408
- buffer = "";
14409
- errorLine = null;
14410
- repaintInput();
14858
+ if (name === "CTRL_U") {
14859
+ buffer = "";
14860
+ errorLine = null;
14861
+ repaintInput();
14862
+ return;
14863
+ }
14864
+ if (name === "CTRL_W") {
14865
+ const trimmedRight = buffer.replace(/[/\s]+$/, "");
14866
+ const lastSep = Math.max(
14867
+ trimmedRight.lastIndexOf("/"),
14868
+ trimmedRight.lastIndexOf(" ")
14869
+ );
14870
+ buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
14871
+ errorLine = null;
14872
+ repaintInput();
14873
+ return;
14874
+ }
14875
+ if (data?.isCharacter) {
14876
+ buffer += name;
14877
+ errorLine = null;
14878
+ repaintInput();
14879
+ return;
14880
+ }
14881
+ };
14882
+ term.grabInput({});
14883
+ term.on("key", onKey);
14884
+ term.on("resize", onResize);
14885
+ });
14886
+ }
14887
+ function truncate3(s, max) {
14888
+ if (max <= 1) {
14889
+ return "";
14890
+ }
14891
+ if (s.length <= max) {
14892
+ return s;
14893
+ }
14894
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
14895
+ }
14896
+ function truncateLeft(s, max) {
14897
+ if (max <= 1) {
14898
+ return "";
14899
+ }
14900
+ if (s.length <= max) {
14901
+ return s;
14902
+ }
14903
+ return "\u2026" + s.slice(s.length - (max - 1));
14904
+ }
14905
+ var init_import_cwd_prompt = __esm({
14906
+ "src/tui/import-cwd-prompt.ts"() {
14907
+ "use strict";
14908
+ init_paths();
14909
+ init_session();
14910
+ init_cwd();
14911
+ init_completion();
14912
+ init_prompt_utils();
14913
+ }
14914
+ });
14915
+
14916
+ // src/tui/agent-prompt.ts
14917
+ function initialIndex(agents) {
14918
+ const idx = agents.findIndex((a) => a.id === PREFERRED_DEFAULT);
14919
+ return idx === -1 ? 0 : idx;
14920
+ }
14921
+ async function promptForAgent(term, agents) {
14922
+ resetTerminalModes();
14923
+ let selected = initialIndex(agents);
14924
+ let windowStart = 0;
14925
+ const visibleRows = () => {
14926
+ const termBudget = readTermHeight(term) - 8;
14927
+ return Math.max(1, Math.min(MAX_VISIBLE_ROWS, agents.length, termBudget));
14928
+ };
14929
+ const reclamp = () => {
14930
+ const rows = visibleRows();
14931
+ if (selected < windowStart) {
14932
+ windowStart = selected;
14933
+ } else if (selected >= windowStart + rows) {
14934
+ windowStart = selected - rows + 1;
14935
+ }
14936
+ const maxStart = Math.max(0, agents.length - rows);
14937
+ if (windowStart > maxStart) {
14938
+ windowStart = maxStart;
14939
+ }
14940
+ if (windowStart < 0) {
14941
+ windowStart = 0;
14942
+ }
14943
+ };
14944
+ const render = () => {
14945
+ reclamp();
14946
+ const rows = visibleRows();
14947
+ const contentHeight = rows + 4;
14948
+ const contentWidth = Math.min(
14949
+ PREFERRED_CONTENT_WIDTH,
14950
+ Math.max(40, readTermWidth(term) - 8)
14951
+ );
14952
+ const layout = drawBox(term, {
14953
+ contentHeight,
14954
+ contentWidth,
14955
+ title: "Select agent"
14956
+ });
14957
+ const innerW = layout.contentW;
14958
+ let row = 0;
14959
+ term.moveTo(layout.contentX, layout.contentY + row);
14960
+ term.noFormat(" Which agent should this session use?");
14961
+ row += 2;
14962
+ const end = Math.min(agents.length, windowStart + rows);
14963
+ for (let i = windowStart; i < end; i++) {
14964
+ const agent = agents[i];
14965
+ if (!agent) {
14966
+ continue;
14967
+ }
14968
+ const pointer = i === selected ? "\u276F" : " ";
14969
+ const desc = agent.description ?? agent.name;
14970
+ const idPart = ` ${pointer} ${agent.id}`;
14971
+ term.moveTo(layout.contentX, layout.contentY + row);
14972
+ if (i === selected) {
14973
+ const line = `${idPart} ${desc}`;
14974
+ term.brightWhite.bgBlue.noFormat(padRight2(truncate4(line, innerW), innerW));
14975
+ } else {
14976
+ term.noFormat(idPart);
14977
+ const room = innerW - idPart.length - 2;
14978
+ if (room > 1) {
14979
+ term.dim.noFormat(` ${truncate4(desc, room)}`);
14980
+ }
14981
+ }
14982
+ row++;
14983
+ }
14984
+ row++;
14985
+ term.moveTo(layout.contentX, layout.contentY + row);
14986
+ const more = agents.length > rows ? ` (${selected + 1}/${agents.length})` : "";
14987
+ term.dim.noFormat(
14988
+ ` \u2191/\u2193 navigate \xB7 Enter this session \xB7 s set default \xB7 Esc back${more}`
14989
+ );
14990
+ return layout;
14991
+ };
14992
+ render();
14993
+ term.hideCursor();
14994
+ return await new Promise((resolve8) => {
14995
+ let resolved = false;
14996
+ const cleanup = () => {
14997
+ if (resolved) {
14998
+ return;
14999
+ }
15000
+ resolved = true;
15001
+ term.off("key", onKey);
15002
+ term.off("resize", onResize);
15003
+ term.grabInput(false);
15004
+ term.hideCursor(false);
15005
+ term.moveTo(1, 1).eraseDisplayBelow();
15006
+ };
15007
+ const finish = (value) => {
15008
+ cleanup();
15009
+ resolve8(value);
15010
+ };
15011
+ const onResize = () => {
15012
+ if (resolved) {
15013
+ return;
15014
+ }
15015
+ render();
15016
+ };
15017
+ const moveDown = () => {
15018
+ if (selected < agents.length - 1) {
15019
+ selected++;
15020
+ render();
15021
+ }
15022
+ };
15023
+ const moveUp = () => {
15024
+ if (selected > 0) {
15025
+ selected--;
15026
+ render();
15027
+ }
15028
+ };
15029
+ const onKey = (name, _m, data) => {
15030
+ if (name === "CTRL_C" || name === "CTRL_D") {
15031
+ finish({ kind: "cancel" });
15032
+ return;
15033
+ }
15034
+ if (name === "ESCAPE") {
15035
+ finish({ kind: "back" });
15036
+ return;
15037
+ }
15038
+ if (name === "ENTER" || name === "KP_ENTER") {
15039
+ const agent = agents[selected];
15040
+ if (agent) {
15041
+ finish({ kind: "select", agentId: agent.id, persist: false });
15042
+ }
15043
+ return;
15044
+ }
15045
+ if (name === "UP" || name === "SHIFT_TAB") {
15046
+ moveUp();
14411
15047
  return;
14412
15048
  }
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();
15049
+ if (name === "DOWN" || name === "TAB") {
15050
+ moveDown();
14422
15051
  return;
14423
15052
  }
14424
15053
  if (data?.isCharacter) {
14425
- buffer += name;
14426
- errorLine = null;
14427
- repaintInput();
14428
- return;
15054
+ const lower = name.toLowerCase();
15055
+ if (lower === "s") {
15056
+ const agent = agents[selected];
15057
+ if (agent) {
15058
+ finish({ kind: "select", agentId: agent.id, persist: true });
15059
+ }
15060
+ return;
15061
+ }
15062
+ if (lower === "j") {
15063
+ moveDown();
15064
+ return;
15065
+ }
15066
+ if (lower === "k") {
15067
+ moveUp();
15068
+ return;
15069
+ }
14429
15070
  }
14430
15071
  };
14431
15072
  term.grabInput({});
@@ -14433,7 +15074,7 @@ async function promptForImportCwd(term, session, opts = {}) {
14433
15074
  term.on("resize", onResize);
14434
15075
  });
14435
15076
  }
14436
- function truncate3(s, max) {
15077
+ function truncate4(s, max) {
14437
15078
  if (max <= 1) {
14438
15079
  return "";
14439
15080
  }
@@ -14442,23 +15083,20 @@ function truncate3(s, max) {
14442
15083
  }
14443
15084
  return s.slice(0, Math.max(0, max - 1)) + "\u2026";
14444
15085
  }
14445
- function truncateLeft(s, max) {
14446
- if (max <= 1) {
14447
- return "";
14448
- }
14449
- if (s.length <= max) {
14450
- return s;
15086
+ function padRight2(s, w) {
15087
+ if (s.length >= w) {
15088
+ return s.slice(0, w);
14451
15089
  }
14452
- return "\u2026" + s.slice(s.length - (max - 1));
15090
+ return s + " ".repeat(w - s.length);
14453
15091
  }
14454
- var init_import_cwd_prompt = __esm({
14455
- "src/tui/import-cwd-prompt.ts"() {
15092
+ var PREFERRED_DEFAULT, MAX_VISIBLE_ROWS, PREFERRED_CONTENT_WIDTH;
15093
+ var init_agent_prompt = __esm({
15094
+ "src/tui/agent-prompt.ts"() {
14456
15095
  "use strict";
14457
- init_paths();
14458
- init_session();
14459
- init_cwd();
14460
- init_completion();
14461
15096
  init_prompt_utils();
15097
+ PREFERRED_DEFAULT = "opencode";
15098
+ MAX_VISIBLE_ROWS = 20;
15099
+ PREFERRED_CONTENT_WIDTH = 88;
14462
15100
  }
14463
15101
  });
14464
15102
 
@@ -14796,7 +15434,12 @@ async function runTuiApp(opts) {
14796
15434
  const term = termkit.terminal;
14797
15435
  const exitHint = {};
14798
15436
  const viewPrefs = {
14799
- showThoughts: config.tui.showThoughts
15437
+ showThoughts: config.tui.showThoughts,
15438
+ toolsExpanded: false,
15439
+ planExpanded: false,
15440
+ showFileUpdates: config.tui.showFileUpdates,
15441
+ mouseEnabled: config.tui.mouse,
15442
+ defaultEnterAction: config.tui.defaultEnterAction
14800
15443
  };
14801
15444
  const pickerPrefs = createPickerPrefs();
14802
15445
  let altScreenEngaged = false;
@@ -14974,7 +15617,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14974
15617
  "current_mode_update",
14975
15618
  "available_commands_update",
14976
15619
  "available_modes_update",
14977
- "usage_update"
15620
+ "usage_update",
15621
+ "config_option_update"
14978
15622
  ]);
14979
15623
  const handleSessionUpdate = (params) => {
14980
15624
  const { update } = params ?? {};
@@ -15319,6 +15963,10 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15319
15963
  historyPolicy: "full",
15320
15964
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
15321
15965
  ...opts.readonly === true ? { readonly: true } : {},
15966
+ ...opts.drip === true ? {
15967
+ replayMode: "drip",
15968
+ ...opts.dripSpeed !== void 0 ? { dripSpeed: opts.dripSpeed } : {}
15969
+ } : {},
15322
15970
  // Forward the user-chosen cwd via a full resume hint. An empty
15323
15971
  // upstreamSessionId routes through doResurrectFromImport
15324
15972
  // (first-launch imports); a real one takes the normal session/load
@@ -15410,7 +16058,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15410
16058
  dispatcher,
15411
16059
  repaintThrottleMs: config.tui.repaintThrottleMs,
15412
16060
  maxScrollbackLines: config.tui.maxScrollbackLines,
15413
- mouse: config.tui.mouse,
16061
+ mouse: viewPrefs.mouseEnabled,
15414
16062
  progressIndicator: config.tui.progressIndicator,
15415
16063
  readonly: opts.readonly === true,
15416
16064
  onKey: (events) => {
@@ -15421,6 +16069,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15421
16069
  if (tryHandleHelpKey(ev)) {
15422
16070
  continue;
15423
16071
  }
16072
+ if (tryHandleOptionsKey(ev)) {
16073
+ continue;
16074
+ }
15424
16075
  if (tryHandleScrollbackSearchKey(ev)) {
15425
16076
  continue;
15426
16077
  }
@@ -15664,7 +16315,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15664
16315
  const buildHelpEntries = () => {
15665
16316
  const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
15666
16317
  const amendDesc = "amend the in-flight turn (cancel + replace)";
15667
- const head = config.tui.defaultEnterAction === "amend" ? [
16318
+ const head = viewPrefs.defaultEnterAction === "amend" ? [
15668
16319
  ["Enter", amendDesc],
15669
16320
  ["Ctrl+Enter / Shift+Enter / ^S", enqueueDesc]
15670
16321
  ] : [
@@ -15695,6 +16346,190 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15695
16346
  screen.setHelpPrompt(null);
15696
16347
  return true;
15697
16348
  };
16349
+ const OPTION_IDS = [
16350
+ "tools",
16351
+ "plan",
16352
+ "thoughts",
16353
+ "diffs",
16354
+ "mouse",
16355
+ "enter"
16356
+ ];
16357
+ let optionsSelectedIndex = 0;
16358
+ const optionValue = (id) => {
16359
+ switch (id) {
16360
+ case "tools":
16361
+ return viewPrefs.toolsExpanded ? "expanded" : "collapsed";
16362
+ case "plan":
16363
+ return viewPrefs.planExpanded ? "expanded" : "collapsed";
16364
+ case "thoughts":
16365
+ return viewPrefs.showThoughts ? "shown" : "hidden";
16366
+ case "diffs":
16367
+ return viewPrefs.showFileUpdates;
16368
+ case "mouse":
16369
+ return viewPrefs.mouseEnabled ? "on" : "off";
16370
+ case "enter":
16371
+ return viewPrefs.defaultEnterAction;
16372
+ }
16373
+ };
16374
+ const optionLabel = (id) => {
16375
+ switch (id) {
16376
+ case "tools":
16377
+ return "Tools";
16378
+ case "plan":
16379
+ return "Plan";
16380
+ case "thoughts":
16381
+ return "Thoughts";
16382
+ case "diffs":
16383
+ return "File updates";
16384
+ case "mouse":
16385
+ return "Mouse capture";
16386
+ case "enter":
16387
+ return "Enter key";
16388
+ }
16389
+ };
16390
+ const buildOptionsSpec = () => ({
16391
+ title: "Session options",
16392
+ options: OPTION_IDS.map((id) => ({
16393
+ label: optionLabel(id),
16394
+ value: optionValue(id)
16395
+ })),
16396
+ selectedIndex: optionsSelectedIndex
16397
+ });
16398
+ const refreshOptionsPrompt = () => {
16399
+ if (!screen.isOptionsPromptActive()) {
16400
+ return;
16401
+ }
16402
+ screen.setOptionsPrompt(buildOptionsSpec());
16403
+ };
16404
+ const toggleOptionsModal = () => {
16405
+ if (screen.isOptionsPromptActive()) {
16406
+ screen.setOptionsPrompt(null);
16407
+ return;
16408
+ }
16409
+ optionsSelectedIndex = 0;
16410
+ screen.setOptionsPrompt(buildOptionsSpec());
16411
+ };
16412
+ const applyOptionToggle = (id) => {
16413
+ switch (id) {
16414
+ case "tools":
16415
+ viewPrefs.toolsExpanded = !viewPrefs.toolsExpanded;
16416
+ renderToolsBlock();
16417
+ break;
16418
+ case "plan":
16419
+ viewPrefs.planExpanded = !viewPrefs.planExpanded;
16420
+ rerenderPlan();
16421
+ break;
16422
+ case "thoughts":
16423
+ viewPrefs.showThoughts = !viewPrefs.showThoughts;
16424
+ screen.setHideThoughts(!viewPrefs.showThoughts);
16425
+ break;
16426
+ case "diffs":
16427
+ viewPrefs.showFileUpdates = viewPrefs.showFileUpdates === "diff" ? "edit" : "diff";
16428
+ reRenderAllEditDiffs();
16429
+ break;
16430
+ case "mouse": {
16431
+ const next = !screen.isMouseEnabled();
16432
+ screen.setMouseEnabled(next);
16433
+ viewPrefs.mouseEnabled = next;
16434
+ break;
16435
+ }
16436
+ case "enter":
16437
+ viewPrefs.defaultEnterAction = viewPrefs.defaultEnterAction === "amend" ? "enqueue" : "amend";
16438
+ break;
16439
+ }
16440
+ refreshOptionsPrompt();
16441
+ };
16442
+ const saveOption = (id) => {
16443
+ void (async () => {
16444
+ try {
16445
+ switch (id) {
16446
+ case "tools":
16447
+ case "plan":
16448
+ screen.notify(`${optionLabel(id)} is session-only \u2014 not saved`);
16449
+ return;
16450
+ case "thoughts":
16451
+ await setTuiConfigValue("showThoughts", viewPrefs.showThoughts);
16452
+ break;
16453
+ case "diffs":
16454
+ await setTuiConfigValue(
16455
+ "showFileUpdates",
16456
+ viewPrefs.showFileUpdates
16457
+ );
16458
+ break;
16459
+ case "mouse":
16460
+ await setTuiConfigValue("mouse", viewPrefs.mouseEnabled);
16461
+ break;
16462
+ case "enter":
16463
+ await setTuiConfigValue(
16464
+ "defaultEnterAction",
16465
+ viewPrefs.defaultEnterAction
16466
+ );
16467
+ break;
16468
+ }
16469
+ screen.notify(`saved default: ${optionLabel(id)} ${optionValue(id)}`);
16470
+ } catch (err) {
16471
+ screen.notify(
16472
+ `save failed: ${err instanceof Error ? err.message : String(err)}`
16473
+ );
16474
+ }
16475
+ })();
16476
+ };
16477
+ const tryHandleOptionsKey = (ev) => {
16478
+ if (!screen.isOptionsPromptActive()) {
16479
+ return false;
16480
+ }
16481
+ if (ev.type === "key") {
16482
+ switch (ev.name) {
16483
+ case "up":
16484
+ optionsSelectedIndex = Math.max(0, optionsSelectedIndex - 1);
16485
+ refreshOptionsPrompt();
16486
+ return true;
16487
+ case "down":
16488
+ optionsSelectedIndex = Math.min(
16489
+ OPTION_IDS.length - 1,
16490
+ optionsSelectedIndex + 1
16491
+ );
16492
+ refreshOptionsPrompt();
16493
+ return true;
16494
+ case "enter": {
16495
+ const id = OPTION_IDS[optionsSelectedIndex];
16496
+ if (id) {
16497
+ applyOptionToggle(id);
16498
+ }
16499
+ return true;
16500
+ }
16501
+ case "ctrl-o":
16502
+ case "escape":
16503
+ case "ctrl-c":
16504
+ screen.setOptionsPrompt(null);
16505
+ return true;
16506
+ case "ctrl-d":
16507
+ screen.setOptionsPrompt(null);
16508
+ return false;
16509
+ default:
16510
+ return true;
16511
+ }
16512
+ }
16513
+ if (ev.type === "char") {
16514
+ if (/^[1-9]$/.test(ev.ch)) {
16515
+ const idx = parseInt(ev.ch, 10) - 1;
16516
+ const id = OPTION_IDS[idx];
16517
+ if (id) {
16518
+ optionsSelectedIndex = idx;
16519
+ applyOptionToggle(id);
16520
+ }
16521
+ return true;
16522
+ }
16523
+ if (ev.ch === "s" || ev.ch === "S") {
16524
+ const id = OPTION_IDS[optionsSelectedIndex];
16525
+ if (id) {
16526
+ saveOption(id);
16527
+ }
16528
+ return true;
16529
+ }
16530
+ }
16531
+ return true;
16532
+ };
15698
16533
  const teardown = () => {
15699
16534
  teardownStarted = true;
15700
16535
  process.off("SIGINT", sigintHandler);
@@ -15893,14 +16728,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15893
16728
  const handleEffect = (effect) => {
15894
16729
  switch (effect.type) {
15895
16730
  case "send":
15896
- if (config.tui.defaultEnterAction === "amend") {
16731
+ if (viewPrefs.defaultEnterAction === "amend") {
15897
16732
  amendPrompt(effect.text, effect.attachments, effect.displayText);
15898
16733
  } else {
15899
16734
  enqueuePrompt(effect.text, effect.attachments, effect.displayText);
15900
16735
  }
15901
16736
  return;
15902
16737
  case "amend":
15903
- if (config.tui.defaultEnterAction === "amend") {
16738
+ if (viewPrefs.defaultEnterAction === "amend") {
15904
16739
  enqueuePrompt(effect.text, effect.attachments, effect.displayText);
15905
16740
  } else {
15906
16741
  amendPrompt(effect.text, effect.attachments, effect.displayText);
@@ -15993,9 +16828,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15993
16828
  case "next-live-session":
15994
16829
  void cycleLiveSession();
15995
16830
  return;
15996
- case "toggle-tools":
15997
- toolsExpanded = !toolsExpanded;
15998
- renderToolsBlock();
16831
+ case "toggle-options":
16832
+ toggleOptionsModal();
15999
16833
  return;
16000
16834
  case "toggle-thoughts":
16001
16835
  viewPrefs.showThoughts = !viewPrefs.showThoughts;
@@ -16007,6 +16841,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16007
16841
  case "toggle-mouse": {
16008
16842
  const next = !screen.isMouseEnabled();
16009
16843
  screen.setMouseEnabled(next);
16844
+ viewPrefs.mouseEnabled = next;
16010
16845
  screen.notify(
16011
16846
  next ? "mouse capture on \u2014 wheel scrolls; shift+drag to select text" : "mouse capture off \u2014 click-drag selects text; PgUp/PgDn scrolls"
16012
16847
  );
@@ -16277,9 +17112,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16277
17112
  toolsBlockStartedAt = null;
16278
17113
  toolsBlockEndedAt = null;
16279
17114
  toolsBlockStopReason = null;
16280
- toolsExpanded = false;
16281
17115
  lastEditMarkPath = null;
16282
17116
  turnHasShownProse = false;
17117
+ renderedEditDiffs.clear();
16283
17118
  screen.clearScrollback();
16284
17119
  return true;
16285
17120
  case "/demo-plan": {
@@ -16460,16 +17295,27 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16460
17295
  }
16461
17296
  };
16462
17297
  const toolStates = /* @__PURE__ */ new Map();
17298
+ const renderedEditDiffs = /* @__PURE__ */ new Map();
16463
17299
  const exitPlanStates = /* @__PURE__ */ new Map();
16464
17300
  const toolCallOrder = [];
16465
- let toolsExpanded = false;
16466
17301
  let toolsBlockStartedAt = null;
16467
17302
  let toolsBlockEndedAt = null;
16468
17303
  let toolsBlockStopReason = null;
16469
17304
  let lastPlanEvent = null;
16470
17305
  const TOOLS_COLLAPSED_LIMIT = config.tui.maxToolItems;
16471
17306
  const PLAN_VISIBLE_LIMIT2 = config.tui.maxPlanItems;
16472
- const formatOptions = { maxPlanItems: PLAN_VISIBLE_LIMIT2 };
17307
+ const planFormatOptions = () => ({
17308
+ maxPlanItems: viewPrefs.planExpanded ? Infinity : PLAN_VISIBLE_LIMIT2
17309
+ });
17310
+ const rerenderPlan = () => {
17311
+ if (lastPlanEvent === null) {
17312
+ return;
17313
+ }
17314
+ const lines = formatEvent(lastPlanEvent, planFormatOptions());
17315
+ if (lines.length > 0) {
17316
+ screen.upsertLines("plan", [{ body: "" }, ...lines]);
17317
+ }
17318
+ };
16473
17319
  let agentBuffer = "";
16474
17320
  let agentKey = null;
16475
17321
  let agentSeq = 0;
@@ -16537,7 +17383,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16537
17383
  }
16538
17384
  const total = toolCallOrder.length;
16539
17385
  const capped = TOOLS_COLLAPSED_LIMIT > 0;
16540
- const visibleIds = !capped || toolsExpanded ? toolCallOrder : toolCallOrder.slice(Math.max(0, total - TOOLS_COLLAPSED_LIMIT));
17386
+ const visibleIds = !capped || viewPrefs.toolsExpanded ? toolCallOrder : toolCallOrder.slice(Math.max(0, total - TOOLS_COLLAPSED_LIMIT));
16541
17387
  const hidden = total - visibleIds.length;
16542
17388
  const inProgress = toolsBlockEndedAt === null;
16543
17389
  const end = toolsBlockEndedAt ?? Date.now();
@@ -16556,12 +17402,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16556
17402
  const noun = total === 1 ? "tool" : "tools";
16557
17403
  const timing = stoppedReason !== null ? stoppedLabel : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
16558
17404
  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
- }
17405
+ if (inProgress && capped && hidden > 0) {
17406
+ parts.push(`${hidden} hidden`);
16565
17407
  }
16566
17408
  summary = parts.join(" \xB7 ");
16567
17409
  }
@@ -16580,7 +17422,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16580
17422
  for (const id of visibleIds) {
16581
17423
  const state = toolStates.get(id);
16582
17424
  if (state) {
16583
- lines.push(...formatToolLine2(state));
17425
+ lines.push(...formatToolLine2(state, end));
16584
17426
  }
16585
17427
  }
16586
17428
  screen.upsertLines("tools", lines);
@@ -16597,7 +17439,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16597
17439
  const state = existing ?? {
16598
17440
  initialTitle: title ?? "tool",
16599
17441
  latestTitle: title ?? "tool",
16600
- status: status ?? "pending"
17442
+ status: status ?? "pending",
17443
+ startedAt: Date.now()
16601
17444
  };
16602
17445
  if (existing && title !== void 0) {
16603
17446
  state.latestTitle = title;
@@ -16608,6 +17451,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16608
17451
  if (!existing) {
16609
17452
  state.status = status ?? "pending";
16610
17453
  }
17454
+ if (state.endedAt === void 0 && isTerminalToolStatus(state.status)) {
17455
+ state.endedAt = Date.now();
17456
+ }
16611
17457
  if (errorText !== void 0) {
16612
17458
  state.errorText = errorText;
16613
17459
  }
@@ -16627,35 +17473,60 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16627
17473
  let lastEditMarkPath = null;
16628
17474
  let turnHasShownProse = false;
16629
17475
  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);
17476
+ const key = `editdiff:${toolCallId}`;
17477
+ const lines = (() => {
17478
+ const mode = viewPrefs.showFileUpdates;
17479
+ if (mode === "none") {
17480
+ return null;
16642
17481
  }
17482
+ const state = toolStates.get(toolCallId);
17483
+ if (!state?.editDiff || state.status !== "completed") {
17484
+ return null;
17485
+ }
17486
+ if (mode === "diff") {
17487
+ const out2 = formatEditDiffBlock(state.editDiff, "diff");
17488
+ return out2.length > 0 ? out2 : null;
17489
+ }
17490
+ if (!turnHasShownProse) {
17491
+ return null;
17492
+ }
17493
+ const diff2 = state.editDiff;
17494
+ if (diff2.path && diff2.path === lastEditMarkPath && !screen.hasKey(key)) {
17495
+ return null;
17496
+ }
17497
+ const out = formatEditDiffBlock(diff2, "edit");
17498
+ if (out.length === 0) {
17499
+ return null;
17500
+ }
17501
+ if (diff2.path) {
17502
+ lastEditMarkPath = diff2.path;
17503
+ }
17504
+ return out;
17505
+ })();
17506
+ if (lines === null) {
17507
+ screen.removeKey(key);
16643
17508
  return;
16644
17509
  }
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;
17510
+ const diff = toolStates.get(toolCallId)?.editDiff;
17511
+ if (diff) {
17512
+ renderedEditDiffs.set(toolCallId, diff);
16655
17513
  }
16656
- screen.upsertLines(`editdiff:${toolCallId}`, lines);
16657
- if (diff.path) {
16658
- lastEditMarkPath = diff.path;
17514
+ screen.upsertLines(key, lines);
17515
+ };
17516
+ const reRenderAllEditDiffs = () => {
17517
+ const mode = viewPrefs.showFileUpdates;
17518
+ for (const [toolCallId, diff] of renderedEditDiffs) {
17519
+ const key = `editdiff:${toolCallId}`;
17520
+ if (mode === "none") {
17521
+ screen.removeKey(key);
17522
+ continue;
17523
+ }
17524
+ const out = formatEditDiffBlock(diff, mode);
17525
+ if (out.length === 0) {
17526
+ screen.removeKey(key);
17527
+ } else {
17528
+ screen.upsertLines(key, out);
17529
+ }
16659
17530
  }
16660
17531
  };
16661
17532
  applyRenderEvent = (event) => {
@@ -16727,7 +17598,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16727
17598
  toolStates.clear();
16728
17599
  exitPlanStates.clear();
16729
17600
  toolCallOrder.length = 0;
16730
- toolsExpanded = false;
16731
17601
  toolsBlockEndedAt = null;
16732
17602
  lastEditMarkPath = null;
16733
17603
  turnHasShownProse = false;
@@ -16789,7 +17659,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16789
17659
  closeAgentText();
16790
17660
  closeThought();
16791
17661
  lastPlanEvent = event;
16792
- const lines = formatEvent(event, formatOptions);
17662
+ const lines = formatEvent(event, planFormatOptions());
16793
17663
  if (lines.length > 0) {
16794
17664
  screen.upsertLines("plan", [{ body: "" }, ...lines]);
16795
17665
  }
@@ -16838,7 +17708,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16838
17708
  stopped: true,
16839
17709
  amended: event.amended === true
16840
17710
  },
16841
- formatOptions
17711
+ planFormatOptions()
16842
17712
  );
16843
17713
  if (lines.length > 0) {
16844
17714
  screen.upsertLines("plan", [{ body: "" }, ...lines]);
@@ -16868,7 +17738,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16868
17738
  toolsBlockStartedAt = null;
16869
17739
  toolsBlockEndedAt = null;
16870
17740
  toolsBlockStopReason = null;
16871
- toolsExpanded = false;
16872
17741
  upstreamInterruptedSeen = false;
16873
17742
  lastEditMarkPath = null;
16874
17743
  turnHasShownProse = false;
@@ -16956,7 +17825,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16956
17825
  toolsBlockStartedAt = null;
16957
17826
  toolsBlockEndedAt = null;
16958
17827
  toolsBlockStopReason = null;
16959
- toolsExpanded = false;
16960
17828
  lastEditMarkPath = null;
16961
17829
  turnHasShownProse = false;
16962
17830
  };
@@ -17114,6 +17982,10 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17114
17982
  return ctx;
17115
17983
  }
17116
17984
  if (opts.forceNew) {
17985
+ const agentStep = await ensureAgentForNew(term, target, opts);
17986
+ if (agentStep !== "ok") {
17987
+ return null;
17988
+ }
17117
17989
  return newCtx(opts, cwd, config);
17118
17990
  }
17119
17991
  if (opts.resume) {
@@ -17137,7 +18009,8 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17137
18009
  sessions,
17138
18010
  config,
17139
18011
  target,
17140
- prefs: pickerPrefs
18012
+ prefs: pickerPrefs,
18013
+ ...opts.initialPrompt !== void 0 ? { initialPrompt: opts.initialPrompt } : {}
17141
18014
  });
17142
18015
  if (choice.kind === "abort") {
17143
18016
  return null;
@@ -17146,6 +18019,13 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17146
18019
  if (choice.prompt !== void 0) {
17147
18020
  opts.initialPrompt = choice.prompt;
17148
18021
  }
18022
+ const agentStep = await ensureAgentForNew(term, target, opts);
18023
+ if (agentStep === "cancel") {
18024
+ return null;
18025
+ }
18026
+ if (agentStep === "back") {
18027
+ continue;
18028
+ }
17149
18029
  return newCtx(opts, cwd, config);
17150
18030
  }
17151
18031
  if (choice.kind === "fork") {
@@ -17305,6 +18185,38 @@ function newCtx(opts, cwd, config) {
17305
18185
  cwd
17306
18186
  };
17307
18187
  }
18188
+ async function ensureAgentForNew(term, target, opts) {
18189
+ if (opts.agentId) {
18190
+ return "ok";
18191
+ }
18192
+ if (await hasConfiguredDefaultAgent()) {
18193
+ return "ok";
18194
+ }
18195
+ let agents;
18196
+ try {
18197
+ agents = await listAgents(target);
18198
+ } catch {
18199
+ return "ok";
18200
+ }
18201
+ if (agents.length === 0) {
18202
+ return "ok";
18203
+ }
18204
+ const result = await promptForAgent(term, agents);
18205
+ if (result.kind === "cancel") {
18206
+ return "cancel";
18207
+ }
18208
+ if (result.kind === "back") {
18209
+ return "back";
18210
+ }
18211
+ opts.agentId = result.agentId;
18212
+ if (result.persist) {
18213
+ try {
18214
+ await setDefaultAgent(result.agentId);
18215
+ } catch {
18216
+ }
18217
+ }
18218
+ return "ok";
18219
+ }
17308
18220
  function debugLogUpdate(update, event) {
17309
18221
  writeDebugLine({
17310
18222
  src: "session/update",
@@ -17450,6 +18362,7 @@ var init_app = __esm({
17450
18362
  init_picker();
17451
18363
  init_import_cwd_prompt();
17452
18364
  init_import_action_prompt();
18365
+ init_agent_prompt();
17453
18366
  init_screen();
17454
18367
  init_input();
17455
18368
  init_attachments();
@@ -17475,7 +18388,7 @@ var init_app = __esm({
17475
18388
  ["Alt+N / Alt+Tab", "next live session"],
17476
18389
  ["^T", "show / hide thoughts"],
17477
18390
  ["^V", "paste image from clipboard"],
17478
- ["^O", "expand / collapse tools block"],
18391
+ ["^O", "session options (tools \xB7 plan \xB7 thoughts \xB7 diffs \xB7 mouse \xB7 enter)"],
17479
18392
  null,
17480
18393
  ["^R", "history reverse search (^S walks forward once engaged)"],
17481
18394
  ["PgUp / PgDn", "scroll scrollback"],
@@ -17516,6 +18429,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17516
18429
  "detach",
17517
18430
  "diff",
17518
18431
  "disabled",
18432
+ "drip",
17519
18433
  "fold",
17520
18434
  "follow",
17521
18435
  "force",
@@ -17538,8 +18452,10 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
17538
18452
  var KNOWN_VALUE_FLAGS = /* @__PURE__ */ new Set([
17539
18453
  "agent",
17540
18454
  "args",
18455
+ "columns",
17541
18456
  "command",
17542
18457
  "cwd",
18458
+ "drip-speed",
17543
18459
  "env",
17544
18460
  "host",
17545
18461
  "model",
@@ -20001,7 +20917,14 @@ var SessionManager = class {
20001
20917
  histories: this.histories,
20002
20918
  synopsisAgent: this.synopsisAgent,
20003
20919
  synopsisModel: this.synopsisModel,
20004
- persistTitle: (id, title) => this.persistTitle(id, title),
20920
+ persistTitle: async (id, title) => {
20921
+ const live = this.get(id);
20922
+ if (live) {
20923
+ await live.retitle(title);
20924
+ return;
20925
+ }
20926
+ await this.persistTitle(id, title);
20927
+ },
20005
20928
  persistSynopsis: (id, synopsis, through) => this.persistSynopsis(id, synopsis, through),
20006
20929
  logger: this.logger,
20007
20930
  npmRegistry: this.npmRegistry
@@ -20354,27 +21277,27 @@ var SessionManager = class {
20354
21277
  return false;
20355
21278
  }
20356
21279
  }
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.
21280
+ // When the last client detaches from a session that was never promoted
21281
+ // to interactive, close it so its agent process doesn't linger until the
21282
+ // (default 1h) idle timeout fires. This covers both `hydra cat` runs
21283
+ // (born interactive:undefined with originatingClient hydra-acp-cat, every
21284
+ // prompt ancillary) and any other client that opened a session but never
21285
+ // sent a real, non-ancillary prompt. Promotion to interactive is
21286
+ // synchronous on the first real prompt (Session.prompt sets _interactive
21287
+ // = true before enqueuing), so a session that ever saw a genuine turn
21288
+ // resolves to true here and is left running. The cold record is kept, so
21289
+ // re-attaching resurrects via the reseed path.
21290
+ //
21291
+ // Note: this only fires from the explicit session/detach handler — raw WS
21292
+ // close deliberately does NOT reap (see acp-ws.ts), so an abrupt
21293
+ // disconnect of a never-prompted session falls through to the idle
21294
+ // timeout rather than being torn down.
20365
21295
  async reapIfOrphanedNonInteractive(sessionId) {
20366
21296
  const session = this.sessions.get(sessionId);
20367
21297
  if (!session || session.attachedCount > 0) {
20368
21298
  return;
20369
21299
  }
20370
- const interactive = effectiveInteractive(
20371
- {
20372
- interactive: session.interactive,
20373
- ...session.originatingClient ? { originatingClient: session.originatingClient } : {}
20374
- },
20375
- true
20376
- );
20377
- if (interactive !== false) {
21300
+ if (session.interactive === true) {
20378
21301
  return;
20379
21302
  }
20380
21303
  this.logger?.info(
@@ -20872,7 +21795,8 @@ var SessionManager = class {
20872
21795
  updatedAt: used,
20873
21796
  attachedClients: session.attachedCount,
20874
21797
  status: "live",
20875
- busy: session.turnStartedAt !== void 0
21798
+ busy: session.turnStartedAt !== void 0,
21799
+ awaitingInput: session.awaitingInput
20876
21800
  });
20877
21801
  }
20878
21802
  const records = await this.store.list().catch(() => []);
@@ -20910,7 +21834,8 @@ var SessionManager = class {
20910
21834
  updatedAt: used,
20911
21835
  attachedClients: 0,
20912
21836
  status: "cold",
20913
- busy: false
21837
+ busy: false,
21838
+ awaitingInput: false
20914
21839
  });
20915
21840
  }
20916
21841
  entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
@@ -24814,10 +25739,11 @@ function registerAcpWsEndpoint(app, deps) {
24814
25739
  params.clientInfo,
24815
25740
  params.clientId
24816
25741
  );
25742
+ const drip = params.replayMode === "drip";
24817
25743
  const { entries: replay, appliedPolicy } = await session.attach(
24818
25744
  client,
24819
25745
  params.historyPolicy,
24820
- { afterMessageId: params.afterMessageId }
25746
+ { afterMessageId: params.afterMessageId, raw: drip }
24821
25747
  );
24822
25748
  state.attached.set(session.sessionId, {
24823
25749
  sessionId: session.sessionId,
@@ -24825,10 +25751,35 @@ function registerAcpWsEndpoint(app, deps) {
24825
25751
  readonly
24826
25752
  });
24827
25753
  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}`
25754
+ `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
25755
  );
24830
- for (const note of replay) {
24831
- await connection.notify(note.method, note.params);
25756
+ if (drip) {
25757
+ const speed = params.dripSpeed && params.dripSpeed > 0 ? params.dripSpeed : 1;
25758
+ const MAX_GAP_MS = 750;
25759
+ void (async () => {
25760
+ let prev = null;
25761
+ for (const note of replay) {
25762
+ const at = typeof note.recordedAt === "number" ? note.recordedAt : null;
25763
+ if (prev !== null && at !== null) {
25764
+ const gap = Math.min(MAX_GAP_MS, Math.max(0, (at - prev) / speed));
25765
+ if (gap > 0) {
25766
+ await new Promise((r) => setTimeout(r, gap));
25767
+ }
25768
+ }
25769
+ if (at !== null) {
25770
+ prev = at;
25771
+ }
25772
+ try {
25773
+ await connection.notify(note.method, note.params);
25774
+ } catch {
25775
+ return;
25776
+ }
25777
+ }
25778
+ })();
25779
+ } else {
25780
+ for (const note of replay) {
25781
+ await connection.notify(note.method, note.params);
25782
+ }
24832
25783
  }
24833
25784
  session.replayPendingPermissions(client);
24834
25785
  const modesPayload = buildModesPayload(session);
@@ -26689,12 +27640,15 @@ async function runSessionsList(opts = {}) {
26689
27640
  }
26690
27641
  const now = Date.now();
26691
27642
  const rows = visible.map((s) => toRow(s, now));
26692
- const widths = computeWidths(rows);
27643
+ const formatOpts = {
27644
+ columns: opts.columns ?? config.tui.sessionColumns ?? DEFAULT_COLUMNS,
27645
+ cwdMaxWidth: config.tui.cwdColumnMaxWidth
27646
+ };
27647
+ const widths = computeWidths(rows, formatOpts);
26693
27648
  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");
27649
+ process.stdout.write(formatRow(HEADER, widths, maxWidth, formatOpts) + "\n");
26696
27650
  for (const r of rows) {
26697
- process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
27651
+ process.stdout.write(formatRow(r, widths, maxWidth, formatOpts) + "\n");
26698
27652
  }
26699
27653
  if (truncated > 0) {
26700
27654
  process.stdout.write(
@@ -26958,10 +27912,14 @@ function printBundleInfo(raw, cwdColumnMaxWidth) {
26958
27912
  }
26959
27913
  const summary = bundleToSummary(parsed);
26960
27914
  const row = toRow(summary);
26961
- const widths = computeWidths([row]);
27915
+ const formatOpts = {
27916
+ columns: ALL_COLUMNS,
27917
+ cwdMaxWidth: cwdColumnMaxWidth
27918
+ };
27919
+ const widths = computeWidths([row], formatOpts);
26962
27920
  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");
27921
+ process.stdout.write(formatRow(HEADER, widths, maxWidth, formatOpts) + "\n");
27922
+ process.stdout.write(formatRow(row, widths, maxWidth, formatOpts) + "\n");
26965
27923
  const originUpstream = parsed.session.upstreamSessionId ?? "-";
26966
27924
  process.stdout.write(
26967
27925
  `
@@ -27537,6 +28495,7 @@ function aggregate(bundle, status) {
27537
28495
  const durationMs = Number.isFinite(createdMs) && Number.isFinite(updatedMs) ? updatedMs - createdMs : null;
27538
28496
  return {
27539
28497
  sessionId: r.sessionId,
28498
+ ...r.upstreamSessionId !== void 0 ? { upstreamSessionId: r.upstreamSessionId } : {},
27540
28499
  ...r.title !== void 0 ? { title: r.title } : {},
27541
28500
  cwd: r.cwd,
27542
28501
  agentId: r.agentId,
@@ -27564,6 +28523,9 @@ function formatSummary(d, verbose) {
27564
28523
  const lines = [];
27565
28524
  const pad = (label) => label.padEnd(14);
27566
28525
  lines.push(`${pad("Session:")}${d.sessionId}`);
28526
+ if (d.upstreamSessionId) {
28527
+ lines.push(`${pad("Upstream:")}${d.upstreamSessionId}`);
28528
+ }
27567
28529
  if (d.title) {
27568
28530
  lines.push(`${pad("Title:")}${d.title}`);
27569
28531
  }
@@ -27688,6 +28650,9 @@ function formatDuration(ms) {
27688
28650
  return parts.join(" ");
27689
28651
  }
27690
28652
 
28653
+ // src/cli.ts
28654
+ init_session_row();
28655
+
27691
28656
  // src/cli/commands/extensions.ts
27692
28657
  init_config();
27693
28658
  init_service_token();
@@ -28704,6 +29669,32 @@ function formatAge(ms) {
28704
29669
  const day = Math.floor(hour / 24);
28705
29670
  return `${day} day${day === 1 ? "" : "s"}`;
28706
29671
  }
29672
+ async function assertKnownAgent(agentId) {
29673
+ const config = await loadConfig();
29674
+ const serviceToken = await loadServiceToken();
29675
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
29676
+ let known;
29677
+ try {
29678
+ const r = await fetch(`${baseUrl}/v1/agents`, {
29679
+ headers: { Authorization: `Bearer ${serviceToken}` }
29680
+ });
29681
+ if (!r.ok) {
29682
+ return;
29683
+ }
29684
+ const body = await r.json();
29685
+ known = body.agents.map((a) => a.id);
29686
+ } catch {
29687
+ return;
29688
+ }
29689
+ if (known.includes(agentId)) {
29690
+ return;
29691
+ }
29692
+ process.stderr.write(
29693
+ `hydra-acp: unknown agent '${agentId}'. Run 'hydra-acp agent list' to see available agents.
29694
+ `
29695
+ );
29696
+ process.exit(2);
29697
+ }
28707
29698
  async function runAgentsInstall(agentId) {
28708
29699
  if (!agentId) {
28709
29700
  process.stderr.write("Usage: hydra-acp agent install <agent-id>\n");
@@ -28875,16 +29866,7 @@ async function runAgentsSet(agentId, modelId) {
28875
29866
  process.exit(1);
28876
29867
  return;
28877
29868
  }
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
- }
29869
+ await setDefaultAgent(agentId, modelId);
28888
29870
  const disk = readAgentDefaults(await readRawConfig3());
28889
29871
  if (modelId !== void 0 && agentId !== disk.agent) {
28890
29872
  process.stdout.write(
@@ -28936,13 +29918,6 @@ async function readRawConfig3() {
28936
29918
  const raw = await fsp12.readFile(paths.config(), "utf8");
28937
29919
  return JSON.parse(raw);
28938
29920
  }
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
29921
  async function runAgentsRefresh() {
28947
29922
  const config = await loadConfig();
28948
29923
  const serviceToken = await loadServiceToken();
@@ -30458,6 +31433,9 @@ async function main() {
30458
31433
  if (streamBufferBytes !== void 0) {
30459
31434
  catOpts.streamBufferBytes = streamBufferBytes;
30460
31435
  }
31436
+ if (agentIdFromFlag !== void 0) {
31437
+ await assertKnownAgent(agentIdFromFlag);
31438
+ }
30461
31439
  suppressUpdateNotice = true;
30462
31440
  await runCat(catOpts);
30463
31441
  return;
@@ -30498,11 +31476,24 @@ async function main() {
30498
31476
  case "sessions": {
30499
31477
  const sub = positional[1];
30500
31478
  if (sub === void 0 || sub === "list") {
31479
+ const columnsRaw = resolveOption(flags, "columns");
31480
+ let columns;
31481
+ if (columnsRaw !== void 0) {
31482
+ try {
31483
+ columns = parseColumns(columnsRaw);
31484
+ } catch (err) {
31485
+ process.stderr.write(`${err.message}
31486
+ `);
31487
+ process.exit(2);
31488
+ return;
31489
+ }
31490
+ }
30501
31491
  await runSessionsList({
30502
31492
  all: flags.all === true,
30503
31493
  json: flags.json === true,
30504
31494
  host: typeof flags.host === "string" ? flags.host : void 0,
30505
- includeNonInteractive: flags["include-non-interactive"] === true
31495
+ includeNonInteractive: flags["include-non-interactive"] === true,
31496
+ columns
30506
31497
  });
30507
31498
  return;
30508
31499
  }
@@ -30737,6 +31728,9 @@ async function dispatchTui(flags, base) {
30737
31728
  );
30738
31729
  process.exit(2);
30739
31730
  }
31731
+ if (base.agentId !== void 0) {
31732
+ await assertKnownAgent(base.agentId);
31733
+ }
30740
31734
  setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
30741
31735
  const { runTui } = await Promise.resolve().then(() => (init_tui(), tui_exports));
30742
31736
  const tuiOpts = { resume, forceNew, readonly };
@@ -30761,6 +31755,19 @@ async function dispatchTui(flags, base) {
30761
31755
  if (base.dangerouslySkipPermissions === true) {
30762
31756
  tuiOpts.dangerouslySkipPermissions = true;
30763
31757
  }
31758
+ if (flags.drip === true) {
31759
+ if (base.sessionId === void 0) {
31760
+ process.stderr.write(
31761
+ "hydra-acp: --drip requires a session id. Pass --session <id-or-url> --drip.\n"
31762
+ );
31763
+ process.exit(2);
31764
+ }
31765
+ tuiOpts.drip = true;
31766
+ const dripSpeed = parseNumericFlag(flags, "drip-speed");
31767
+ if (dripSpeed !== void 0 && dripSpeed > 0) {
31768
+ tuiOpts.dripSpeed = dripSpeed;
31769
+ }
31770
+ }
30764
31771
  await runTui(tuiOpts);
30765
31772
  }
30766
31773
  function parseNumericFlag(flags, name) {
@@ -30875,10 +31882,11 @@ function printHelp() {
30875
31882
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
30876
31883
  " hydra-acp daemon stop|restart",
30877
31884
  " 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]",
31885
+ " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-non-interactive] [--columns=<list>]",
30879
31886
  " List sessions (live + 20 most-recent cold; --all lifts the cold cap AND surfaces non-interactive sessions; --json emits JSON for scripts).",
30880
31887
  " --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
31888
  " --include-non-interactive surfaces ancillary (e.g. `hydra cat`) or never-prompted sessions while keeping the cold cap (a narrower --all).",
31889
+ " --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
31890
  " hydra-acp session info <id> [--verbose] [--json] [--diff] [--fold] [--no-color] [--no-pager]",
30883
31891
  " 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
31892
  " hydra-acp session diff <id> [--json] [--no-color] [--no-pager] [--fold]",