@hydra-acp/cli 0.1.40 → 0.1.42

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
@@ -91,6 +91,11 @@ var init_paths = __esm({
91
91
  extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
92
92
  extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
93
93
  tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
94
+ // Cross-session prompt history. Up-arrow / ^R fall through to this
95
+ // after the per-session list is exhausted. JSONL, one entry per
96
+ // line, append-only so concurrent TUIs don't lose each other's
97
+ // writes.
98
+ globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
94
99
  tuiLogFile: () => path.join(hydraHome(), "tui.log")
95
100
  };
96
101
  }
@@ -980,7 +985,8 @@ function mergeMeta(passthrough, ours) {
980
985
  function sessionListEntryToWire(entry) {
981
986
  const hydraMeta = {
982
987
  attachedClients: entry.attachedClients,
983
- status: entry.status
988
+ status: entry.status,
989
+ busy: entry.busy
984
990
  };
985
991
  if (entry.agentId !== void 0) {
986
992
  hydraMeta.agentId = entry.agentId;
@@ -1126,6 +1132,10 @@ var init_types = __esm({
1126
1132
  updatedAt: z3.string(),
1127
1133
  attachedClients: z3.number().int().nonnegative(),
1128
1134
  status: z3.enum(["live", "cold"]).default("live"),
1135
+ // True while the session is mid-turn (an agent prompt is in flight).
1136
+ // Always false for cold sessions. Lets pickers render a busy dot
1137
+ // without having to attach.
1138
+ busy: z3.boolean().default(false),
1129
1139
  _meta: z3.record(z3.unknown()).optional()
1130
1140
  });
1131
1141
  SessionListEntryWire = z3.object({
@@ -3832,7 +3842,7 @@ function parseHistory(text) {
3832
3842
  }
3833
3843
  return out;
3834
3844
  }
3835
- function appendEntry(history, entry) {
3845
+ function appendEntry(history, entry, cap = HISTORY_CAP) {
3836
3846
  const trimmed = entry.replace(/\n+$/, "");
3837
3847
  if (trimmed.length === 0) {
3838
3848
  return history;
@@ -3841,8 +3851,8 @@ function appendEntry(history, entry) {
3841
3851
  return history;
3842
3852
  }
3843
3853
  const out = history.concat(trimmed);
3844
- if (out.length > HISTORY_CAP) {
3845
- return out.slice(out.length - HISTORY_CAP);
3854
+ if (out.length > cap) {
3855
+ return out.slice(out.length - cap);
3846
3856
  }
3847
3857
  return out;
3848
3858
  }
@@ -3851,11 +3861,30 @@ async function saveHistory(file, history) {
3851
3861
  const lines = history.map((entry) => JSON.stringify(entry));
3852
3862
  await fs10.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
3853
3863
  }
3854
- var HISTORY_CAP;
3864
+ async function appendHistoryLine(file, entry) {
3865
+ const trimmed = entry.replace(/\n+$/, "");
3866
+ if (trimmed.length === 0) {
3867
+ return;
3868
+ }
3869
+ await fs10.mkdir(path6.dirname(file), { recursive: true });
3870
+ await fs10.appendFile(file, JSON.stringify(trimmed) + "\n", {
3871
+ encoding: "utf8"
3872
+ });
3873
+ }
3874
+ function buildCombinedHistory(global, session) {
3875
+ if (session.length === 0) {
3876
+ return [...global];
3877
+ }
3878
+ const sessionSet = new Set(session);
3879
+ const filteredGlobal = global.filter((e) => !sessionSet.has(e));
3880
+ return [...filteredGlobal, ...session];
3881
+ }
3882
+ var HISTORY_CAP, GLOBAL_HISTORY_CAP;
3855
3883
  var init_history = __esm({
3856
3884
  "src/tui/history.ts"() {
3857
3885
  "use strict";
3858
3886
  HISTORY_CAP = 500;
3887
+ GLOBAL_HISTORY_CAP = 2e3;
3859
3888
  }
3860
3889
  });
3861
3890
 
@@ -4425,7 +4454,8 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
4425
4454
  currentUsage: s.currentUsage,
4426
4455
  title: s.title,
4427
4456
  importedFromMachine: s.importedFromMachine,
4428
- importedFromUpstreamSessionId: s.importedFromUpstreamSessionId
4457
+ importedFromUpstreamSessionId: s.importedFromUpstreamSessionId,
4458
+ busy: s.busy
4429
4459
  }));
4430
4460
  }
4431
4461
  async function killSession(target, id, fetchImpl = fetch) {
@@ -4549,7 +4579,7 @@ function toRow(s, now = Date.now()) {
4549
4579
  return {
4550
4580
  session: stripHydraSessionPrefix(s.sessionId),
4551
4581
  upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
4552
- state: formatState(s.status, s.attachedClients),
4582
+ state: formatState(s.status, s.busy),
4553
4583
  agent: formatAgentCell(s.agentId, s.currentUsage),
4554
4584
  age: formatRelativeAge(s.updatedAt, now),
4555
4585
  title: s.title ?? "-",
@@ -4565,11 +4595,11 @@ function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
4565
4595
  }
4566
4596
  return "-";
4567
4597
  }
4568
- function formatState(status, clients) {
4598
+ function formatState(status, busy) {
4569
4599
  if (status === "cold") {
4570
4600
  return "COLD";
4571
4601
  }
4572
- return `LIVE(${clients})`;
4602
+ return busy ? "LIVE\u2022" : "LIVE";
4573
4603
  }
4574
4604
  function computeWidths(rows) {
4575
4605
  return {
@@ -7005,6 +7035,10 @@ uncaught: ${err.stack ?? err.message}
7005
7035
  this.handleCsi27Stdin(text);
7006
7036
  return;
7007
7037
  }
7038
+ if (text.includes("\x1B[200~")) {
7039
+ this.handleRawStdinSegment(text);
7040
+ return;
7041
+ }
7008
7042
  if (text.includes("\n")) {
7009
7043
  const parts = text.split("\n");
7010
7044
  for (let i = 0; i < parts.length; i++) {
@@ -7353,24 +7387,22 @@ uncaught: ${err.stack ?? err.message}
7353
7387
  this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
7354
7388
  this.syncedPartialRepaint(() => this.drawBanner());
7355
7389
  }
7356
- // Wrap a partial repaint (banner-only, indicator-only, etc.) in a
7390
+ // Wrap a partial repaint (banner-only, prompt-only, etc.) in a
7357
7391
  // synchronized-output bracket so the row swap is atomic on terminals
7358
- // that support DEC 2026, and hide the cursor across the paint so it
7359
- // doesn't visibly jump to the row being repainted before placeCursor
7360
- // snaps it back. placeCursor re-asserts visibility for normal /
7361
- // scrollback-search / readonly; modal modes only moveTo, so we
7362
- // re-show explicitly when one of them is active.
7392
+ // that support DEC 2026. Cursor movement (moveTo) is buffered inside
7393
+ // BSU/ESU, so the cursor appears at its final placeCursor position
7394
+ // without visibly visiting intermediate rows. We intentionally do NOT
7395
+ // hide the cursor here: ?25l/h (cursor visibility) is terminal *state*
7396
+ // applied immediately rather than buffered, so hiding inside a BSU/ESU
7397
+ // block causes a visible blink (cursor disappears → frame commits →
7398
+ // cursor reappears) on every banner tick — worse than any skitter.
7363
7399
  syncedPartialRepaint(paint) {
7364
7400
  if (!this.started) {
7365
7401
  return;
7366
7402
  }
7367
7403
  withSync(() => {
7368
- this.term.hideCursor();
7369
7404
  paint();
7370
7405
  this.placeCursor();
7371
- if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
7372
- this.term.hideCursor(false);
7373
- }
7374
7406
  });
7375
7407
  }
7376
7408
  currentModeId() {
@@ -8462,7 +8494,7 @@ uncaught: ${err.stack ?? err.message}
8462
8494
  const body = ` ${marker} ${i + 1}. ${truncate(opt.label, w - 8)}`;
8463
8495
  writeRow(`perm|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.label}`, () => {
8464
8496
  if (isSel) {
8465
- this.term.brightCyan(body);
8497
+ this.term.brightYellow(body);
8466
8498
  } else {
8467
8499
  this.term.dim(body);
8468
8500
  }
@@ -8771,7 +8803,7 @@ async function pickSession(term, opts) {
8771
8803
  if (tier !== 0) {
8772
8804
  return tier;
8773
8805
  }
8774
- return b.updatedAt.localeCompare(a.updatedAt);
8806
+ return b.updatedAt.slice(0, 16).localeCompare(a.updatedAt.slice(0, 16));
8775
8807
  });
8776
8808
  };
8777
8809
  let cwdOnly = false;
@@ -9178,6 +9210,8 @@ async function pickSession(term, opts) {
9178
9210
  renderFromScratch();
9179
9211
  return await new Promise((resolve6) => {
9180
9212
  let resolved = false;
9213
+ let autoRefreshTimer = null;
9214
+ let autoRefreshInFlight = false;
9181
9215
  const onResize = () => {
9182
9216
  if (resolved) {
9183
9217
  return;
@@ -9189,6 +9223,10 @@ async function pickSession(term, opts) {
9189
9223
  return;
9190
9224
  }
9191
9225
  resolved = true;
9226
+ if (autoRefreshTimer) {
9227
+ clearInterval(autoRefreshTimer);
9228
+ autoRefreshTimer = null;
9229
+ }
9192
9230
  term.off("key", onKey);
9193
9231
  term.off("resize", onResize);
9194
9232
  process.stdout.write("\x1B[?2004l");
@@ -9205,8 +9243,16 @@ async function pickSession(term, opts) {
9205
9243
  term.moveTo(1, indicatorRow() + 1);
9206
9244
  term("\n");
9207
9245
  };
9208
- const refresh = async (preferredId) => {
9246
+ const renderFingerprint = () => {
9247
+ const cells = rows.map(
9248
+ (r) => `${r.session}|${r.upstream}|${r.state}|${r.agent}|${r.age}|${r.title}|${r.cwd}`
9249
+ ).join("\n");
9250
+ return `${selectedIdx}:${scrollOffset}:${transientStatus ?? ""}
9251
+ ${cells}`;
9252
+ };
9253
+ const refresh = async (preferredId, refreshOpts = {}) => {
9209
9254
  try {
9255
+ const beforeKey = refreshOpts.silent ? renderFingerprint() : "";
9210
9256
  const next = await listSessions(opts.target);
9211
9257
  allSessions = sortSessions(next);
9212
9258
  applyFilter();
@@ -9223,8 +9269,14 @@ async function pickSession(term, opts) {
9223
9269
  scrollOffset = Math.max(0, visible.length - viewportSize);
9224
9270
  }
9225
9271
  adjustScroll();
9272
+ if (refreshOpts.silent && renderFingerprint() === beforeKey) {
9273
+ return;
9274
+ }
9226
9275
  renderFromScratch();
9227
9276
  } catch (err) {
9277
+ if (refreshOpts.silent) {
9278
+ return;
9279
+ }
9228
9280
  transientStatus = `refresh failed: ${err.message}`;
9229
9281
  renderFromScratch();
9230
9282
  }
@@ -9701,6 +9753,16 @@ async function pickSession(term, opts) {
9701
9753
  }
9702
9754
  term.on("key", onKey);
9703
9755
  term.on("resize", onResize);
9756
+ autoRefreshTimer = setInterval(() => {
9757
+ if (resolved || mode !== "normal" || searchActive || autoRefreshInFlight) {
9758
+ return;
9759
+ }
9760
+ const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
9761
+ autoRefreshInFlight = true;
9762
+ void refresh(currentId, { silent: true }).finally(() => {
9763
+ autoRefreshInFlight = false;
9764
+ });
9765
+ }, 3e3);
9704
9766
  });
9705
9767
  }
9706
9768
  function readTermHeight(term) {
@@ -11019,11 +11081,13 @@ function toolIconStyle(status) {
11019
11081
  }
11020
11082
  function formatPlan(event) {
11021
11083
  const stopped = event.stopped === true;
11084
+ const amended = event.amended === true;
11085
+ const stoppedStyle = amended ? "tool-status-cancelled" : "tool-status-fail";
11022
11086
  if (event.entries.length === 0) {
11023
11087
  return [
11024
11088
  {
11025
11089
  prefix: "\u25A3 ",
11026
- prefixStyle: stopped ? "tool-status-fail" : "plan",
11090
+ prefixStyle: stopped ? stoppedStyle : "plan",
11027
11091
  body: "(empty plan)",
11028
11092
  bodyStyle: "dim"
11029
11093
  }
@@ -11032,7 +11096,7 @@ function formatPlan(event) {
11032
11096
  const allComplete = event.entries.every(
11033
11097
  (e) => (e.status ?? "pending") === "completed"
11034
11098
  );
11035
- const headerStyle = allComplete ? "plan-done" : stopped ? "tool-status-fail" : "plan";
11099
+ const headerStyle = allComplete ? "plan-done" : stopped ? stoppedStyle : "plan";
11036
11100
  const lines = [
11037
11101
  {
11038
11102
  prefix: "\u25A3 ",
@@ -11137,9 +11201,38 @@ async function runTuiApp(opts) {
11137
11201
  const viewPrefs = {
11138
11202
  showThoughts: config.tui.showThoughts
11139
11203
  };
11204
+ let altScreenEngaged = false;
11205
+ const enterAltScreen = () => {
11206
+ if (altScreenEngaged) {
11207
+ return;
11208
+ }
11209
+ term.fullscreen(true);
11210
+ altScreenEngaged = true;
11211
+ };
11212
+ const leaveAltScreen = () => {
11213
+ if (!altScreenEngaged) {
11214
+ return;
11215
+ }
11216
+ term.fullscreen(false);
11217
+ altScreenEngaged = false;
11218
+ process.stdout.write("\n");
11219
+ };
11220
+ enterAltScreen();
11221
+ const altScreenCleanup = () => {
11222
+ if (altScreenEngaged) {
11223
+ term.fullscreen(false);
11224
+ altScreenEngaged = false;
11225
+ }
11226
+ };
11227
+ process.once("exit", altScreenCleanup);
11140
11228
  let nextOpts = opts;
11141
- while (nextOpts !== null) {
11142
- nextOpts = await runSession(term, config, target, nextOpts, exitHint, viewPrefs);
11229
+ try {
11230
+ while (nextOpts !== null) {
11231
+ nextOpts = await runSession(term, config, target, nextOpts, exitHint, viewPrefs);
11232
+ }
11233
+ } finally {
11234
+ leaveAltScreen();
11235
+ process.off("exit", altScreenCleanup);
11143
11236
  }
11144
11237
  const pendingUpdate = await getPendingUpdate();
11145
11238
  if (pendingUpdate) {
@@ -11149,7 +11242,6 @@ async function runTuiApp(opts) {
11149
11242
  if (exitHint.sessionId && process.stdout.isTTY) {
11150
11243
  const short = stripHydraSessionPrefix(exitHint.sessionId);
11151
11244
  const flags = exitHint.readonly ? " --readonly" : "";
11152
- process.stdout.write("\x1B[2J\x1B[H");
11153
11245
  process.stdout.write(
11154
11246
  `To resume: ${invokedBinName()} tui --session ${short}${flags}
11155
11247
  `
@@ -11160,7 +11252,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
11160
11252
  const ctx = await resolveSession(term, config, target, opts);
11161
11253
  if (!ctx) {
11162
11254
  term.grabInput(false);
11163
- process.exit(0);
11255
+ return null;
11164
11256
  }
11165
11257
  const launchLabelBase = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
11166
11258
  const installStatus = createInstallStatusLine(term, launchLabelBase);
@@ -11656,9 +11748,35 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
11656
11748
  initialQueue = hydraMeta.queue;
11657
11749
  }
11658
11750
  const historyFile = paths.tuiHistoryFile(resolvedSessionId);
11751
+ const globalHistoryFile = paths.globalTuiHistoryFile();
11659
11752
  let history = await loadHistory(historyFile).catch(() => []);
11660
- const dispatcher = new InputDispatcher({ history });
11753
+ let globalHistory = await loadHistory(globalHistoryFile).catch(() => []);
11754
+ if (globalHistory.length > GLOBAL_HISTORY_CAP) {
11755
+ globalHistory = globalHistory.slice(globalHistory.length - GLOBAL_HISTORY_CAP);
11756
+ }
11757
+ const dispatcher = new InputDispatcher({
11758
+ history: buildCombinedHistory(globalHistory, history)
11759
+ });
11661
11760
  dispatcherRef = dispatcher;
11761
+ const recordHistoryEntry = (entry) => {
11762
+ const trimmed = entry.replace(/\n+$/, "");
11763
+ if (trimmed.length === 0) {
11764
+ return;
11765
+ }
11766
+ const nextSession = appendEntry(history, trimmed);
11767
+ const sessionChanged = nextSession !== history;
11768
+ history = nextSession;
11769
+ const nextGlobal = appendEntry(globalHistory, trimmed, GLOBAL_HISTORY_CAP);
11770
+ const globalChanged = nextGlobal !== globalHistory;
11771
+ globalHistory = nextGlobal;
11772
+ dispatcher.setHistory(buildCombinedHistory(globalHistory, history));
11773
+ if (sessionChanged) {
11774
+ saveHistory(historyFile, history).catch(() => void 0);
11775
+ }
11776
+ if (globalChanged) {
11777
+ appendHistoryLine(globalHistoryFile, trimmed).catch(() => void 0);
11778
+ }
11779
+ };
11662
11780
  if (pendingTurns > 0) {
11663
11781
  dispatcher.setTurnRunning(true);
11664
11782
  }
@@ -11867,7 +11985,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
11867
11985
  const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
11868
11986
  const usage = { ...initialUsage ?? {} };
11869
11987
  installStatus.finalize();
11870
- screen.start();
11988
+ screen.start({ skipFullscreen: true });
11871
11989
  screen.setHideThoughts(!viewPrefs.showThoughts);
11872
11990
  screen.setSessionbar({
11873
11991
  agent: sessionbarAgent,
@@ -12015,7 +12133,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12015
12133
  sessionElapsedTimer = null;
12016
12134
  }
12017
12135
  screen.clearWindowTitle();
12018
- screen.stop();
12136
+ screen.stop({ keepFullscreen: true });
12019
12137
  saveHistory(historyFile, history).catch(() => void 0);
12020
12138
  void stream.close().catch(() => void 0);
12021
12139
  };
@@ -12035,8 +12153,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12035
12153
  }
12036
12154
  const pendingDraft = dispatcher.state().buffer.join("\n");
12037
12155
  if (pendingDraft.replace(/\s+$/, "").length > 0) {
12038
- history = appendEntry(history, pendingDraft);
12039
- dispatcher.setHistory(history);
12156
+ recordHistoryEntry(pendingDraft);
12040
12157
  }
12041
12158
  screen.pauseRepaint();
12042
12159
  screen.stop({ keepFullscreen: true });
@@ -12415,9 +12532,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12415
12532
  if (handleBuiltinCommand(text)) {
12416
12533
  return;
12417
12534
  }
12418
- history = appendEntry(history, text);
12419
- dispatcher.setHistory(history);
12420
- saveHistory(historyFile, history).catch(() => void 0);
12535
+ recordHistoryEntry(text);
12421
12536
  void runPrompt(text, attachments);
12422
12537
  };
12423
12538
  const amendPrompt = (text, attachments) => {
@@ -12425,9 +12540,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12425
12540
  if (handleBuiltinCommand(text)) {
12426
12541
  return;
12427
12542
  }
12428
- history = appendEntry(history, text);
12429
- dispatcher.setHistory(history);
12430
- saveHistory(historyFile, history).catch(() => void 0);
12543
+ recordHistoryEntry(text);
12431
12544
  if (!daemonSupportsAmend || currentHeadMessageId === void 0) {
12432
12545
  void runPrompt(text, attachments);
12433
12546
  return;
@@ -12787,16 +12900,18 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12787
12900
  const end = toolsBlockEndedAt ?? Date.now();
12788
12901
  const elapsed = end - toolsBlockStartedAt;
12789
12902
  const stoppedReason = !inProgress && toolsBlockStopReason !== null && toolsBlockStopReason !== "end_turn" ? toolsBlockStopReason : null;
12903
+ const isAmended = stoppedReason === "amended";
12904
+ const stoppedLabel = isAmended ? `amended \xB7 ${formatElapsed(elapsed)}` : `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}`;
12790
12905
  let summary;
12791
12906
  if (total === 0) {
12792
12907
  if (stoppedReason !== null) {
12793
- summary = `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}`;
12908
+ summary = stoppedLabel;
12794
12909
  } else {
12795
12910
  summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
12796
12911
  }
12797
12912
  } else {
12798
12913
  const noun = total === 1 ? "tool" : "tools";
12799
- const timing = stoppedReason !== null ? `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}` : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
12914
+ const timing = stoppedReason !== null ? stoppedLabel : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
12800
12915
  const parts = [`${total} ${noun}`, timing];
12801
12916
  if (inProgress) {
12802
12917
  if (hidden > 0) {
@@ -12808,8 +12923,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12808
12923
  summary = parts.join(" \xB7 ");
12809
12924
  }
12810
12925
  const pureThinking = total === 0 && inProgress;
12811
- const frozenStyle = stoppedReason !== null ? "tool-status-fail" : "tool";
12812
- const frozenBodyStyle = stoppedReason !== null ? "tool-status-fail" : "dim";
12926
+ const stoppedHeaderStyle = isAmended ? "tool-status-cancelled" : "tool-status-fail";
12927
+ const frozenStyle = stoppedReason !== null ? stoppedHeaderStyle : "tool";
12928
+ const frozenBodyStyle = stoppedReason !== null ? stoppedHeaderStyle : "dim";
12813
12929
  const lines = [
12814
12930
  {
12815
12931
  prefix: "\u2699 ",
@@ -12985,7 +13101,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12985
13101
  effectiveStopReason = "error";
12986
13102
  }
12987
13103
  if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
12988
- const lines = formatEvent({ ...lastPlanEvent, stopped: true });
13104
+ const lines = formatEvent({
13105
+ ...lastPlanEvent,
13106
+ stopped: true,
13107
+ amended: event.amended === true
13108
+ });
12989
13109
  if (lines.length > 0) {
12990
13110
  screen.upsertLines("plan", lines);
12991
13111
  }
@@ -12997,7 +13117,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12997
13117
  toolsBlockStopReason = effectiveStopReason ?? null;
12998
13118
  renderToolsBlock();
12999
13119
  screen.clearKey("tools");
13000
- } else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
13120
+ } else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn" && effectiveStopReason !== "amended") {
13001
13121
  screen.appendLines([
13002
13122
  {
13003
13123
  prefix: "\u26A0 ",
@@ -14693,6 +14813,12 @@ var SessionRecord = z4.object({
14693
14813
  agentCommands: z4.array(PersistedAgentCommand).optional(),
14694
14814
  agentModes: z4.array(PersistedAgentMode).optional(),
14695
14815
  agentModels: z4.array(PersistedAgentModel).optional(),
14816
+ // One-shot flag set when `hydra agent sync` mints a row from an
14817
+ // agent-side session/list entry: signals that the first resurrect
14818
+ // should *keep* the agent's session/load replay (instead of draining
14819
+ // it) so the local history.jsonl gets populated from the agent's
14820
+ // memory. Cleared after that first resurrect completes.
14821
+ pendingHistorySync: z4.boolean().optional(),
14696
14822
  createdAt: z4.string(),
14697
14823
  updatedAt: z4.string()
14698
14824
  });
@@ -14813,6 +14939,7 @@ function recordFromMemorySession(args) {
14813
14939
  agentCommands: args.agentCommands,
14814
14940
  agentModes: args.agentModes,
14815
14941
  agentModels: args.agentModels,
14942
+ pendingHistorySync: args.pendingHistorySync,
14816
14943
  createdAt: args.createdAt ?? now,
14817
14944
  updatedAt: args.updatedAt ?? now
14818
14945
  };
@@ -15118,7 +15245,13 @@ var SessionManager = class {
15118
15245
  await agent.kill().catch(() => void 0);
15119
15246
  return this.doResurrectFromImport(params);
15120
15247
  }
15121
- agent.connection.drainBuffered("session/update");
15248
+ if (params.pendingHistorySync === true) {
15249
+ void this.clearPendingHistorySync(params.hydraSessionId).catch(
15250
+ () => void 0
15251
+ );
15252
+ } else {
15253
+ agent.connection.drainBuffered("session/update");
15254
+ }
15122
15255
  const session = new Session({
15123
15256
  sessionId: params.hydraSessionId,
15124
15257
  cwd: params.cwd,
@@ -15208,6 +15341,133 @@ var SessionManager = class {
15208
15341
  }
15209
15342
  return os3.homedir();
15210
15343
  }
15344
+ // Pull every session the agent itself remembers (across all cwds) and
15345
+ // persist a cold hydra record for each one we don't already track.
15346
+ // Used by `hydra agent sync <id>` to surface sessions created outside
15347
+ // hydra — or by other tools — in `hydra session list` so the picker
15348
+ // can resurrect them. Spawns a throwaway agent process for the
15349
+ // initialize + session/list pair, then kills it. Records are minted
15350
+ // with pendingHistorySync:true so the first resurrect records the
15351
+ // agent's session/load replay into history.jsonl rather than dropping
15352
+ // it.
15353
+ async syncFromAgent(agentId) {
15354
+ const agentDef = await this.registry.getAgent(agentId);
15355
+ if (!agentDef) {
15356
+ const err = new Error(
15357
+ `agent ${agentId} not found in registry`
15358
+ );
15359
+ err.code = JsonRpcErrorCodes.AgentNotInstalled;
15360
+ throw err;
15361
+ }
15362
+ const plan = await planSpawn(agentDef, [], {
15363
+ npmRegistry: this.npmRegistry
15364
+ });
15365
+ const agent = this.spawner({
15366
+ agentId,
15367
+ cwd: os3.homedir(),
15368
+ plan
15369
+ });
15370
+ let initResult;
15371
+ try {
15372
+ initResult = await agent.connection.request(
15373
+ "initialize",
15374
+ {
15375
+ protocolVersion: ACP_PROTOCOL_VERSION,
15376
+ clientCapabilities: {},
15377
+ clientInfo: { name: "hydra", version: HYDRA_VERSION }
15378
+ }
15379
+ );
15380
+ } catch (err) {
15381
+ await agent.kill().catch(() => void 0);
15382
+ throw err;
15383
+ }
15384
+ const caps = initResult.agentCapabilities ?? {};
15385
+ if (caps.sessionCapabilities?.list === void 0) {
15386
+ await agent.kill().catch(() => void 0);
15387
+ throw new Error(
15388
+ `agent ${agentId} does not advertise sessionCapabilities.list; cannot sync`
15389
+ );
15390
+ }
15391
+ let entries;
15392
+ try {
15393
+ entries = await this.collectAgentSessions(agent);
15394
+ } catch (err) {
15395
+ await agent.kill().catch(() => void 0);
15396
+ throw err;
15397
+ }
15398
+ await agent.kill().catch(() => void 0);
15399
+ const existing = /* @__PURE__ */ new Set();
15400
+ for (const live of this.sessions.values()) {
15401
+ existing.add(`${live.agentId}::${live.upstreamSessionId}`);
15402
+ }
15403
+ const stored = await this.store.list().catch(() => []);
15404
+ for (const rec of stored) {
15405
+ existing.add(`${rec.agentId}::${rec.upstreamSessionId}`);
15406
+ }
15407
+ const synced = [];
15408
+ let skipped = 0;
15409
+ for (const entry of entries) {
15410
+ const dedupeKey = `${agentId}::${entry.sessionId}`;
15411
+ if (existing.has(dedupeKey)) {
15412
+ skipped += 1;
15413
+ continue;
15414
+ }
15415
+ existing.add(dedupeKey);
15416
+ const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
15417
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15418
+ const ts = entry.updatedAt ?? now;
15419
+ const recordArgs = {
15420
+ sessionId: newId,
15421
+ lineageId: generateLineageId(),
15422
+ upstreamSessionId: entry.sessionId,
15423
+ agentId,
15424
+ cwd: entry.cwd,
15425
+ pendingHistorySync: true,
15426
+ createdAt: ts,
15427
+ updatedAt: ts
15428
+ };
15429
+ if (entry.title !== void 0) {
15430
+ recordArgs.title = entry.title;
15431
+ }
15432
+ const record = recordFromMemorySession(recordArgs);
15433
+ await this.store.write(record);
15434
+ synced.push({ version: 1, ...record });
15435
+ }
15436
+ return { synced, skipped };
15437
+ }
15438
+ // Paginate the agent's session/list, threading nextCursor until the
15439
+ // agent stops returning one. Each entry the spec guarantees has
15440
+ // { sessionId, cwd }; title and updatedAt are optional.
15441
+ async collectAgentSessions(agent) {
15442
+ const out = [];
15443
+ let cursor;
15444
+ for (let page = 0; page < 100; page += 1) {
15445
+ const params = {};
15446
+ if (cursor !== void 0) {
15447
+ params.cursor = cursor;
15448
+ }
15449
+ const result = await agent.connection.request("session/list", params);
15450
+ const rows = Array.isArray(result.sessions) ? result.sessions : [];
15451
+ for (const row of rows) {
15452
+ if (typeof row.sessionId !== "string" || typeof row.cwd !== "string") {
15453
+ continue;
15454
+ }
15455
+ const entry = { sessionId: row.sessionId, cwd: row.cwd };
15456
+ if (typeof row.title === "string") {
15457
+ entry.title = row.title;
15458
+ }
15459
+ if (typeof row.updatedAt === "string") {
15460
+ entry.updatedAt = row.updatedAt;
15461
+ }
15462
+ out.push(entry);
15463
+ }
15464
+ if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
15465
+ break;
15466
+ }
15467
+ cursor = result.nextCursor;
15468
+ }
15469
+ return out;
15470
+ }
15211
15471
  // Bootstrap a fresh agent process: registry resolve → spawn → initialize
15212
15472
  // → session/new. Shared by create() and the /hydra agent path so both
15213
15473
  // go through the same env / capabilities / error-handling.
@@ -15400,9 +15660,21 @@ var SessionManager = class {
15400
15660
  agentCommands: record.agentCommands,
15401
15661
  agentModes: record.agentModes,
15402
15662
  agentModels: record.agentModels,
15403
- createdAt: record.createdAt
15663
+ createdAt: record.createdAt,
15664
+ pendingHistorySync: record.pendingHistorySync
15404
15665
  };
15405
15666
  }
15667
+ async clearPendingHistorySync(sessionId) {
15668
+ await this.enqueueMetaWrite(sessionId, async () => {
15669
+ const record = await this.store.read(sessionId);
15670
+ if (!record || record.pendingHistorySync !== true) {
15671
+ return;
15672
+ }
15673
+ const next = { ...record };
15674
+ delete next.pendingHistorySync;
15675
+ await this.store.write(next);
15676
+ });
15677
+ }
15406
15678
  // Best-effort: peek at the persisted history's first prompt and use
15407
15679
  // its first line (capped to 200 chars) as a session title. Returns
15408
15680
  // undefined if no usable prompt is found or any I/O fails.
@@ -15488,7 +15760,8 @@ var SessionManager = class {
15488
15760
  currentUsage: session.currentUsage,
15489
15761
  updatedAt: used,
15490
15762
  attachedClients: session.attachedCount,
15491
- status: "live"
15763
+ status: "live",
15764
+ busy: session.turnStartedAt !== void 0
15492
15765
  });
15493
15766
  }
15494
15767
  const records = await this.store.list().catch(() => []);
@@ -15512,7 +15785,8 @@ var SessionManager = class {
15512
15785
  importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
15513
15786
  updatedAt: used,
15514
15787
  attachedClients: 0,
15515
- status: "cold"
15788
+ status: "cold",
15789
+ busy: false
15516
15790
  });
15517
15791
  }
15518
15792
  entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
@@ -17451,7 +17725,8 @@ function registerSessionRoutes(app, manager, defaults) {
17451
17725
  }
17452
17726
 
17453
17727
  // src/daemon/routes/agents.ts
17454
- function registerAgentRoutes(app, registry) {
17728
+ init_types();
17729
+ function registerAgentRoutes(app, registry, manager, opts = {}) {
17455
17730
  app.get("/v1/agents", async () => {
17456
17731
  const doc = await registry.load();
17457
17732
  return {
@@ -17472,6 +17747,61 @@ function registerAgentRoutes(app, registry) {
17472
17747
  const doc = await registry.refresh();
17473
17748
  return { version: doc.version, agentCount: doc.agents.length };
17474
17749
  });
17750
+ app.post("/v1/agents/:id/install", async (request, reply) => {
17751
+ const id = request.params.id;
17752
+ const agent = await registry.getAgent(id);
17753
+ if (!agent) {
17754
+ reply.code(404).send({ error: `agent ${id} not found in registry` });
17755
+ return;
17756
+ }
17757
+ if (agent.distribution.uvx && !agent.distribution.npx && !agent.distribution.binary) {
17758
+ reply.send({
17759
+ agentId: agent.id,
17760
+ version: agent.version ?? "current",
17761
+ distribution: "uvx",
17762
+ installed: false,
17763
+ message: "uvx agents resolve on first run; nothing to pre-install."
17764
+ });
17765
+ return;
17766
+ }
17767
+ try {
17768
+ const plan = await planSpawn(agent, [], { npmRegistry: opts.npmRegistry });
17769
+ const distribution = agent.distribution.npx ? "npx" : agent.distribution.binary ? "binary" : "unknown";
17770
+ reply.send({
17771
+ agentId: agent.id,
17772
+ version: plan.version,
17773
+ distribution,
17774
+ installed: true,
17775
+ command: plan.command
17776
+ });
17777
+ } catch (err) {
17778
+ reply.code(500).send({ error: err.message });
17779
+ }
17780
+ });
17781
+ app.post("/v1/agents/:id/sync", async (request, reply) => {
17782
+ const agentId = request.params.id;
17783
+ try {
17784
+ const { synced, skipped } = await manager.syncFromAgent(agentId);
17785
+ return {
17786
+ synced: synced.map((r) => ({
17787
+ sessionId: r.sessionId,
17788
+ upstreamSessionId: r.upstreamSessionId,
17789
+ agentId: r.agentId,
17790
+ cwd: r.cwd,
17791
+ title: r.title,
17792
+ updatedAt: r.updatedAt
17793
+ })),
17794
+ skipped
17795
+ };
17796
+ } catch (err) {
17797
+ const e = err;
17798
+ if (e.code === JsonRpcErrorCodes.AgentNotInstalled) {
17799
+ reply.code(404).send({ error: e.message });
17800
+ return;
17801
+ }
17802
+ reply.code(409).send({ error: e.message });
17803
+ }
17804
+ });
17475
17805
  }
17476
17806
 
17477
17807
  // src/daemon/routes/health.ts
@@ -18542,7 +18872,7 @@ async function startDaemon(config, serviceToken) {
18542
18872
  agentId: config.defaultAgent,
18543
18873
  cwd: config.defaultCwd
18544
18874
  });
18545
- registerAgentRoutes(app, registry);
18875
+ registerAgentRoutes(app, registry, manager, { npmRegistry: config.npmRegistry });
18546
18876
  registerExtensionRoutes(app, extensions);
18547
18877
  registerConfigRoutes(app, {
18548
18878
  defaultAgent: config.defaultAgent,
@@ -19886,6 +20216,136 @@ async function runAgentsList() {
19886
20216
  Registry version: ${body.version}
19887
20217
  `);
19888
20218
  }
20219
+ async function runAgentsInstall(agentId) {
20220
+ if (!agentId) {
20221
+ process.stderr.write("Usage: hydra-acp agent install <agent-id>\n");
20222
+ process.exit(2);
20223
+ return;
20224
+ }
20225
+ const config = await loadConfig();
20226
+ const serviceToken = await loadServiceToken();
20227
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
20228
+ process.stdout.write(`Installing ${agentId}\u2026
20229
+ `);
20230
+ let body;
20231
+ try {
20232
+ const r = await fetch(
20233
+ `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/install`,
20234
+ {
20235
+ method: "POST",
20236
+ headers: { Authorization: `Bearer ${serviceToken}` }
20237
+ }
20238
+ );
20239
+ if (!r.ok) {
20240
+ let detail = `HTTP ${r.status}`;
20241
+ try {
20242
+ const j = await r.json();
20243
+ if (j.error) {
20244
+ detail = j.error;
20245
+ }
20246
+ } catch {
20247
+ }
20248
+ process.stderr.write(`hydra agent install ${agentId}: ${detail}
20249
+ `);
20250
+ process.exit(1);
20251
+ }
20252
+ body = await r.json();
20253
+ } catch (err) {
20254
+ process.stderr.write(
20255
+ `Could not reach daemon at ${baseUrl}: ${err.message}
20256
+ `
20257
+ );
20258
+ process.exit(1);
20259
+ return;
20260
+ }
20261
+ if (!body.installed) {
20262
+ process.stdout.write(
20263
+ `${body.agentId} (${body.version}, ${body.distribution}): ${body.message ?? "nothing to install"}
20264
+ `
20265
+ );
20266
+ return;
20267
+ }
20268
+ process.stdout.write(
20269
+ `Installed ${body.agentId} (${body.version}, ${body.distribution})
20270
+ `
20271
+ );
20272
+ if (body.command) {
20273
+ process.stdout.write(` \u2192 ${body.command}
20274
+ `);
20275
+ }
20276
+ }
20277
+ async function runAgentsSync(agentId) {
20278
+ if (!agentId) {
20279
+ process.stderr.write("Usage: hydra-acp agent sync <agent-id>\n");
20280
+ process.exit(2);
20281
+ return;
20282
+ }
20283
+ const config = await loadConfig();
20284
+ const serviceToken = await loadServiceToken();
20285
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
20286
+ let body;
20287
+ try {
20288
+ const r = await fetch(`${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sync`, {
20289
+ method: "POST",
20290
+ headers: { Authorization: `Bearer ${serviceToken}` }
20291
+ });
20292
+ if (!r.ok) {
20293
+ let detail = `HTTP ${r.status}`;
20294
+ try {
20295
+ const j = await r.json();
20296
+ if (j.error) {
20297
+ detail = j.error;
20298
+ }
20299
+ } catch {
20300
+ }
20301
+ process.stderr.write(`hydra agent sync ${agentId}: ${detail}
20302
+ `);
20303
+ process.exit(1);
20304
+ }
20305
+ body = await r.json();
20306
+ } catch (err) {
20307
+ process.stderr.write(
20308
+ `Could not reach daemon at ${baseUrl}: ${err.message}
20309
+ `
20310
+ );
20311
+ process.exit(1);
20312
+ return;
20313
+ }
20314
+ if (body.synced.length === 0) {
20315
+ process.stdout.write(
20316
+ `Nothing new to sync (${body.skipped} already tracked).
20317
+ `
20318
+ );
20319
+ return;
20320
+ }
20321
+ const rows = body.synced.map((s) => ({
20322
+ id: s.sessionId,
20323
+ upstream: s.upstreamSessionId,
20324
+ cwd: s.cwd,
20325
+ title: s.title ?? "-"
20326
+ }));
20327
+ const header = { id: "ID", upstream: "UPSTREAM", cwd: "CWD", title: "TITLE" };
20328
+ const widths = {
20329
+ id: maxLen3(header.id, rows.map((r) => r.id)),
20330
+ upstream: maxLen3(header.upstream, rows.map((r) => r.upstream)),
20331
+ cwd: maxLen3(header.cwd, rows.map((r) => r.cwd))
20332
+ };
20333
+ const fmt = (r) => [
20334
+ r.id.padEnd(widths.id),
20335
+ r.upstream.padEnd(widths.upstream),
20336
+ r.cwd.padEnd(widths.cwd),
20337
+ r.title
20338
+ ].join(" ");
20339
+ process.stdout.write(fmt(header) + "\n");
20340
+ for (const r of rows) {
20341
+ process.stdout.write(fmt(r) + "\n");
20342
+ }
20343
+ process.stdout.write(
20344
+ `
20345
+ Synced ${body.synced.length} session(s); skipped ${body.skipped} already tracked.
20346
+ `
20347
+ );
20348
+ }
19889
20349
  async function runAgentsRefresh() {
19890
20350
  const config = await loadConfig();
19891
20351
  const serviceToken = await loadServiceToken();
@@ -20942,7 +21402,11 @@ async function main() {
20942
21402
  const daemonIdx = argv.indexOf("daemon");
20943
21403
  const tail = argv.slice(daemonIdx + 1);
20944
21404
  const sub = tail[0];
20945
- if (sub === "start" || sub === void 0) {
21405
+ if (sub === void 0 || sub === "status") {
21406
+ await runDaemonStatus();
21407
+ return;
21408
+ }
21409
+ if (sub === "start") {
20946
21410
  await runDaemonStart(flags);
20947
21411
  return;
20948
21412
  }
@@ -20954,10 +21418,6 @@ async function main() {
20954
21418
  await runDaemonRestart();
20955
21419
  return;
20956
21420
  }
20957
- if (sub === "status") {
20958
- await runDaemonStatus();
20959
- return;
20960
- }
20961
21421
  if (sub === "logs") {
20962
21422
  await runDaemonLogs(tail.slice(1));
20963
21423
  return;
@@ -21070,6 +21530,14 @@ async function main() {
21070
21530
  await runAgentsRefresh();
21071
21531
  return;
21072
21532
  }
21533
+ if (sub === "install") {
21534
+ await runAgentsInstall(positional[2]);
21535
+ return;
21536
+ }
21537
+ if (sub === "sync") {
21538
+ await runAgentsSync(positional[2]);
21539
+ return;
21540
+ }
21073
21541
  process.stderr.write(`Unknown agent subcommand: ${sub}
21074
21542
  `);
21075
21543
  process.exit(2);
@@ -21226,8 +21694,9 @@ function printHelp() {
21226
21694
  " --readonly Open a session as a transcript viewer (requires --session).",
21227
21695
  " HYDRA_ACP_SESSION Env var equivalent of --session (flag wins).",
21228
21696
  " hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
21697
+ " hydra-acp daemon [status] Show daemon pid/version (default when no subcommand)",
21229
21698
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
21230
- " hydra-acp daemon stop|restart|status",
21699
+ " hydra-acp daemon stop|restart",
21231
21700
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
21232
21701
  " hydra-acp session [list] [--all] [--json] [--host=<host>]",
21233
21702
  " List sessions (live + 20 most-recent cold; --all for everything; --json emits JSON for scripts).",
@@ -21249,6 +21718,8 @@ function printHelp() {
21249
21718
  " hydra-acp extension logs <name> [-f] [-n N] Tail or follow an extension's log",
21250
21719
  " hydra-acp agent [list] List agents in the cached registry",
21251
21720
  " hydra-acp agent refresh Force a registry re-fetch",
21721
+ " hydra-acp agent install <id> Pre-install <id> from the registry (else lazy on first session)",
21722
+ " hydra-acp agent sync <id> Spawn <id> just long enough to ACP session/list it, then persist any sessions it remembers (across every cwd) as cold rows in `session list`",
21252
21723
  " hydra-acp auth password [--force] Set the daemon's master password",
21253
21724
  " hydra-acp auth [list] List active session tokens",
21254
21725
  " hydra-acp auth revoke <id> Revoke a session token",