@hydra-acp/cli 0.1.11 → 0.1.13

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
@@ -264,7 +264,13 @@ var init_config = __esm({
264
264
  // Width cap on the cwd column in the `sessions list` output and the
265
265
  // TUI picker. Set higher if you keep deeply-nested working directories
266
266
  // and want them visible; the elastic title column shrinks to make room.
267
- cwdColumnMaxWidth: z.number().int().positive().default(24)
267
+ cwdColumnMaxWidth: z.number().int().positive().default(24),
268
+ // When true (default), emit OSC 9;4 progress-bar control codes so the
269
+ // host terminal can show an indeterminate busy indicator (taskbar pulse
270
+ // on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
271
+ // running. Set false if your terminal renders this obnoxiously or you
272
+ // just don't want it.
273
+ progressIndicator: z.boolean().default(true)
268
274
  });
269
275
  ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
270
276
  ExtensionBody = z.object({
@@ -302,7 +308,8 @@ var init_config = __esm({
302
308
  maxScrollbackLines: 1e4,
303
309
  mouse: true,
304
310
  logMaxBytes: 5 * 1024 * 1024,
305
- cwdColumnMaxWidth: 24
311
+ cwdColumnMaxWidth: 24,
312
+ progressIndicator: true
306
313
  })
307
314
  });
308
315
  HydraConfigReadOnly = HydraConfig.extend({
@@ -483,6 +490,11 @@ var init_types = __esm({
483
490
  // Last-known usage snapshot so list views can show per-session cost
484
491
  // (and tokens, in callers that care) without resurrecting cold sessions.
485
492
  currentUsage: SessionListUsage.optional(),
493
+ // Origin host (and origin upstream id) for imported sessions. Picker
494
+ // uses the host to fill in the UPSTREAM cell pre-first-attach;
495
+ // future "connect back to origin" callers would dial both.
496
+ importedFromMachine: z3.string().optional(),
497
+ importedFromUpstreamSessionId: z3.string().optional(),
486
498
  updatedAt: z3.string(),
487
499
  attachedClients: z3.number().int().nonnegative(),
488
500
  status: z3.enum(["live", "cold"]).default("live"),
@@ -737,6 +749,11 @@ var init_hydra_commands = __esm({
737
749
  name: "hydra agent",
738
750
  argsHint: "<agent>",
739
751
  description: "Swap the agent backing this session, preserving context"
752
+ },
753
+ {
754
+ verb: "kill",
755
+ name: "hydra kill",
756
+ description: "Close this session (kills the agent; record is kept so it can be resumed later)"
740
757
  }
741
758
  ];
742
759
  VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
@@ -1006,6 +1023,7 @@ var init_session = __esm({
1006
1023
  // and noisy state churn keep a quiet session alive forever.
1007
1024
  lastRecordedAt;
1008
1025
  spawnReplacementAgent;
1026
+ logger;
1009
1027
  agentChangeHandlers = [];
1010
1028
  // Last available_commands_update we observed from the agent. Stored
1011
1029
  // so we can re-broadcast a merged (hydra ∪ agent) list whenever
@@ -1037,6 +1055,7 @@ var init_session = __esm({
1037
1055
  }
1038
1056
  this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
1039
1057
  this.spawnReplacementAgent = init.spawnReplacementAgent;
1058
+ this.logger = init.logger;
1040
1059
  if (init.firstPromptSeeded) {
1041
1060
  this.firstPromptSeeded = true;
1042
1061
  }
@@ -1366,6 +1385,9 @@ var init_session = __esm({
1366
1385
  if (this.closed) {
1367
1386
  return;
1368
1387
  }
1388
+ this.logger?.info(
1389
+ `session ${this.sessionId} closing deleteRecord=${opts.deleteRecord ?? false} regenTitle=${opts.regenTitle ?? false}`
1390
+ );
1369
1391
  this.cancelIdleTimer();
1370
1392
  if (opts.regenTitle && this.firstPromptSeeded) {
1371
1393
  const timeoutMs = opts.regenTitleTimeoutMs ?? 5e3;
@@ -1618,6 +1640,8 @@ var init_session = __esm({
1618
1640
  return this.runTitleCommand(arg);
1619
1641
  case "agent":
1620
1642
  return this.runAgentCommand(arg);
1643
+ case "kill":
1644
+ return this.runKillCommand();
1621
1645
  default: {
1622
1646
  const err = new Error(
1623
1647
  `no dispatcher for /hydra verb ${verb}`
@@ -1729,6 +1753,17 @@ var init_session = __esm({
1729
1753
  return { stopReason: "end_turn" };
1730
1754
  });
1731
1755
  }
1756
+ // Close this session in-place. Bypasses enqueuePrompt deliberately so a
1757
+ // mid-turn /hydra kill takes effect immediately — agent.kill() will tear
1758
+ // down any in-flight request as a side effect. The record is kept
1759
+ // (deleteRecord:false) so the session goes cold and can be resurrected.
1760
+ // Returns end_turn so the prompt() caller's response resolves normally,
1761
+ // but every attached client has already received hydra-acp/session_closed
1762
+ // by the time this returns.
1763
+ async runKillCommand() {
1764
+ await this.close({ deleteRecord: false });
1765
+ return { stopReason: "end_turn" };
1766
+ }
1732
1767
  // Walk the persisted history and produce a labeled transcript suitable
1733
1768
  // for handing to a fresh agent. Includes user prompts, agent replies,
1734
1769
  // and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
@@ -1920,6 +1955,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
1920
1955
  return;
1921
1956
  }
1922
1957
  const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
1958
+ const idleSec = Math.round(idle / 1e3);
1959
+ this.logger?.info(
1960
+ `session ${this.sessionId} idle timeout fired after ${idleSec}s (window=${Math.round(this.idleTimeoutMs / 1e3)}s) \u2014 closing`
1961
+ );
1923
1962
  void this.close(opts).catch(() => void 0);
1924
1963
  }
1925
1964
  cancelIdleTimer() {
@@ -2037,7 +2076,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2037
2076
  }
2038
2077
  async enqueuePrompt(task) {
2039
2078
  return new Promise((resolve5, reject) => {
2040
- const run2 = async () => {
2079
+ const run3 = async () => {
2041
2080
  try {
2042
2081
  const result = await task();
2043
2082
  resolve5(result);
@@ -2045,7 +2084,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2045
2084
  reject(err);
2046
2085
  }
2047
2086
  };
2048
- this.promptQueue.push(run2);
2087
+ this.promptQueue.push(run3);
2049
2088
  void this.drainQueue();
2050
2089
  });
2051
2090
  }
@@ -2096,6 +2135,8 @@ function recordFromMemorySession(args) {
2096
2135
  lineageId: args.lineageId,
2097
2136
  upstreamSessionId: args.upstreamSessionId,
2098
2137
  importedFromSessionId: args.importedFromSessionId,
2138
+ importedFromUpstreamSessionId: args.importedFromUpstreamSessionId,
2139
+ importedFromMachine: args.importedFromMachine,
2099
2140
  agentId: args.agentId,
2100
2141
  cwd: args.cwd,
2101
2142
  title: args.title,
@@ -2143,6 +2184,16 @@ var init_session_store = __esm({
2143
2184
  // origin's local id at export time, kept for debuggability and as a
2144
2185
  // breadcrumb in `sessions list` (informational, not used for routing).
2145
2186
  importedFromSessionId: z4.string().optional(),
2187
+ // Origin's agent-side session id at export time. Carried as a
2188
+ // breadcrumb and as the handle a future "connect back to origin"
2189
+ // feature would dial. Absent when the origin record had no upstream
2190
+ // bound (re-export of an imported, not-yet-attached session).
2191
+ importedFromUpstreamSessionId: z4.string().optional(),
2192
+ // Hostname of the machine that exported the bundle we imported
2193
+ // (i.e. the most recent hop, not necessarily the true multi-hop
2194
+ // origin). Surfaced in the picker so imported rows don't look like
2195
+ // they materialized from nowhere.
2196
+ importedFromMachine: z4.string().optional(),
2146
2197
  agentId: z4.string(),
2147
2198
  cwd: z4.string(),
2148
2199
  title: z4.string().optional(),
@@ -2358,6 +2409,7 @@ function encodeBundle(params) {
2358
2409
  session: {
2359
2410
  sessionId: params.record.sessionId,
2360
2411
  lineageId: params.record.lineageId,
2412
+ ...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
2361
2413
  agentId: params.record.agentId,
2362
2414
  cwd: params.record.cwd,
2363
2415
  ...params.record.title !== void 0 ? { title: params.record.title } : {},
@@ -2395,6 +2447,12 @@ var init_bundle = __esm({
2395
2447
  // Required on bundles — the export path backfills if the source
2396
2448
  // record was written before lineageId existed.
2397
2449
  lineageId: z5.string(),
2450
+ // The exporter's agent-side session id at export time. Carried so
2451
+ // importers can persist it as a breadcrumb (and, eventually, as the
2452
+ // handle a "connect back to origin" feature would need). Omitted on
2453
+ // bundles whose source record never bound to an agent (e.g. a
2454
+ // re-export of an imported, not-yet-attached session).
2455
+ upstreamSessionId: z5.string().optional(),
2398
2456
  agentId: z5.string(),
2399
2457
  cwd: z5.string(),
2400
2458
  title: z5.string().optional(),
@@ -2608,7 +2666,7 @@ var init_agent_display = __esm({
2608
2666
  function toRow(s, now = Date.now()) {
2609
2667
  return {
2610
2668
  session: stripHydraSessionPrefix(s.sessionId),
2611
- upstream: s.upstreamSessionId ?? "-",
2669
+ upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
2612
2670
  state: formatState(s.status, s.attachedClients),
2613
2671
  agent: formatAgentCell(s.agentId, s.currentUsage),
2614
2672
  age: formatRelativeAge(s.updatedAt, now),
@@ -2616,6 +2674,15 @@ function toRow(s, now = Date.now()) {
2616
2674
  cwd: shortenHomePath(s.cwd)
2617
2675
  };
2618
2676
  }
2677
+ function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
2678
+ if (upstreamSessionId && upstreamSessionId.length > 0) {
2679
+ return upstreamSessionId;
2680
+ }
2681
+ if (importedFromMachine && importedFromMachine.length > 0) {
2682
+ return `\u2190 ${importedFromMachine}`;
2683
+ }
2684
+ return "-";
2685
+ }
2619
2686
  function formatState(status, clients) {
2620
2687
  if (status === "cold") {
2621
2688
  return "COLD";
@@ -2955,11 +3022,11 @@ async function runSessionsImport(file, opts = {}) {
2955
3022
  function bundleToSummary(parsed) {
2956
3023
  return {
2957
3024
  sessionId: parsed.session.sessionId,
2958
- upstreamSessionId: "-",
2959
3025
  cwd: parsed.session.cwd,
2960
3026
  agentId: parsed.session.agentId,
2961
3027
  currentUsage: parsed.session.currentUsage,
2962
3028
  title: parsed.session.title,
3029
+ importedFromMachine: parsed.exportedFrom.machine,
2963
3030
  attachedClients: 0,
2964
3031
  updatedAt: parsed.session.updatedAt,
2965
3032
  status: "cold"
@@ -2980,10 +3047,13 @@ function printBundleInfo(raw, cwdColumnMaxWidth) {
2980
3047
  const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
2981
3048
  process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
2982
3049
  process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
3050
+ const originUpstream = parsed.session.upstreamSessionId ?? "-";
2983
3051
  process.stdout.write(
2984
3052
  `
2985
3053
  lineage: ${parsed.session.lineageId}
2986
3054
  exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
3055
+ origin session: ${parsed.session.sessionId}
3056
+ origin upstream: ${originUpstream}
2987
3057
  history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
2988
3058
  ` : "\n")
2989
3059
  );
@@ -3284,7 +3354,9 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
3284
3354
  agentId: s.agentId,
3285
3355
  currentModel: s.currentModel,
3286
3356
  currentUsage: s.currentUsage,
3287
- title: s.title
3357
+ title: s.title,
3358
+ importedFromMachine: s.importedFromMachine,
3359
+ importedFromUpstreamSessionId: s.importedFromUpstreamSessionId
3288
3360
  }));
3289
3361
  }
3290
3362
  async function killSession(config, id, fetchImpl = fetch) {
@@ -3356,8 +3428,15 @@ async function pickSession(term, opts) {
3356
3428
  let total = 1 + visible.length;
3357
3429
  let selectedIdx = 0;
3358
3430
  let scrollOffset = 0;
3431
+ if (opts.currentSessionId !== void 0) {
3432
+ const idx = visible.findIndex((s) => s.sessionId === opts.currentSessionId);
3433
+ if (idx >= 0) {
3434
+ selectedIdx = idx + 1;
3435
+ }
3436
+ }
3359
3437
  let searchActive = false;
3360
3438
  let searchTerm = "";
3439
+ let cwdOnly = false;
3361
3440
  let mode = "normal";
3362
3441
  let pendingAction = null;
3363
3442
  let transientStatus = null;
@@ -3386,10 +3465,14 @@ async function pickSession(term, opts) {
3386
3465
  computeLayout();
3387
3466
  };
3388
3467
  const applyFilter = () => {
3468
+ let base = allSessions;
3469
+ if (cwdOnly) {
3470
+ base = base.filter((s) => s.cwd === opts.cwd);
3471
+ }
3389
3472
  if (searchActive && searchTerm.length > 0) {
3390
- visible = allSessions.filter((s) => matchesSearch(s, searchTerm));
3473
+ visible = base.filter((s) => matchesSearch(s, searchTerm));
3391
3474
  } else {
3392
- visible = allSessions;
3475
+ visible = base;
3393
3476
  }
3394
3477
  rebuildRows();
3395
3478
  if (searchActive) {
@@ -3434,16 +3517,19 @@ async function pickSession(term, opts) {
3434
3517
  const formatIndicator = () => {
3435
3518
  const above = scrollOffset;
3436
3519
  const below = Math.max(0, visible.length - scrollOffset - viewportSize);
3437
- if (above === 0 && below === 0) {
3438
- return "";
3439
- }
3440
3520
  const parts = [];
3521
+ if (cwdOnly) {
3522
+ parts.push("cwd-only");
3523
+ }
3441
3524
  if (above > 0) {
3442
3525
  parts.push(`\u2191 ${above} above`);
3443
3526
  }
3444
3527
  if (below > 0) {
3445
3528
  parts.push(`\u2193 ${below} below`);
3446
3529
  }
3530
+ if (parts.length === 0) {
3531
+ return "";
3532
+ }
3447
3533
  return ` ${parts.join(" \xB7 ")}`;
3448
3534
  };
3449
3535
  const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
@@ -3667,6 +3753,38 @@ async function pickSession(term, opts) {
3667
3753
  renderFromScratch();
3668
3754
  return;
3669
3755
  }
3756
+ if (name === "n" || name === "N") {
3757
+ move(1);
3758
+ return;
3759
+ }
3760
+ if (name === "p" || name === "P") {
3761
+ move(-1);
3762
+ return;
3763
+ }
3764
+ if (name === "c" || name === "C") {
3765
+ cleanup();
3766
+ resolve5({ kind: "new" });
3767
+ return;
3768
+ }
3769
+ if (name === "q" || name === "Q") {
3770
+ cleanup();
3771
+ resolve5({ kind: "abort" });
3772
+ return;
3773
+ }
3774
+ if (name === "o" || name === "O") {
3775
+ const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
3776
+ cwdOnly = !cwdOnly;
3777
+ applyFilter();
3778
+ if (keepId !== void 0) {
3779
+ const idx = visible.findIndex((s) => s.sessionId === keepId);
3780
+ if (idx >= 0) {
3781
+ selectedIdx = idx + 1;
3782
+ adjustScroll();
3783
+ }
3784
+ }
3785
+ renderFromScratch();
3786
+ return;
3787
+ }
3670
3788
  if (name === "r" || name === "R") {
3671
3789
  const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
3672
3790
  void refresh(currentId);
@@ -3805,6 +3923,90 @@ var init_picker = __esm({
3805
3923
  }
3806
3924
  });
3807
3925
 
3926
+ // src/tui/attachments.ts
3927
+ import path9 from "path";
3928
+ function mimeFromExtension(p) {
3929
+ return EXTENSION_TO_MIME[path9.extname(p).toLowerCase()] ?? null;
3930
+ }
3931
+ function isSupportedImagePath(p) {
3932
+ return mimeFromExtension(p) !== null;
3933
+ }
3934
+ function formatSize(bytes) {
3935
+ if (bytes >= 1024 * 1024) {
3936
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
3937
+ }
3938
+ if (bytes >= 1024) {
3939
+ return `${(bytes / 1024).toFixed(0)}KB`;
3940
+ }
3941
+ return `${bytes}B`;
3942
+ }
3943
+ function parseImageDropPaste(raw) {
3944
+ const text = raw.trim();
3945
+ if (text.length === 0) {
3946
+ return null;
3947
+ }
3948
+ const tokens = [];
3949
+ let i = 0;
3950
+ while (i < text.length) {
3951
+ while (i < text.length && /\s/.test(text[i] ?? "")) {
3952
+ i++;
3953
+ }
3954
+ if (i >= text.length) {
3955
+ break;
3956
+ }
3957
+ const ch = text[i];
3958
+ let token = "";
3959
+ if (ch === "'" || ch === '"') {
3960
+ const quote = ch;
3961
+ i++;
3962
+ while (i < text.length && text[i] !== quote) {
3963
+ token += text[i];
3964
+ i++;
3965
+ }
3966
+ if (i >= text.length) {
3967
+ return null;
3968
+ }
3969
+ i++;
3970
+ } else {
3971
+ while (i < text.length && !/\s/.test(text[i] ?? "")) {
3972
+ if (text[i] === "\\" && i + 1 < text.length) {
3973
+ token += text[i + 1];
3974
+ i += 2;
3975
+ } else {
3976
+ token += text[i];
3977
+ i++;
3978
+ }
3979
+ }
3980
+ }
3981
+ let normalized = token;
3982
+ if (normalized.startsWith("file://")) {
3983
+ normalized = decodeURI(normalized.slice("file://".length));
3984
+ }
3985
+ if (!normalized.startsWith("/")) {
3986
+ return null;
3987
+ }
3988
+ if (!isSupportedImagePath(normalized)) {
3989
+ return null;
3990
+ }
3991
+ tokens.push(normalized);
3992
+ }
3993
+ return tokens.length > 0 ? tokens : null;
3994
+ }
3995
+ var MAX_ATTACHMENT_BYTES, EXTENSION_TO_MIME;
3996
+ var init_attachments = __esm({
3997
+ "src/tui/attachments.ts"() {
3998
+ "use strict";
3999
+ MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
4000
+ EXTENSION_TO_MIME = {
4001
+ ".png": "image/png",
4002
+ ".jpg": "image/jpeg",
4003
+ ".jpeg": "image/jpeg",
4004
+ ".gif": "image/gif",
4005
+ ".webp": "image/webp"
4006
+ };
4007
+ }
4008
+ });
4009
+
3808
4010
  // src/tui/screen.ts
3809
4011
  import stringWidth from "string-width";
3810
4012
  import wrapAnsi from "wrap-ansi";
@@ -3813,7 +4015,8 @@ function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null
3813
4015
  if (!line) {
3814
4016
  return `${zone}|${width}|empty|${highlight2 ?? ""}|${active}`;
3815
4017
  }
3816
- return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}|${highlight2 ?? ""}|${active}`;
4018
+ const img = line.iterm2Image ? `i${line.iterm2Image.heightCells}:${line.iterm2Image.data.length}` : "";
4019
+ return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}|${highlight2 ?? ""}|${active}|${img}`;
3817
4020
  }
3818
4021
  function computePromptVisualRows(buffer, room) {
3819
4022
  const rows = [];
@@ -4061,6 +4264,14 @@ function* segmentForWidth(text) {
4061
4264
  i = runEnd;
4062
4265
  }
4063
4266
  }
4267
+ function buildIterm2ImageEscape(base64, heightCells, insideTmux) {
4268
+ const inner = `\x1B]1337;File=inline=1;height=${heightCells};preserveAspectRatio=1:${base64}\x07`;
4269
+ if (!insideTmux) {
4270
+ return inner;
4271
+ }
4272
+ const doubled = inner.replace(/\x1b/g, "\x1B\x1B");
4273
+ return `\x1BPtmux;${doubled}\x1B\\`;
4274
+ }
4064
4275
  function wrap(text, width, opts = {}) {
4065
4276
  if (width <= 0) {
4066
4277
  return [text];
@@ -4310,6 +4521,8 @@ function mapKeyName(name) {
4310
4521
  return "ctrl-s";
4311
4522
  case "CTRL_U":
4312
4523
  return "ctrl-u";
4524
+ case "CTRL_V":
4525
+ return "ctrl-v";
4313
4526
  case "CTRL_W":
4314
4527
  return "ctrl-w";
4315
4528
  case "CTRL_Y":
@@ -4320,13 +4533,14 @@ function mapKeyName(name) {
4320
4533
  return null;
4321
4534
  }
4322
4535
  }
4323
- var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
4536
+ var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
4324
4537
  var init_screen = __esm({
4325
4538
  "src/tui/screen.ts"() {
4326
4539
  "use strict";
4327
4540
  init_agent_display();
4328
4541
  init_paths();
4329
4542
  init_session();
4543
+ init_attachments();
4330
4544
  HEADER_ROWS = 2;
4331
4545
  BANNER_ROWS = 1;
4332
4546
  SEPARATOR_ROWS = 1;
@@ -4334,6 +4548,7 @@ var init_screen = __esm({
4334
4548
  MAX_QUEUED_ROWS = 5;
4335
4549
  MAX_PERMISSION_ROWS = 12;
4336
4550
  MAX_COMPLETION_ROWS = 6;
4551
+ MAX_CHIP_ROWS = 4;
4337
4552
  CONFIRM_PROMPT_ROWS = 2;
4338
4553
  DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
4339
4554
  DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
@@ -4353,6 +4568,12 @@ var init_screen = __esm({
4353
4568
  lastPromptRows = 0;
4354
4569
  queuedTexts = [];
4355
4570
  lastQueueEditingIndex = -1;
4571
+ // Attachments on the current draft, pushed by the app whenever the
4572
+ // dispatcher mutates. The chip zone (drawAttachmentChipZone) renders
4573
+ // one row per attachment plus, in iTerm2-capable terminals, an inline
4574
+ // thumbnail. Capped at MAX_CHIP_ROWS in the visible zone — additional
4575
+ // chips collapse into an overflow row.
4576
+ attachments = [];
4356
4577
  repaintPaused = 0;
4357
4578
  repaintPending = false;
4358
4579
  lastRepaintAt = 0;
@@ -4415,7 +4636,7 @@ var init_screen = __esm({
4415
4636
  banner = {
4416
4637
  status: "ready",
4417
4638
  planMode: false,
4418
- hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
4639
+ hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
4419
4640
  queued: 0
4420
4641
  };
4421
4642
  header = { agent: "?", cwd: "?", sessionId: "?" };
@@ -4436,6 +4657,12 @@ var init_screen = __esm({
4436
4657
  pasteBuffer = "";
4437
4658
  rawStdinHandler;
4438
4659
  mouseEnabled;
4660
+ progressIndicatorEnabled;
4661
+ // Last OSC 9;4 state we wrote (3 = indeterminate, 0 = remove). Used to
4662
+ // suppress redundant writes when setBanner runs but `status` didn't
4663
+ // actually change, and to re-emit on start() if a picker round-trip
4664
+ // cleared the host terminal's indicator.
4665
+ lastProgressState = 0;
4439
4666
  constructor(opts) {
4440
4667
  this.term = opts.term;
4441
4668
  this.dispatcher = opts.dispatcher;
@@ -4443,6 +4670,7 @@ var init_screen = __esm({
4443
4670
  this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
4444
4671
  this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
4445
4672
  this.mouseEnabled = opts.mouse ?? true;
4673
+ this.progressIndicatorEnabled = opts.progressIndicator ?? true;
4446
4674
  this.resizeHandler = () => this.repaint();
4447
4675
  this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
4448
4676
  this.mouseHandler = (name) => this.handleMouse(name);
@@ -4471,6 +4699,8 @@ var init_screen = __esm({
4471
4699
  }
4472
4700
  this.term.on("resize", this.resizeHandler);
4473
4701
  this.installBracketedPaste();
4702
+ this.lastProgressState = 0;
4703
+ this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
4474
4704
  this.repaint();
4475
4705
  }
4476
4706
  stop() {
@@ -4491,6 +4721,7 @@ var init_screen = __esm({
4491
4721
  this.term.grabInput(false);
4492
4722
  this.term.hideCursor(false);
4493
4723
  process.stdout.write("\x1B[?7h");
4724
+ this.writeProgressIndicator(0);
4494
4725
  this.term.fullscreen(false);
4495
4726
  this.term("\n");
4496
4727
  }
@@ -4536,7 +4767,12 @@ var init_screen = __esm({
4536
4767
  this.pasteActive = false;
4537
4768
  const pasted = Buffer.from(this.pasteBuffer, "binary").toString("utf-8").replace(/\r\n?/g, "\n");
4538
4769
  this.pasteBuffer = "";
4539
- this.onKey([{ type: "paste", text: pasted }]);
4770
+ const paths2 = parseImageDropPaste(pasted);
4771
+ if (paths2 !== null) {
4772
+ this.onKey([{ type: "attachment-paths", paths: paths2 }]);
4773
+ } else {
4774
+ this.onKey([{ type: "paste", text: pasted }]);
4775
+ }
4540
4776
  continue;
4541
4777
  }
4542
4778
  const startIdx = text.indexOf(startMarker);
@@ -4779,9 +5015,26 @@ var init_screen = __esm({
4779
5015
  }
4780
5016
  setBanner(banner) {
4781
5017
  this.banner = { ...this.banner, ...banner };
5018
+ this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
4782
5019
  this.drawBanner();
4783
5020
  this.placeCursor();
4784
5021
  }
5022
+ // OSC 9;4 progress-bar control. State 3 = indeterminate (pulsing
5023
+ // taskbar / dock badge while a turn is running); state 0 = remove.
5024
+ // ConEmu-flavor sequence — supported by Windows Terminal, WezTerm,
5025
+ // Ghostty, Konsole, Black Box, Rio, and others; ignored harmlessly
5026
+ // by terminals that don't implement it. Disabled entirely when
5027
+ // tui.progressIndicator is false.
5028
+ writeProgressIndicator(state) {
5029
+ if (!this.progressIndicatorEnabled) {
5030
+ return;
5031
+ }
5032
+ if (state === this.lastProgressState) {
5033
+ return;
5034
+ }
5035
+ this.lastProgressState = state;
5036
+ process.stdout.write(`\x1B]9;4;${state}\x1B\\`);
5037
+ }
4785
5038
  // Transient right-side banner message. Cleared automatically after
4786
5039
  // durationMs (default 4s). Each call resets the timer, so rapid
4787
5040
  // successive notifications coalesce on the latest text. Active
@@ -5270,7 +5523,7 @@ var init_screen = __esm({
5270
5523
  }
5271
5524
  scrollbackVisibleRows() {
5272
5525
  const top = HEADER_ROWS + SEPARATOR_ROWS;
5273
- const bottom = this.term.height - this.promptRows() - BANNER_ROWS - SEPARATOR_ROWS - this.queuedRows() - this.completionRows();
5526
+ const bottom = this.term.height - this.promptRows() - BANNER_ROWS - SEPARATOR_ROWS - this.chipRows() - this.queuedRows() - this.completionRows();
5274
5527
  return Math.max(0, bottom - top + 1);
5275
5528
  }
5276
5529
  maxScrollOffset() {
@@ -5352,6 +5605,7 @@ var init_screen = __esm({
5352
5605
  this.drawScrollback();
5353
5606
  this.drawCompletionZone();
5354
5607
  this.drawQueuedZone();
5608
+ this.drawAttachmentChipZone();
5355
5609
  const promptRows = this.promptRows();
5356
5610
  const separatorRow = h - promptRows - BANNER_ROWS;
5357
5611
  this.drawSeparator(separatorRow);
@@ -5443,6 +5697,16 @@ var init_screen = __esm({
5443
5697
  queuedRows() {
5444
5698
  return Math.min(MAX_QUEUED_ROWS, this.queuedTexts.length);
5445
5699
  }
5700
+ chipRows() {
5701
+ return Math.min(MAX_CHIP_ROWS, this.attachments.length);
5702
+ }
5703
+ setAttachments(attachments) {
5704
+ if (this.attachments.length === attachments.length && this.attachments.every((a, i) => a === attachments[i])) {
5705
+ return;
5706
+ }
5707
+ this.attachments = [...attachments];
5708
+ this.repaint();
5709
+ }
5446
5710
  completionRows() {
5447
5711
  if (this.permissionPrompt) {
5448
5712
  return 0;
@@ -5458,7 +5722,8 @@ var init_screen = __esm({
5458
5722
  const promptRows = this.promptRows();
5459
5723
  const separatorRow = this.term.height - promptRows - BANNER_ROWS;
5460
5724
  const queuedRows = this.queuedRows();
5461
- const completionBottom = separatorRow - 1 - queuedRows;
5725
+ const chipRows = this.chipRows();
5726
+ const completionBottom = separatorRow - 1 - queuedRows - chipRows;
5462
5727
  const completionTop = completionBottom - rows + 1;
5463
5728
  let nameWidth = 0;
5464
5729
  for (const item of this.completions.slice(0, rows)) {
@@ -5491,6 +5756,58 @@ var init_screen = __esm({
5491
5756
  });
5492
5757
  }
5493
5758
  }
5759
+ // Chip zone: one row per attached image, sitting between the queued
5760
+ // zone and the separator (closest to the user's draft). Each row
5761
+ // shows "📎 <name> · <size>" plus, in iTerm2-capable terminals, a
5762
+ // tiny inline thumbnail at the end. Overflow collapses into a
5763
+ // single "+ N more attached" row.
5764
+ drawAttachmentChipZone() {
5765
+ const rows = this.chipRows();
5766
+ if (rows === 0) {
5767
+ return;
5768
+ }
5769
+ const w = this.term.width;
5770
+ const promptRows = this.promptRows();
5771
+ const separatorRow = this.term.height - promptRows - BANNER_ROWS;
5772
+ const chipBottom = separatorRow - 1;
5773
+ const chipTop = chipBottom - rows + 1;
5774
+ const iterm = this.isIterm2();
5775
+ for (let i = 0; i < rows; i++) {
5776
+ const row = chipTop + i;
5777
+ const isLast = i === rows - 1 && this.attachments.length > MAX_CHIP_ROWS;
5778
+ const overflow = this.attachments.length - MAX_CHIP_ROWS;
5779
+ const att = this.attachments[i];
5780
+ const label = att ? `${att.name ?? "image"} \xB7 ${formatSize(att.sizeBytes)}` : "";
5781
+ const sig = isLast ? `chip|${w}|overflow|${overflow}` : att ? `chip|${w}|${iterm ? "i" : "t"}|${label}|${att.sizeBytes}` : `chip|${w}|empty`;
5782
+ this.paintRow(row, sig, () => {
5783
+ if (isLast) {
5784
+ this.term.dim(` \u{1F4CE} + ${overflow + 1} more attached`);
5785
+ return;
5786
+ }
5787
+ if (!att) {
5788
+ return;
5789
+ }
5790
+ this.term(" ").yellow(`\u{1F4CE} ${label}`);
5791
+ if (iterm) {
5792
+ this.term(" ");
5793
+ this.writeIterm2Image(att.data, 1);
5794
+ }
5795
+ });
5796
+ }
5797
+ }
5798
+ isIterm2() {
5799
+ const env = process.env;
5800
+ return env.LC_TERMINAL === "iTerm2" || env.TERM_PROGRAM === "iTerm.app";
5801
+ }
5802
+ // Emits the iTerm2 OSC 1337 inline image escape at the current
5803
+ // cursor position. Wraps in DCS-passthrough when tmux is detected
5804
+ // (requires `set -g allow-passthrough on` in the user's tmux conf).
5805
+ // Caller is responsible for knowing iTerm2 is the active terminal.
5806
+ writeIterm2Image(base64, heightCells) {
5807
+ process.stdout.write(
5808
+ buildIterm2ImageEscape(base64, heightCells, Boolean(process.env.TMUX))
5809
+ );
5810
+ }
5494
5811
  drawQueuedZone() {
5495
5812
  const rows = this.queuedRows();
5496
5813
  if (rows === 0) {
@@ -5499,7 +5816,8 @@ var init_screen = __esm({
5499
5816
  const w = this.term.width;
5500
5817
  const promptRows = this.promptRows();
5501
5818
  const separatorRow = this.term.height - promptRows - BANNER_ROWS;
5502
- const queuedBottom = separatorRow - 1;
5819
+ const chipRows = this.chipRows();
5820
+ const queuedBottom = separatorRow - 1 - chipRows;
5503
5821
  const queuedTop = queuedBottom - rows + 1;
5504
5822
  const editingIndex = this.dispatcher.state().queueIndex;
5505
5823
  for (let i = 0; i < rows; i++) {
@@ -5648,6 +5966,8 @@ var init_screen = __esm({
5648
5966
  }
5649
5967
  } else if (this.banner.status === "disconnected") {
5650
5968
  this.term.brightRed(`${dot} ${this.banner.status}`);
5969
+ } else if (this.banner.status === "cold") {
5970
+ this.term.brightMagenta(`${dot} ${this.banner.status}`);
5651
5971
  } else {
5652
5972
  this.term.brightGreen(`${dot} ${this.banner.status}`);
5653
5973
  }
@@ -5802,6 +6122,9 @@ var init_screen = __esm({
5802
6122
  if (line.ansi) {
5803
6123
  wrappedLine.ansi = true;
5804
6124
  }
6125
+ if (i === 0 && line.iterm2Image) {
6126
+ wrappedLine.iterm2Image = line.iterm2Image;
6127
+ }
5805
6128
  if (id !== void 0 && chunk.length > 0) {
5806
6129
  const found = line.body.indexOf(chunk, scanPos);
5807
6130
  const colOffset = found === -1 ? scanPos : found;
@@ -5847,6 +6170,12 @@ var init_screen = __esm({
5847
6170
  if (line.ansi || line.body.includes("^")) {
5848
6171
  this.term.styleReset();
5849
6172
  }
6173
+ if (line.iterm2Image && this.isIterm2()) {
6174
+ this.writeIterm2Image(
6175
+ line.iterm2Image.data,
6176
+ line.iterm2Image.heightCells
6177
+ );
6178
+ }
5850
6179
  }
5851
6180
  };
5852
6181
  NON_ASCII = /[^\x20-\x7e]/;
@@ -5891,6 +6220,17 @@ var init_input = __esm({
5891
6220
  // here so ^Y can yank it back. Standard readline keeps a stack; we
5892
6221
  // only keep one slot because that's what 99% of yank uses look like.
5893
6222
  killBuffer = "";
6223
+ // Images attached to the current draft. Cleared in the same paths
6224
+ // that clear the text buffer (clearBuffer, after send). Queue
6225
+ // navigation snapshots/restores them alongside savedDraft so up/down
6226
+ // through queued items doesn't drop chips.
6227
+ attachments = [];
6228
+ // Snapshot of `attachments` taken when the user starts walking
6229
+ // history/queue with chips already attached. Restored alongside the
6230
+ // text draft when the walk ends. Distinct from savedDraft because
6231
+ // queue slots (which may carry their own attachments — though we
6232
+ // don't surface that yet) shouldn't blend with the current draft's.
6233
+ savedAttachments = null;
5894
6234
  constructor(opts = {}) {
5895
6235
  this.history = [...opts.history ?? []];
5896
6236
  this.planMode = opts.planMode ?? false;
@@ -5903,9 +6243,22 @@ var init_input = __esm({
5903
6243
  planMode: this.planMode,
5904
6244
  historyIndex: this.historyIndex,
5905
6245
  queueIndex: this.queueIndex,
6246
+ attachments: [...this.attachments],
5906
6247
  historySearchQuery: this.historySearch?.query ?? null
5907
6248
  };
5908
6249
  }
6250
+ // App calls this after asynchronously acquiring an image (drag-drop
6251
+ // file read, clipboard shellout). The dispatcher just records it;
6252
+ // chip rendering and capability gating live in the app/screen layer.
6253
+ addAttachment(attachment) {
6254
+ this.attachments.push(attachment);
6255
+ }
6256
+ removeAttachment(index) {
6257
+ if (index < 0 || index >= this.attachments.length) {
6258
+ return;
6259
+ }
6260
+ this.attachments.splice(index, 1);
6261
+ }
5909
6262
  setTurnRunning(running) {
5910
6263
  this.turnRunning = running;
5911
6264
  }
@@ -5936,13 +6289,17 @@ var init_input = __esm({
5936
6289
  }
5937
6290
  // Public seed for the buffer (used for Escape pre-fill). Treated like a
5938
6291
  // fresh draft: nav state and any saved draft are cleared, cursor lands
5939
- // at the end so the user can edit immediately.
5940
- setBuffer(text) {
6292
+ // at the end so the user can edit immediately. Attachments restore
6293
+ // alongside the text so a cancelled turn's chips land back in the
6294
+ // draft together with the typed prompt.
6295
+ setBuffer(text, attachments = []) {
5941
6296
  this.loadEntry(text);
5942
6297
  this.historyIndex = -1;
5943
6298
  this.queueIndex = -1;
5944
6299
  this.savedDraft = null;
6300
+ this.savedAttachments = null;
5945
6301
  this.historySearch = null;
6302
+ this.attachments = [...attachments];
5946
6303
  }
5947
6304
  feed(event) {
5948
6305
  if (this.historySearch !== null) {
@@ -5988,6 +6345,9 @@ var init_input = __esm({
5988
6345
  this.insertText(event.text);
5989
6346
  return [];
5990
6347
  }
6348
+ if (event.type === "attachment-paths") {
6349
+ return [];
6350
+ }
5991
6351
  return this.handleKey(event.name);
5992
6352
  }
5993
6353
  handleKey(name) {
@@ -6064,6 +6424,8 @@ var init_input = __esm({
6064
6424
  case "ctrl-u":
6065
6425
  this.killLine();
6066
6426
  return [];
6427
+ case "ctrl-v":
6428
+ return [{ type: "attachment-request", source: "clipboard" }];
6067
6429
  case "ctrl-w":
6068
6430
  this.killWord();
6069
6431
  return [];
@@ -6096,7 +6458,9 @@ var init_input = __esm({
6096
6458
  this.historyIndex = -1;
6097
6459
  this.queueIndex = -1;
6098
6460
  this.savedDraft = null;
6461
+ this.savedAttachments = null;
6099
6462
  this.historySearch = null;
6463
+ this.attachments = [];
6100
6464
  }
6101
6465
  insertChar(ch) {
6102
6466
  if (ch.length === 0) {
@@ -6248,6 +6612,8 @@ var init_input = __esm({
6248
6612
  row: this.row,
6249
6613
  col: this.col
6250
6614
  };
6615
+ this.savedAttachments = [...this.attachments];
6616
+ this.attachments = [];
6251
6617
  if (this.queue.length > 0) {
6252
6618
  this.queueIndex = this.queue.length - 1;
6253
6619
  this.loadEntry(this.queue[this.queueIndex] ?? "");
@@ -6320,6 +6686,8 @@ var init_input = __esm({
6320
6686
  this.row = this.savedDraft.row;
6321
6687
  this.col = this.savedDraft.col;
6322
6688
  this.savedDraft = null;
6689
+ this.attachments = this.savedAttachments ?? [];
6690
+ this.savedAttachments = null;
6323
6691
  } else {
6324
6692
  this.clearBuffer();
6325
6693
  }
@@ -6473,18 +6841,20 @@ var init_input = __esm({
6473
6841
  const text = this.bufferText();
6474
6842
  if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
6475
6843
  const index = this.queueIndex;
6844
+ const attachments2 = [...this.attachments];
6476
6845
  this.clearBuffer();
6477
6846
  if (text.trim().length === 0) {
6478
6847
  return [{ type: "queue-remove", index }];
6479
6848
  }
6480
- return [{ type: "queue-edit", index, text }];
6849
+ return [{ type: "queue-edit", index, text, attachments: attachments2 }];
6481
6850
  }
6482
- if (text.trim().length === 0) {
6851
+ if (text.trim().length === 0 && this.attachments.length === 0) {
6483
6852
  return [];
6484
6853
  }
6485
6854
  const planMode = this.planMode;
6855
+ const attachments = [...this.attachments];
6486
6856
  this.clearBuffer();
6487
- return [{ type: "send", text, planMode }];
6857
+ return [{ type: "send", text, planMode, attachments }];
6488
6858
  }
6489
6859
  // Home: jump to the very start of the prompt buffer. If we're already
6490
6860
  // there, fall through to scrolling the scrollback to its top.
@@ -6509,13 +6879,15 @@ var init_input = __esm({
6509
6879
  return [{ type: "scroll-to-bottom" }];
6510
6880
  }
6511
6881
  handleCtrlC() {
6512
- if (!this.bufferIsEmpty()) {
6882
+ if (!this.bufferIsEmpty() || this.attachments.length > 0) {
6513
6883
  this.buffer = [""];
6514
6884
  this.row = 0;
6515
6885
  this.col = 0;
6886
+ this.attachments = [];
6516
6887
  if (this.queueIndex === -1) {
6517
6888
  this.historyIndex = -1;
6518
6889
  this.savedDraft = null;
6890
+ this.savedAttachments = null;
6519
6891
  }
6520
6892
  return [];
6521
6893
  }
@@ -6533,6 +6905,232 @@ var init_input = __esm({
6533
6905
  }
6534
6906
  });
6535
6907
 
6908
+ // src/tui/clipboard.ts
6909
+ import { spawn as nodeSpawn } from "child_process";
6910
+ import fs14 from "fs/promises";
6911
+ import os4 from "os";
6912
+ import path10 from "path";
6913
+ async function readClipboard(envIn = {}) {
6914
+ const env = { ...defaultEnv, ...envIn };
6915
+ if (env.platform === "darwin") {
6916
+ return readMacOS(env);
6917
+ }
6918
+ if (env.platform === "linux") {
6919
+ return readLinux(env);
6920
+ }
6921
+ return {
6922
+ ok: false,
6923
+ reason: `clipboard paste is not supported on ${env.platform}`
6924
+ };
6925
+ }
6926
+ async function readMacOS(env) {
6927
+ const tmpPath = path10.join(
6928
+ env.tmpdir(),
6929
+ `hydra-clipboard-${Date.now()}-${process.pid}.png`
6930
+ );
6931
+ const script = [
6932
+ "set png_data to the clipboard as \xABclass PNGf\xBB",
6933
+ `set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
6934
+ "write png_data to out_file",
6935
+ "close access out_file"
6936
+ ];
6937
+ const args = [];
6938
+ for (const line of script) {
6939
+ args.push("-e", line);
6940
+ }
6941
+ try {
6942
+ await run2(env.spawn, "osascript", args);
6943
+ const img = await readFileAsAttachment(tmpPath, true);
6944
+ if (img.ok) {
6945
+ return img;
6946
+ }
6947
+ if (img.reason.startsWith("clipboard image is")) {
6948
+ return img;
6949
+ }
6950
+ } catch {
6951
+ await fs14.unlink(tmpPath).catch(() => void 0);
6952
+ }
6953
+ try {
6954
+ const buf = await runCapture(env.spawn, "pbpaste", []);
6955
+ if (buf.length === 0) {
6956
+ return { ok: false, reason: "clipboard is empty" };
6957
+ }
6958
+ return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
6959
+ } catch {
6960
+ return { ok: false, reason: "clipboard read failed" };
6961
+ }
6962
+ }
6963
+ async function readLinux(env) {
6964
+ const tool = await detectLinuxTool(env);
6965
+ if (!tool) {
6966
+ return {
6967
+ ok: false,
6968
+ reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
6969
+ };
6970
+ }
6971
+ try {
6972
+ const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
6973
+ if (buf.length > 0) {
6974
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
6975
+ return {
6976
+ ok: false,
6977
+ reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
6978
+ };
6979
+ }
6980
+ return {
6981
+ ok: true,
6982
+ kind: "image",
6983
+ attachment: {
6984
+ mimeType: "image/png",
6985
+ data: buf.toString("base64"),
6986
+ sizeBytes: buf.length
6987
+ }
6988
+ };
6989
+ }
6990
+ } catch {
6991
+ }
6992
+ try {
6993
+ const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
6994
+ if (buf.length === 0) {
6995
+ return { ok: false, reason: "clipboard is empty" };
6996
+ }
6997
+ return {
6998
+ ok: true,
6999
+ kind: "text",
7000
+ text: normalizeText(buf.toString("utf-8"))
7001
+ };
7002
+ } catch {
7003
+ return { ok: false, reason: "clipboard read failed" };
7004
+ }
7005
+ }
7006
+ async function detectLinuxTool(env) {
7007
+ if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
7008
+ return {
7009
+ cmd: "wl-paste",
7010
+ imageArgs: ["-t", "image/png"],
7011
+ // -n: drop trailing newline wl-paste adds by default. We further
7012
+ // normalize line endings below, but this avoids a spurious
7013
+ // empty trailing row from a single-line clipboard text.
7014
+ textArgs: ["-n"]
7015
+ };
7016
+ }
7017
+ if (env.env.DISPLAY && await which(env, "xclip")) {
7018
+ return {
7019
+ cmd: "xclip",
7020
+ imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
7021
+ textArgs: ["-selection", "clipboard", "-o"]
7022
+ };
7023
+ }
7024
+ return null;
7025
+ }
7026
+ function normalizeText(text) {
7027
+ return text.replace(/\r\n?/g, "\n");
7028
+ }
7029
+ async function which(env, cmd) {
7030
+ try {
7031
+ await run2(env.spawn, "which", [cmd]);
7032
+ return true;
7033
+ } catch {
7034
+ return false;
7035
+ }
7036
+ }
7037
+ async function readFileAsAttachment(p, unlinkAfter) {
7038
+ try {
7039
+ const buf = await fs14.readFile(p);
7040
+ if (unlinkAfter) {
7041
+ await fs14.unlink(p).catch(() => void 0);
7042
+ }
7043
+ if (buf.length === 0) {
7044
+ return { ok: false, reason: "no image on clipboard" };
7045
+ }
7046
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
7047
+ return {
7048
+ ok: false,
7049
+ reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
7050
+ };
7051
+ }
7052
+ const mimeType = mimeFromExtension(p) ?? "image/png";
7053
+ return {
7054
+ ok: true,
7055
+ kind: "image",
7056
+ attachment: {
7057
+ mimeType,
7058
+ data: buf.toString("base64"),
7059
+ sizeBytes: buf.length
7060
+ }
7061
+ };
7062
+ } catch {
7063
+ return { ok: false, reason: "failed to read clipboard image" };
7064
+ }
7065
+ }
7066
+ function run2(spawn6, cmd, args) {
7067
+ return new Promise((resolve5, reject) => {
7068
+ const proc = spawn6(cmd, args);
7069
+ proc.stdout?.on("data", () => void 0);
7070
+ proc.stderr?.on("data", () => void 0);
7071
+ proc.on("error", reject);
7072
+ proc.on("close", (code) => {
7073
+ if (code === 0) {
7074
+ resolve5();
7075
+ } else {
7076
+ reject(new Error(`${cmd} exited ${code}`));
7077
+ }
7078
+ });
7079
+ });
7080
+ }
7081
+ function runCapture(spawn6, cmd, args) {
7082
+ return new Promise((resolve5, reject) => {
7083
+ const proc = spawn6(cmd, args);
7084
+ const chunks = [];
7085
+ let stdoutEnded = proc.stdout === null;
7086
+ let closedCode = null;
7087
+ let settled = false;
7088
+ const settle = () => {
7089
+ if (settled || !stdoutEnded || closedCode === null) {
7090
+ return;
7091
+ }
7092
+ settled = true;
7093
+ if (closedCode === 0) {
7094
+ resolve5(Buffer.concat(chunks));
7095
+ } else {
7096
+ reject(new Error(`${cmd} exited ${closedCode}`));
7097
+ }
7098
+ };
7099
+ proc.stdout?.on("data", (chunk) => {
7100
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
7101
+ });
7102
+ proc.stdout?.on("end", () => {
7103
+ stdoutEnded = true;
7104
+ settle();
7105
+ });
7106
+ proc.stderr?.on("data", () => void 0);
7107
+ proc.on("error", (err) => {
7108
+ if (settled) {
7109
+ return;
7110
+ }
7111
+ settled = true;
7112
+ reject(err);
7113
+ });
7114
+ proc.on("close", (code) => {
7115
+ closedCode = code ?? 0;
7116
+ settle();
7117
+ });
7118
+ });
7119
+ }
7120
+ var defaultEnv;
7121
+ var init_clipboard = __esm({
7122
+ "src/tui/clipboard.ts"() {
7123
+ "use strict";
7124
+ init_attachments();
7125
+ defaultEnv = {
7126
+ platform: process.platform,
7127
+ env: process.env,
7128
+ spawn: nodeSpawn,
7129
+ tmpdir: os4.tmpdir
7130
+ };
7131
+ }
7132
+ });
7133
+
6536
7134
  // src/tui/completion.ts
6537
7135
  function longestCommonPrefix(names) {
6538
7136
  if (names.length === 0) {
@@ -6867,8 +7465,29 @@ import chalk from "chalk";
6867
7465
  import { highlight, supportsLanguage } from "cli-highlight";
6868
7466
  function formatEvent(event) {
6869
7467
  switch (event.kind) {
6870
- case "user-text":
6871
- return formatBlock(event.text, "\u258E ", "user", void 0, event.sentBy, true);
7468
+ case "user-text": {
7469
+ const lines = formatBlock(
7470
+ event.text,
7471
+ "\u258E ",
7472
+ "user",
7473
+ void 0,
7474
+ event.sentBy,
7475
+ true
7476
+ );
7477
+ if (event.attachments && event.attachments.length > 0) {
7478
+ for (const a of event.attachments) {
7479
+ lines.push({
7480
+ prefix: "\u258E ",
7481
+ prefixStyle: "user",
7482
+ body: `\u{1F4CE} ${a.name ?? "image"}`,
7483
+ bodyStyle: "user",
7484
+ fillRow: true,
7485
+ iterm2Image: { data: a.data, heightCells: 5 }
7486
+ });
7487
+ }
7488
+ }
7489
+ return lines;
7490
+ }
6872
7491
  case "agent-text":
6873
7492
  return formatBlock(event.text, " ", "agent");
6874
7493
  case "agent-thought":
@@ -7198,6 +7817,8 @@ var init_format = __esm({
7198
7817
  import { appendFileSync, statSync, renameSync } from "fs";
7199
7818
  import { nanoid as nanoid3 } from "nanoid";
7200
7819
  import termkit from "terminal-kit";
7820
+ import fs15 from "fs/promises";
7821
+ import path11 from "path";
7201
7822
  async function runTuiApp(opts) {
7202
7823
  const config = await ensureConfig();
7203
7824
  logMaxBytes = config.tui.logMaxBytes;
@@ -7315,6 +7936,15 @@ async function runSession(term, config, opts, exitHint) {
7315
7936
  appendRender(event);
7316
7937
  maybeDismissPermissionByToolUpdate(update);
7317
7938
  });
7939
+ conn.onNotification("hydra-acp/session_closed", () => {
7940
+ if (pendingTurns > 0) {
7941
+ adjustPendingTurns(-pendingTurns);
7942
+ }
7943
+ const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
7944
+ if (screenReady) {
7945
+ screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
7946
+ }
7947
+ });
7318
7948
  const handlePermissionResolved = (update) => {
7319
7949
  const u = update ?? {};
7320
7950
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
@@ -7418,6 +8048,7 @@ async function runSession(term, config, opts, exitHint) {
7418
8048
  });
7419
8049
  let upstreamSessionId;
7420
8050
  let agentInfoName;
8051
+ let agentAcceptsImages = true;
7421
8052
  try {
7422
8053
  const initResult = await conn.request("initialize", {
7423
8054
  protocolVersion: ACP_PROTOCOL_VERSION,
@@ -7428,6 +8059,10 @@ async function runSession(term, config, opts, exitHint) {
7428
8059
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
7429
8060
  });
7430
8061
  agentInfoName = initResult?.agentInfo?.name;
8062
+ const imageCap = initResult?.agentCapabilities?.promptCapabilities?.image;
8063
+ if (imageCap === false) {
8064
+ agentAcceptsImages = false;
8065
+ }
7431
8066
  } catch {
7432
8067
  }
7433
8068
  let resolvedSessionId = ctx.sessionId;
@@ -7511,6 +8146,7 @@ async function runSession(term, config, opts, exitHint) {
7511
8146
  repaintThrottleMs: config.tui.repaintThrottleMs,
7512
8147
  maxScrollbackLines: config.tui.maxScrollbackLines,
7513
8148
  mouse: config.tui.mouse,
8149
+ progressIndicator: config.tui.progressIndicator,
7514
8150
  onKey: (events) => {
7515
8151
  for (const ev of events) {
7516
8152
  if (pendingPermission && tryHandlePermissionKey(ev)) {
@@ -7525,6 +8161,10 @@ async function runSession(term, config, opts, exitHint) {
7525
8161
  if (tryHandleCompletionKey(ev)) {
7526
8162
  continue;
7527
8163
  }
8164
+ if (ev.type === "attachment-paths") {
8165
+ void handleAttachmentPaths(ev.paths);
8166
+ continue;
8167
+ }
7528
8168
  const effects = dispatcher.feed(ev);
7529
8169
  for (const effect of effects) {
7530
8170
  handleEffect(effect);
@@ -7534,6 +8174,7 @@ async function runSession(term, config, opts, exitHint) {
7534
8174
  screen.setBannerSearchIndicator(
7535
8175
  dispatcher.state().historySearchQuery
7536
8176
  );
8177
+ screen.setAttachments(dispatcher.state().attachments);
7537
8178
  screen.refreshPrompt();
7538
8179
  }
7539
8180
  });
@@ -7824,7 +8465,8 @@ async function runSession(term, config, opts, exitHint) {
7824
8465
  const choice = await pickSession(term, {
7825
8466
  cwd: resolvedCwd,
7826
8467
  sessions,
7827
- config
8468
+ config,
8469
+ currentSessionId: resolvedSessionId
7828
8470
  });
7829
8471
  if (choice.kind === "abort") {
7830
8472
  screen.start();
@@ -7855,13 +8497,17 @@ async function runSession(term, config, opts, exitHint) {
7855
8497
  const handleEffect = (effect) => {
7856
8498
  switch (effect.type) {
7857
8499
  case "send":
7858
- enqueuePrompt(effect.text, effect.planMode);
8500
+ enqueuePrompt(effect.text, effect.planMode, effect.attachments);
7859
8501
  return;
7860
8502
  case "queue-edit": {
7861
8503
  const realIdx = effect.index + queueHeadOffset();
7862
8504
  const existing = promptQueue[realIdx];
7863
8505
  if (existing) {
7864
- promptQueue[realIdx] = { text: effect.text, planMode: existing.planMode };
8506
+ promptQueue[realIdx] = {
8507
+ text: effect.text,
8508
+ planMode: existing.planMode,
8509
+ attachments: effect.attachments
8510
+ };
7865
8511
  refreshQueueDisplay();
7866
8512
  }
7867
8513
  return;
@@ -7880,7 +8526,10 @@ async function runSession(term, config, opts, exitHint) {
7880
8526
  const waitingEmpty = promptQueue.length <= headOffset;
7881
8527
  const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
7882
8528
  if (waitingEmpty && bufferEmpty) {
7883
- pendingPrefill = turnInFlight.text;
8529
+ pendingPrefill = {
8530
+ text: turnInFlight.text,
8531
+ attachments: turnInFlight.attachments
8532
+ };
7884
8533
  }
7885
8534
  }
7886
8535
  if (turnInFlight) {
@@ -7919,17 +8568,81 @@ async function runSession(term, config, opts, exitHint) {
7919
8568
  screen.enterScrollbackSearch();
7920
8569
  screen.updateScrollbackSearchTerm(effect.query);
7921
8570
  return;
8571
+ case "attachment-request":
8572
+ void handleClipboardAttachment();
8573
+ return;
8574
+ }
8575
+ };
8576
+ const handleAttachmentPaths = async (paths2) => {
8577
+ if (!agentAcceptsImages) {
8578
+ screen.notify("agent does not accept image attachments");
8579
+ return;
8580
+ }
8581
+ let added = 0;
8582
+ for (const p of paths2) {
8583
+ const mimeType = mimeFromExtension(p);
8584
+ if (!mimeType) {
8585
+ screen.notify(`unsupported image type: ${path11.basename(p)}`);
8586
+ continue;
8587
+ }
8588
+ try {
8589
+ const buf = await fs15.readFile(p);
8590
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
8591
+ screen.notify(
8592
+ `image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
8593
+ );
8594
+ continue;
8595
+ }
8596
+ dispatcher.addAttachment({
8597
+ mimeType,
8598
+ data: buf.toString("base64"),
8599
+ name: path11.basename(p),
8600
+ sizeBytes: buf.length
8601
+ });
8602
+ added++;
8603
+ } catch (err) {
8604
+ screen.notify(`cannot read ${path11.basename(p)}: ${err.message}`);
8605
+ }
8606
+ }
8607
+ if (added > 0) {
8608
+ screen.setAttachments(dispatcher.state().attachments);
8609
+ screen.refreshPrompt();
8610
+ }
8611
+ };
8612
+ const handleClipboardAttachment = async () => {
8613
+ const result = await readClipboard();
8614
+ if (!result.ok) {
8615
+ screen.notify(result.reason);
8616
+ return;
8617
+ }
8618
+ if (result.kind === "image") {
8619
+ if (!agentAcceptsImages) {
8620
+ screen.notify("agent does not accept image attachments");
8621
+ return;
8622
+ }
8623
+ dispatcher.addAttachment(result.attachment);
8624
+ screen.setAttachments(dispatcher.state().attachments);
8625
+ screen.refreshPrompt();
8626
+ return;
7922
8627
  }
8628
+ const effects = dispatcher.feed({ type: "paste", text: result.text });
8629
+ for (const effect of effects) {
8630
+ handleEffect(effect);
8631
+ }
8632
+ screen.refreshPrompt();
7923
8633
  };
7924
8634
  const promptQueue = [];
7925
8635
  let workerActive = false;
7926
8636
  const refreshQueueDisplay = () => {
7927
8637
  const waiting = promptQueue.slice(workerActive ? 1 : 0);
7928
- screen.setQueuedPrompts(waiting.map((p) => p.text));
8638
+ const displayTexts = waiting.map(
8639
+ (p) => p.attachments.length > 0 ? `${p.text} \xB7 \u{1F4CE}\xD7${p.attachments.length}` : p.text
8640
+ );
8641
+ screen.setQueuedPrompts(displayTexts);
7929
8642
  screen.setBanner({ queued: waiting.length });
7930
8643
  dispatcher.setQueue(waiting.map((p) => p.text));
7931
8644
  };
7932
- const enqueuePrompt = (text, planMode) => {
8645
+ const enqueuePrompt = (text, planMode, attachments) => {
7933
8646
  screen.scrollToBottom();
7934
8647
  if (handleBuiltinCommand(text)) {
7935
8648
  return;
@@ -7937,7 +8650,7 @@ async function runSession(term, config, opts, exitHint) {
7937
8650
  history = appendEntry(history, text);
7938
8651
  dispatcher.setHistory(history);
7939
8652
  saveHistory(historyFile, history).catch(() => void 0);
7940
- promptQueue.push({ text, planMode });
8653
+ promptQueue.push({ text, planMode, attachments });
7941
8654
  refreshQueueDisplay();
7942
8655
  tickWorker();
7943
8656
  };
@@ -8104,31 +8817,38 @@ async function runSession(term, config, opts, exitHint) {
8104
8817
  break;
8105
8818
  }
8106
8819
  refreshQueueDisplay();
8107
- await processPrompt(next.text, next.planMode);
8820
+ await processPrompt(next.text, next.planMode, next.attachments);
8108
8821
  promptQueue.shift();
8109
8822
  }
8110
8823
  } finally {
8111
8824
  workerActive = false;
8112
8825
  refreshQueueDisplay();
8113
8826
  if (pendingPrefill !== null) {
8114
- const text = pendingPrefill;
8827
+ const { text, attachments } = pendingPrefill;
8115
8828
  pendingPrefill = null;
8116
8829
  const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
8117
8830
  if (bufferEmpty) {
8118
- dispatcher.setBuffer(text);
8831
+ dispatcher.setBuffer(text, attachments);
8119
8832
  screen.refreshPrompt();
8120
8833
  }
8121
8834
  }
8122
8835
  }
8123
8836
  };
8124
- const processPrompt = async (text, planMode) => {
8125
- const userBlocks = [{ type: "text", text }];
8837
+ const processPrompt = async (text, planMode, attachments) => {
8838
+ const userBlocks = [];
8839
+ if (text.length > 0) {
8840
+ userBlocks.push({ type: "text", text });
8841
+ }
8842
+ for (const a of attachments) {
8843
+ userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
8844
+ }
8126
8845
  const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
8127
8846
  adjustPendingTurns(1);
8128
- appendRender({ kind: "user-text", text });
8847
+ appendRender({ kind: "user-text", text, attachments });
8129
8848
  let cancelled = false;
8130
8849
  turnInFlight = {
8131
8850
  text,
8851
+ attachments,
8132
8852
  cancel: () => {
8133
8853
  if (cancelled) {
8134
8854
  return;
@@ -8625,6 +9345,8 @@ var init_app = __esm({
8625
9345
  init_picker();
8626
9346
  init_screen();
8627
9347
  init_input();
9348
+ init_attachments();
9349
+ init_clipboard();
8628
9350
  init_completion();
8629
9351
  init_render_update();
8630
9352
  init_format();
@@ -9408,12 +10130,14 @@ var AgentInstance = class _AgentInstance {
9408
10130
  killed = false;
9409
10131
  stderrTail = "";
9410
10132
  stderrTailBytes;
10133
+ logger;
9411
10134
  exitHandlers = [];
9412
10135
  constructor(opts, child) {
9413
10136
  this.agentId = opts.agentId;
9414
10137
  this.cwd = opts.cwd;
9415
10138
  this.child = child;
9416
10139
  this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
10140
+ this.logger = opts.logger;
9417
10141
  if (!child.stdout || !child.stdin) {
9418
10142
  throw new Error("agent subprocess missing stdio");
9419
10143
  }
@@ -9422,7 +10146,15 @@ var AgentInstance = class _AgentInstance {
9422
10146
  child.stderr?.setEncoding("utf8");
9423
10147
  child.stderr?.on("data", (chunk) => {
9424
10148
  this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
9425
- process.stderr.write(`[${opts.agentId}] ${chunk}`);
10149
+ if (this.logger) {
10150
+ for (const line of chunk.split(/\r?\n/)) {
10151
+ if (line.length > 0) {
10152
+ this.logger.info(`[${opts.agentId}] ${line}`);
10153
+ }
10154
+ }
10155
+ } else {
10156
+ process.stderr.write(`[${opts.agentId}] ${chunk}`);
10157
+ }
9426
10158
  });
9427
10159
  child.on("error", (err) => {
9428
10160
  const msg = this.formatFailure(err.message);
@@ -9430,9 +10162,16 @@ var AgentInstance = class _AgentInstance {
9430
10162
  });
9431
10163
  child.on("exit", (code, signal) => {
9432
10164
  this.exited = true;
9433
- if (!this.killed) {
10165
+ if (this.killed) {
10166
+ this.logger?.info(
10167
+ `agent ${opts.agentId} pid=${child.pid} exited after kill code=${code} signal=${signal}`
10168
+ );
10169
+ } else {
9434
10170
  const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
9435
10171
  this.connection.fail(new Error(this.formatFailure(reason)));
10172
+ this.logger?.warn(
10173
+ `agent ${opts.agentId} pid=${child.pid} exited unexpectedly code=${code} signal=${signal}`
10174
+ );
9436
10175
  }
9437
10176
  for (const handler of this.exitHandlers) {
9438
10177
  handler(code, signal);
@@ -9453,7 +10192,15 @@ stderr: ${tail}` : reason;
9453
10192
  const child = spawn3(opts.plan.command, opts.plan.args, {
9454
10193
  cwd: opts.cwd,
9455
10194
  env,
9456
- stdio: ["pipe", "pipe", "pipe"]
10195
+ stdio: ["pipe", "pipe", "pipe"],
10196
+ // setsid the agent into its own session/process group. The daemon
10197
+ // already runs in its own setsid'd session, but macOS terminals
10198
+ // (iTerm2, Terminal.app) sometimes still reach inherited child
10199
+ // processes when the user closes a window — putting the agent
10200
+ // one more session-boundary away keeps it alive across terminal
10201
+ // restarts. The daemon still owns the pipes, so this.kill()
10202
+ // continues to terminate it cleanly on idle/close.
10203
+ detached: true
9457
10204
  });
9458
10205
  return new _AgentInstance(opts, child);
9459
10206
  }
@@ -9468,6 +10215,9 @@ stderr: ${tail}` : reason;
9468
10215
  return;
9469
10216
  }
9470
10217
  this.killed = true;
10218
+ this.logger?.info(
10219
+ `agent ${this.agentId} pid=${this.child.pid} kill requested signal=${signal}`
10220
+ );
9471
10221
  await this.connection.close().catch(() => void 0);
9472
10222
  this.child.kill(signal);
9473
10223
  }
@@ -9653,6 +10403,7 @@ var SessionManager = class {
9653
10403
  this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
9654
10404
  this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
9655
10405
  this.defaultModels = options.defaultModels ?? {};
10406
+ this.logger = options.logger;
9656
10407
  }
9657
10408
  registry;
9658
10409
  sessions = /* @__PURE__ */ new Map();
@@ -9667,6 +10418,7 @@ var SessionManager = class {
9667
10418
  // concurrent snapshot updates (e.g. an agent emitting model + mode
9668
10419
  // back-to-back) don't lose writes via interleaved reads.
9669
10420
  metaWriteQueues = /* @__PURE__ */ new Map();
10421
+ logger;
9670
10422
  async create(params) {
9671
10423
  const fresh = await this.bootstrapAgent({
9672
10424
  agentId: params.agentId,
@@ -9684,6 +10436,7 @@ var SessionManager = class {
9684
10436
  title: params.title,
9685
10437
  agentArgs: params.agentArgs,
9686
10438
  idleTimeoutMs: this.idleTimeoutMs,
10439
+ logger: this.logger,
9687
10440
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
9688
10441
  historyStore: this.histories,
9689
10442
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -9777,6 +10530,7 @@ var SessionManager = class {
9777
10530
  title: params.title,
9778
10531
  agentArgs: params.agentArgs,
9779
10532
  idleTimeoutMs: this.idleTimeoutMs,
10533
+ logger: this.logger,
9780
10534
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
9781
10535
  historyStore: this.histories,
9782
10536
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -9823,6 +10577,7 @@ var SessionManager = class {
9823
10577
  title: params.title,
9824
10578
  agentArgs: params.agentArgs,
9825
10579
  idleTimeoutMs: this.idleTimeoutMs,
10580
+ logger: this.logger,
9826
10581
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
9827
10582
  historyStore: this.histories,
9828
10583
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -10087,6 +10842,8 @@ var SessionManager = class {
10087
10842
  agentId: r.agentId,
10088
10843
  currentModel: r.currentModel,
10089
10844
  currentUsage: r.currentUsage,
10845
+ importedFromMachine: r.importedFromMachine,
10846
+ importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
10090
10847
  updatedAt: used,
10091
10848
  attachedClients: 0,
10092
10849
  status: "cold"
@@ -10194,6 +10951,8 @@ var SessionManager = class {
10194
10951
  lineageId: args.bundle.session.lineageId,
10195
10952
  upstreamSessionId: "",
10196
10953
  importedFromSessionId: args.bundle.session.sessionId,
10954
+ importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
10955
+ importedFromMachine: args.bundle.exportedFrom.machine,
10197
10956
  agentId: args.bundle.session.agentId,
10198
10957
  cwd: args.cwd ?? args.bundle.session.cwd,
10199
10958
  title: args.bundle.session.title,
@@ -10316,6 +11075,8 @@ function mergeForPersistence(session, existing) {
10316
11075
  lineageId: existing?.lineageId ?? generateLineageId(),
10317
11076
  upstreamSessionId: session.upstreamSessionId,
10318
11077
  importedFromSessionId: existing?.importedFromSessionId,
11078
+ importedFromUpstreamSessionId: existing?.importedFromUpstreamSessionId,
11079
+ importedFromMachine: existing?.importedFromMachine,
10319
11080
  agentId: session.agentId,
10320
11081
  cwd: session.cwd,
10321
11082
  title: session.title,
@@ -11595,11 +12356,20 @@ async function startDaemon(config) {
11595
12356
  await auth(request, reply);
11596
12357
  });
11597
12358
  const registry = new Registry(config);
11598
- const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
12359
+ const agentLogger = {
12360
+ info: (msg) => app.log.info(msg),
12361
+ warn: (msg) => app.log.warn(msg)
12362
+ };
12363
+ const spawner = (opts) => AgentInstance.spawn({
12364
+ ...opts,
12365
+ stderrTailBytes: config.daemon.agentStderrTailBytes,
12366
+ logger: agentLogger
12367
+ });
11599
12368
  const manager = new SessionManager(registry, spawner, void 0, {
11600
12369
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
11601
12370
  defaultModels: config.defaultModels,
11602
- sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
12371
+ sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
12372
+ logger: agentLogger
11603
12373
  });
11604
12374
  const extensions = new ExtensionManager(extensionList(config));
11605
12375
  registerHealthRoutes(app, HYDRA_VERSION);
@@ -11836,6 +12606,7 @@ async function runDaemonStart(flags = {}) {
11836
12606
  };
11837
12607
  process.on("SIGINT", () => void shutdown());
11838
12608
  process.on("SIGTERM", () => void shutdown());
12609
+ process.on("SIGHUP", () => void 0);
11839
12610
  return;
11840
12611
  }
11841
12612
  spawnDaemonDetached();