@hydra-acp/cli 0.1.56 → 0.1.58

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
@@ -108,7 +108,11 @@ var init_paths = __esm({
108
108
  // line, append-only so concurrent TUIs don't lose each other's
109
109
  // writes.
110
110
  globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
111
- tuiLogFile: () => path.join(hydraHome(), "tui.log")
111
+ tuiLogFile: () => path.join(hydraHome(), "tui.log"),
112
+ // Diagnostic dump of every JSON-RPC message that crosses a `hydra-acp
113
+ // shim` process. Append-only NDJSON. One file shared by every shim;
114
+ // each line carries the writing process's pid for disambiguation.
115
+ shimWireLogFile: () => path.join(hydraHome(), "shim-wire.log")
112
116
  };
113
117
  }
114
118
  });
@@ -1025,6 +1029,12 @@ function extractHydraMeta(meta) {
1025
1029
  if (typeof obj.mcpStdin === "boolean") {
1026
1030
  out.mcpStdin = obj.mcpStdin;
1027
1031
  }
1032
+ if (typeof obj.interactive === "boolean") {
1033
+ out.interactive = obj.interactive;
1034
+ }
1035
+ if (typeof obj.ancillary === "boolean") {
1036
+ out.ancillary = obj.ancillary;
1037
+ }
1028
1038
  if (typeof obj.promptAmending === "boolean") {
1029
1039
  out.promptAmending = obj.promptAmending;
1030
1040
  }
@@ -1269,10 +1279,14 @@ var init_types = __esm({
1269
1279
  // local session, an import is a cross-machine takeover.
1270
1280
  forkedFromSessionId: z3.string().optional(),
1271
1281
  forkedFromMessageId: z3.string().optional(),
1272
- // clientInfo from the process that issued session/new. Lets list views
1273
- // hide cat-style ancillary sessions by default while letting an
1274
- // override flag surface them.
1282
+ // clientInfo from the process that issued session/new. Carried for
1283
+ // log/display; the effective filtering signal is `interactive` below.
1275
1284
  originatingClient: z3.object({ name: z3.string(), version: z3.string().optional() }).optional(),
1285
+ // Tristate filter signal computed by effectiveInteractive(): explicit
1286
+ // when the record stored a value, else inferred (legacy cat hint or
1287
+ // history-presence). Clients can use this to render a hint glyph
1288
+ // (e.g. dim non-interactive rows when the user toggles them in).
1289
+ interactive: z3.boolean().optional(),
1276
1290
  updatedAt: z3.string(),
1277
1291
  attachedClients: z3.number().int().nonnegative(),
1278
1292
  status: z3.enum(["live", "cold"]).default("live"),
@@ -1295,7 +1309,10 @@ var init_types = __esm({
1295
1309
  });
1296
1310
  SessionPromptParams = z3.object({
1297
1311
  sessionId: z3.string(),
1298
- prompt: z3.array(z3.unknown())
1312
+ prompt: z3.array(z3.unknown()),
1313
+ // Hydra extensions ride under _meta["hydra-acp"] (e.g. `ancillary` to
1314
+ // mark a non-promoting turn). Kept so Session.prompt can read them.
1315
+ _meta: z3.record(z3.unknown()).optional()
1299
1316
  });
1300
1317
  SessionCancelParams = z3.object({
1301
1318
  sessionId: z3.string()
@@ -2102,12 +2119,6 @@ var init_stream_buffer = __esm({
2102
2119
  });
2103
2120
 
2104
2121
  // src/core/hydra-commands.ts
2105
- function hydraCommandsAsAdvertised() {
2106
- return HYDRA_COMMANDS.map((c) => ({
2107
- name: c.argsHint ? `${c.name} ${c.argsHint}` : c.name,
2108
- description: c.description
2109
- }));
2110
- }
2111
2122
  var HYDRA_COMMANDS, VERB_INDEX;
2112
2123
  var init_hydra_commands = __esm({
2113
2124
  "src/core/hydra-commands.ts"() {
@@ -2628,6 +2639,12 @@ var init_session = __esm({
2628
2639
  forkedFromSessionId;
2629
2640
  forkedFromMessageId;
2630
2641
  originatingClient;
2642
+ // Tristate. Mutates from undefined → true on first prompt (or directly
2643
+ // false if init.interactive === false). Persisted via interactiveHandlers.
2644
+ _interactive;
2645
+ get interactive() {
2646
+ return this._interactive;
2647
+ }
2631
2648
  title;
2632
2649
  // Snapshot state delivered to attaching clients via the attach
2633
2650
  // response _meta rather than via history replay (which would be
@@ -2756,6 +2773,7 @@ var init_session = __esm({
2756
2773
  agentModelsHandlers = [];
2757
2774
  modelHandlers = [];
2758
2775
  modeHandlers = [];
2776
+ interactiveHandlers = [];
2759
2777
  usageHandlers = [];
2760
2778
  cumulativeCost = 0;
2761
2779
  // Total cost across all agent lives. costAmount in the returned snapshot
@@ -2839,6 +2857,7 @@ var init_session = __esm({
2839
2857
  if (init.firstPromptSeeded) {
2840
2858
  this._firstPromptSeeded = true;
2841
2859
  }
2860
+ this._interactive = init.interactive;
2842
2861
  this.historyStore = init.historyStore;
2843
2862
  this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
2844
2863
  this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
@@ -3101,19 +3120,25 @@ var init_session = __esm({
3101
3120
  return this.loadReplay(historyPolicy, opts);
3102
3121
  }
3103
3122
  async loadReplay(historyPolicy, opts) {
3104
- const all = coalesceReplay(await this.getHistorySnapshot());
3123
+ const raw = await this.getHistorySnapshot();
3105
3124
  const state = this.buildStateSnapshotReplay();
3106
3125
  if (historyPolicy === "after_message") {
3107
- const cutoff = opts.afterMessageId ? findMessageIdIndex(all, opts.afterMessageId) : -1;
3126
+ const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
3108
3127
  if (cutoff < 0) {
3109
- return { entries: [...state, ...all], appliedPolicy: "full" };
3128
+ return {
3129
+ entries: [...state, ...coalesceReplay(raw)],
3130
+ appliedPolicy: "full"
3131
+ };
3110
3132
  }
3111
3133
  return {
3112
- entries: [...state, ...all.slice(cutoff + 1)],
3134
+ entries: [...state, ...coalesceReplay(raw.slice(cutoff + 1))],
3113
3135
  appliedPolicy: "after_message"
3114
3136
  };
3115
3137
  }
3116
- return { entries: [...state, ...all], appliedPolicy: "full" };
3138
+ return {
3139
+ entries: [...state, ...coalesceReplay(raw)],
3140
+ appliedPolicy: "full"
3141
+ };
3117
3142
  }
3118
3143
  // Synthesizes one session/update notification per cached STATE_UPDATE_KIND
3119
3144
  // so an attaching client receives the current snapshot through the
@@ -3294,6 +3319,18 @@ var init_session = __esm({
3294
3319
  const messageId = generateMessageId();
3295
3320
  this.maybeSeedTitleFromPrompt(params);
3296
3321
  this._firstPromptSeeded = true;
3322
+ const ancillary = extractHydraMeta(
3323
+ (params ?? {})._meta
3324
+ ).ancillary === true;
3325
+ if (!ancillary && this._interactive === void 0) {
3326
+ this._interactive = true;
3327
+ for (const handler of this.interactiveHandlers) {
3328
+ try {
3329
+ handler(true);
3330
+ } catch {
3331
+ }
3332
+ }
3333
+ }
3297
3334
  return this.enqueueUserPrompt(client, params, messageId);
3298
3335
  }
3299
3336
  // DEVIATION FROM RFD #533: this broadcast is deliberately deferred
@@ -4076,13 +4113,7 @@ var init_session = __esm({
4076
4113
  this.logger?.info(
4077
4114
  `live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
4078
4115
  );
4079
- this.currentModel = trimmed;
4080
- for (const handler of this.modelHandlers) {
4081
- try {
4082
- handler(trimmed);
4083
- } catch {
4084
- }
4085
- }
4116
+ this.applyModelChange(trimmed);
4086
4117
  }
4087
4118
  }
4088
4119
  break;
@@ -4278,24 +4309,38 @@ var init_session = __esm({
4278
4309
  onModeChange(handler) {
4279
4310
  this.modeHandlers.push(handler);
4280
4311
  }
4312
+ onInteractiveChange(handler) {
4313
+ this.interactiveHandlers.push(handler);
4314
+ }
4281
4315
  // Apply a model change initiated by a client request (session/set_model)
4282
4316
  // when the agent doesn't emit a current_model_update notification, or
4283
4317
  // emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
4284
4318
  // (persistence) and broadcasts a synthetic current_model_update so all
4285
4319
  // attached clients — including the originator — repaint immediately.
4320
+ //
4321
+ // The broadcast fires even when `modelId` already equals currentModel.
4322
+ // claude-acp emits a stale current_model_update (with the pre-change
4323
+ // value) during set_model processing and a separate config_option_update
4324
+ // with the new value; the configOption path updates currentModel here
4325
+ // before our synthetic broadcast would run, so a value-equality guard
4326
+ // would suppress the corrective broadcast and leave attached clients
4327
+ // (notably the TUI, which doesn't render config_option_update) showing
4328
+ // the stale model from the agent's earlier notification.
4286
4329
  applyModelChange(modelId) {
4287
4330
  const trimmed = modelId.trim();
4288
- if (!trimmed || trimmed === this.currentModel) {
4331
+ if (!trimmed) {
4289
4332
  return;
4290
4333
  }
4291
- this.logger?.info(
4292
- `applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
4293
- );
4294
- this.currentModel = trimmed;
4295
- for (const handler of this.modelHandlers) {
4296
- try {
4297
- handler(trimmed);
4298
- } catch {
4334
+ if (trimmed !== this.currentModel) {
4335
+ this.logger?.info(
4336
+ `applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
4337
+ );
4338
+ this.currentModel = trimmed;
4339
+ for (const handler of this.modelHandlers) {
4340
+ try {
4341
+ handler(trimmed);
4342
+ } catch {
4343
+ }
4299
4344
  }
4300
4345
  }
4301
4346
  const update = {
@@ -4312,23 +4357,39 @@ var init_session = __esm({
4312
4357
  }
4313
4358
  // Apply a mode change initiated by a client request (session/set_mode)
4314
4359
  // when the agent doesn't emit a current_mode_update notification on its
4315
- // own. Fires modeHandlers so the persistence hook and any other listeners
4316
- // see the change, identical to the agent-notification path.
4360
+ // own. Fires modeHandlers (persistence) and broadcasts a synthetic
4361
+ // current_mode_update so all attached clients — including the originator
4362
+ // — repaint immediately, mirroring applyModelChange. Without the
4363
+ // broadcast, peer clients (e.g. the TUI when set_mode was issued by Zed
4364
+ // through the shim) would stay on the prior mode.
4317
4365
  applyModeChange(modeId) {
4318
4366
  const trimmed = modeId.trim();
4319
- if (!trimmed || trimmed === this.currentMode) {
4367
+ if (!trimmed) {
4320
4368
  return;
4321
4369
  }
4322
- this.logger?.info(
4323
- `applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
4324
- );
4325
- this.currentMode = trimmed;
4326
- for (const handler of this.modeHandlers) {
4327
- try {
4328
- handler(trimmed);
4329
- } catch {
4370
+ if (trimmed !== this.currentMode) {
4371
+ this.logger?.info(
4372
+ `applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
4373
+ );
4374
+ this.currentMode = trimmed;
4375
+ for (const handler of this.modeHandlers) {
4376
+ try {
4377
+ handler(trimmed);
4378
+ } catch {
4379
+ }
4330
4380
  }
4331
4381
  }
4382
+ const update = {
4383
+ sessionUpdate: "current_mode_update",
4384
+ currentModeId: trimmed
4385
+ };
4386
+ if (this.agentAdvertisedModes.length > 0) {
4387
+ update.availableModes = [...this.agentAdvertisedModes];
4388
+ }
4389
+ this.recordAndBroadcast("session/update", {
4390
+ sessionId: this.upstreamSessionId,
4391
+ update
4392
+ });
4332
4393
  }
4333
4394
  onUsageChange(handler) {
4334
4395
  this.usageHandlers.push(handler);
@@ -4340,8 +4401,8 @@ var init_session = __esm({
4340
4401
  // entries, then whatever the agent advertised.
4341
4402
  mergedAvailableCommands() {
4342
4403
  const out = [
4343
- ...hydraCommandsAsAdvertised(),
4344
- { name: "model <model-id>", description: "Switch model; omit arg to list available models" },
4404
+ { name: "hydra", description: "Hydra session command (kill, restart, title, agent <agent>)" },
4405
+ { name: "model", description: "Switch model; omit arg to list available models" },
4345
4406
  { name: "sessions", description: "List all sessions" },
4346
4407
  { name: "help", description: "Show available commands" }
4347
4408
  ];
@@ -6248,6 +6309,9 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
6248
6309
  if (opts.all) {
6249
6310
  url.searchParams.set("all", "true");
6250
6311
  }
6312
+ if (opts.includeNonInteractive) {
6313
+ url.searchParams.set("includeNonInteractive", "true");
6314
+ }
6251
6315
  const response = await fetchImpl(url.toString(), {
6252
6316
  headers: { Authorization: `Bearer ${target.token}` }
6253
6317
  });
@@ -6274,7 +6338,8 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
6274
6338
  forkedFromSessionId: s.forkedFromSessionId,
6275
6339
  forkedFromMessageId: s.forkedFromMessageId,
6276
6340
  busy: s.busy,
6277
- originatingClient: s.originatingClient
6341
+ originatingClient: s.originatingClient,
6342
+ interactive: s.interactive
6278
6343
  }));
6279
6344
  }
6280
6345
  async function forkSession(target, id, opts = {}, fetchImpl = fetch) {
@@ -6338,13 +6403,17 @@ async function regenSessionTitle(target, id, fetchImpl = fetch) {
6338
6403
  }
6339
6404
  }
6340
6405
  async function searchSessions(target, query, opts = {}, fetchImpl = fetch) {
6341
- const url = new URL(`${target.baseUrl}/v1/sessions/search`);
6342
- url.searchParams.set("q", query);
6406
+ const body = { q: query };
6343
6407
  if (opts.sessionIds && opts.sessionIds.length > 0) {
6344
- url.searchParams.set("sessionIds", opts.sessionIds.join(","));
6408
+ body.sessionIds = opts.sessionIds;
6345
6409
  }
6346
- const response = await fetchImpl(url.toString(), {
6347
- headers: { Authorization: `Bearer ${target.token}` }
6410
+ const response = await fetchImpl(`${target.baseUrl}/v1/sessions/search`, {
6411
+ method: "POST",
6412
+ headers: {
6413
+ Authorization: `Bearer ${target.token}`,
6414
+ "Content-Type": "application/json"
6415
+ },
6416
+ body: JSON.stringify(body)
6348
6417
  });
6349
6418
  if (!response.ok) {
6350
6419
  throw new Error(`daemon returned HTTP ${response.status}`);
@@ -7843,6 +7912,85 @@ var init_update_check = __esm({
7843
7912
  }
7844
7913
  });
7845
7914
 
7915
+ // src/core/cwd.ts
7916
+ import * as fs22 from "fs/promises";
7917
+ import * as path17 from "path";
7918
+ async function validateLocalCwd(input) {
7919
+ const trimmed = input.trim();
7920
+ if (trimmed.length === 0) {
7921
+ return { ok: false, reason: "path is empty" };
7922
+ }
7923
+ const resolved = path17.resolve(expandHome(trimmed));
7924
+ let stat5;
7925
+ try {
7926
+ stat5 = await fs22.stat(resolved);
7927
+ } catch {
7928
+ return { ok: false, reason: `${resolved} does not exist` };
7929
+ }
7930
+ if (!stat5.isDirectory()) {
7931
+ return { ok: false, reason: `${resolved} is not a directory` };
7932
+ }
7933
+ return { ok: true, path: resolved };
7934
+ }
7935
+ async function pickInitialLocalCwd(sessionCwd) {
7936
+ const candidates = [];
7937
+ const seen = /* @__PURE__ */ new Set();
7938
+ const push = (p) => {
7939
+ if (!seen.has(p)) {
7940
+ seen.add(p);
7941
+ candidates.push(p);
7942
+ }
7943
+ };
7944
+ push(sessionCwd);
7945
+ if (sessionCwd.startsWith("/Users/")) {
7946
+ push("/home/" + sessionCwd.slice("/Users/".length));
7947
+ } else if (sessionCwd.startsWith("/home/")) {
7948
+ push("/Users/" + sessionCwd.slice("/home/".length));
7949
+ }
7950
+ for (const candidate of candidates) {
7951
+ try {
7952
+ const stat5 = await fs22.stat(candidate);
7953
+ if (stat5.isDirectory()) {
7954
+ return candidate;
7955
+ }
7956
+ } catch {
7957
+ }
7958
+ }
7959
+ return null;
7960
+ }
7961
+ async function completeLocalPath(input) {
7962
+ const lastSlash = input.lastIndexOf("/");
7963
+ let prefix;
7964
+ let basePrefix;
7965
+ let dirForRead;
7966
+ if (lastSlash === -1) {
7967
+ prefix = "";
7968
+ basePrefix = input;
7969
+ dirForRead = ".";
7970
+ } else {
7971
+ prefix = input.slice(0, lastSlash + 1);
7972
+ basePrefix = input.slice(lastSlash + 1);
7973
+ dirForRead = lastSlash === 0 ? "/" : prefix;
7974
+ }
7975
+ const resolvedDir = path17.resolve(expandHome(dirForRead));
7976
+ let entries;
7977
+ try {
7978
+ const list = await fs22.readdir(resolvedDir, { withFileTypes: true });
7979
+ entries = list.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
7980
+ } catch {
7981
+ return { prefix, basePrefix, matches: [] };
7982
+ }
7983
+ const showHidden = basePrefix.startsWith(".");
7984
+ const matches = entries.filter((e) => e.name.startsWith(basePrefix)).filter((e) => showHidden || !e.name.startsWith(".")).map((e) => e.isDir ? `${e.name}/` : e.name).sort();
7985
+ return { prefix, basePrefix, matches };
7986
+ }
7987
+ var init_cwd = __esm({
7988
+ "src/core/cwd.ts"() {
7989
+ "use strict";
7990
+ init_config();
7991
+ }
7992
+ });
7993
+
7846
7994
  // src/tui/input.ts
7847
7995
  function formatPasteToken(id, lineCount) {
7848
7996
  return `[pasted #${id} +${lineCount} lines]`;
@@ -8104,12 +8252,18 @@ var init_input = __esm({
8104
8252
  return [];
8105
8253
  case "ctrl-c":
8106
8254
  return this.handleCtrlC();
8107
- case "ctrl-d":
8255
+ case "ctrl-d": {
8108
8256
  if (this.bufferIsEmpty()) {
8109
8257
  return [{ type: "exit" }];
8110
8258
  }
8259
+ const lastLine = this.buffer[this.buffer.length - 1] ?? "";
8260
+ const atEndOfBuffer = this.row === this.buffer.length - 1 && this.col >= lastLine.length;
8261
+ if (atEndOfBuffer) {
8262
+ return [{ type: "exit" }];
8263
+ }
8111
8264
  this.deleteForward();
8112
8265
  return [];
8266
+ }
8113
8267
  case "ctrl-l":
8114
8268
  return [{ type: "redraw" }];
8115
8269
  case "ctrl-p":
@@ -8787,9 +8941,9 @@ var init_input = __esm({
8787
8941
  });
8788
8942
 
8789
8943
  // src/tui/attachments.ts
8790
- import path17 from "path";
8944
+ import path18 from "path";
8791
8945
  function mimeFromExtension(p) {
8792
- return EXTENSION_TO_MIME[path17.extname(p).toLowerCase()] ?? null;
8946
+ return EXTENSION_TO_MIME[path18.extname(p).toLowerCase()] ?? null;
8793
8947
  }
8794
8948
  function isSupportedImagePath(p) {
8795
8949
  return mimeFromExtension(p) !== null;
@@ -10677,9 +10831,9 @@ uncaught: ${err.stack ?? err.message}
10677
10831
  this.permissionPrompt = spec ? { ...spec } : null;
10678
10832
  this.repaint();
10679
10833
  }
10680
- // Two-line confirmation modal that takes over the prompt area. Used to
10681
- // ask "interrupt before exit?" when the user quits during an in-flight
10682
- // turn that no one else is watching. Pass null to dismiss.
10834
+ // Two-line confirmation modal that takes over the prompt area. Pass
10835
+ // null to dismiss. Currently unused kept as a generic primitive for
10836
+ // any future modal that needs a question + hint footer.
10683
10837
  setConfirmPrompt(spec) {
10684
10838
  this.confirmPrompt = spec ? { ...spec } : null;
10685
10839
  this.repaint();
@@ -12280,7 +12434,11 @@ var init_import_action_prompt = __esm({
12280
12434
  // src/tui/picker.ts
12281
12435
  function createPickerPrefs() {
12282
12436
  return {
12283
- filters: { cwdOnly: false, hostFilter: "__local", showCat: false }
12437
+ filters: {
12438
+ cwdOnly: false,
12439
+ hostFilter: "__local",
12440
+ includeNonInteractive: false
12441
+ }
12284
12442
  };
12285
12443
  }
12286
12444
  async function pickSession(term, opts) {
@@ -12308,10 +12466,8 @@ async function pickSession(term, opts) {
12308
12466
  if (prefs.filters.cwdOnly) {
12309
12467
  base = base.filter((s) => s.cwd === opts.cwd);
12310
12468
  }
12311
- if (!prefs.filters.showCat) {
12312
- base = base.filter(
12313
- (s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
12314
- );
12469
+ if (!prefs.filters.includeNonInteractive) {
12470
+ base = base.filter((s) => s.interactive === true);
12315
12471
  }
12316
12472
  base = filterByHost(base, prefs.filters.hostFilter);
12317
12473
  return base;
@@ -12513,8 +12669,8 @@ async function pickSession(term, opts) {
12513
12669
  prefs.filters.hostFilter === "__local" ? "host: local" : `host: ${prefs.filters.hostFilter}`
12514
12670
  );
12515
12671
  }
12516
- if (prefs.filters.showCat) {
12517
- parts.push("+cat");
12672
+ if (prefs.filters.includeNonInteractive) {
12673
+ parts.push("+non-interactive");
12518
12674
  }
12519
12675
  if (above > 0) {
12520
12676
  parts.push(`\u2191 ${above} above`);
@@ -13155,7 +13311,9 @@ ${cells}`;
13155
13311
  try {
13156
13312
  const beforeKey = refreshOpts.silent ? renderFingerprint() : "";
13157
13313
  const beforeTotal = total;
13158
- const next = await listSessions(opts.target);
13314
+ const next = await listSessions(opts.target, {
13315
+ includeNonInteractive: true
13316
+ });
13159
13317
  const followId = preferredId ?? (selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0);
13160
13318
  allSessions = sortSessions(next, opts.cwd);
13161
13319
  applyFilter();
@@ -13633,7 +13791,7 @@ ${cells}`;
13633
13791
  const effects = composer.feed(event);
13634
13792
  const after = composer.state();
13635
13793
  const unchanged = before.buffer.length === after.buffer.length && before.buffer.every((line, i) => line === after.buffer[i]) && before.row === after.row && before.col === after.col;
13636
- if (effects.some((e) => e.type === "exit") || unchanged && name === "CTRL_D") {
13794
+ if (effects.some((e) => e.type === "exit")) {
13637
13795
  cleanup();
13638
13796
  resolve8({ kind: "abort" });
13639
13797
  return;
@@ -13733,7 +13891,7 @@ ${cells}`;
13733
13891
  }
13734
13892
  if (name === "i" || name === "I") {
13735
13893
  const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
13736
- prefs.filters.showCat = !prefs.filters.showCat;
13894
+ prefs.filters.includeNonInteractive = !prefs.filters.includeNonInteractive;
13737
13895
  applyFilter();
13738
13896
  restoreCursorAfterFilter(keepId);
13739
13897
  renderFromScratch();
@@ -13999,7 +14157,6 @@ var init_picker = __esm({
13999
14157
  init_session_row();
14000
14158
  init_paths();
14001
14159
  init_session();
14002
- init_hydra_version();
14003
14160
  init_discovery();
14004
14161
  init_history();
14005
14162
  init_input();
@@ -14039,85 +14196,6 @@ var init_picker = __esm({
14039
14196
  }
14040
14197
  });
14041
14198
 
14042
- // src/core/cwd.ts
14043
- import * as fs21 from "fs/promises";
14044
- import * as path18 from "path";
14045
- async function validateLocalCwd(input) {
14046
- const trimmed = input.trim();
14047
- if (trimmed.length === 0) {
14048
- return { ok: false, reason: "path is empty" };
14049
- }
14050
- const resolved = path18.resolve(expandHome(trimmed));
14051
- let stat5;
14052
- try {
14053
- stat5 = await fs21.stat(resolved);
14054
- } catch {
14055
- return { ok: false, reason: `${resolved} does not exist` };
14056
- }
14057
- if (!stat5.isDirectory()) {
14058
- return { ok: false, reason: `${resolved} is not a directory` };
14059
- }
14060
- return { ok: true, path: resolved };
14061
- }
14062
- async function pickInitialLocalCwd(sessionCwd) {
14063
- const candidates = [];
14064
- const seen = /* @__PURE__ */ new Set();
14065
- const push = (p) => {
14066
- if (!seen.has(p)) {
14067
- seen.add(p);
14068
- candidates.push(p);
14069
- }
14070
- };
14071
- push(sessionCwd);
14072
- if (sessionCwd.startsWith("/Users/")) {
14073
- push("/home/" + sessionCwd.slice("/Users/".length));
14074
- } else if (sessionCwd.startsWith("/home/")) {
14075
- push("/Users/" + sessionCwd.slice("/home/".length));
14076
- }
14077
- for (const candidate of candidates) {
14078
- try {
14079
- const stat5 = await fs21.stat(candidate);
14080
- if (stat5.isDirectory()) {
14081
- return candidate;
14082
- }
14083
- } catch {
14084
- }
14085
- }
14086
- return null;
14087
- }
14088
- async function completeLocalPath(input) {
14089
- const lastSlash = input.lastIndexOf("/");
14090
- let prefix;
14091
- let basePrefix;
14092
- let dirForRead;
14093
- if (lastSlash === -1) {
14094
- prefix = "";
14095
- basePrefix = input;
14096
- dirForRead = ".";
14097
- } else {
14098
- prefix = input.slice(0, lastSlash + 1);
14099
- basePrefix = input.slice(lastSlash + 1);
14100
- dirForRead = lastSlash === 0 ? "/" : prefix;
14101
- }
14102
- const resolvedDir = path18.resolve(expandHome(dirForRead));
14103
- let entries;
14104
- try {
14105
- const list = await fs21.readdir(resolvedDir, { withFileTypes: true });
14106
- entries = list.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
14107
- } catch {
14108
- return { prefix, basePrefix, matches: [] };
14109
- }
14110
- const showHidden = basePrefix.startsWith(".");
14111
- const matches = entries.filter((e) => e.name.startsWith(basePrefix)).filter((e) => showHidden || !e.name.startsWith(".")).map((e) => e.isDir ? `${e.name}/` : e.name).sort();
14112
- return { prefix, basePrefix, matches };
14113
- }
14114
- var init_cwd = __esm({
14115
- "src/core/cwd.ts"() {
14116
- "use strict";
14117
- init_config();
14118
- }
14119
- });
14120
-
14121
14199
  // src/tui/completion.ts
14122
14200
  function longestCommonPrefix(names) {
14123
14201
  if (names.length === 0) {
@@ -14168,24 +14246,25 @@ async function promptForImportCwd(term, session, opts = {}) {
14168
14246
  const defaultCwd = opts.defaultCwd ?? await pickInitialLocalCwd(session.cwd) ?? os6.homedir();
14169
14247
  resetTerminalModes();
14170
14248
  const shortId2 = stripHydraSessionPrefix(session.sessionId);
14171
- const fromMachine = session.importedFromMachine ?? "another machine";
14172
14249
  const originalCwd = shortenHomePath(session.cwd);
14250
+ const title = opts.title ?? "Fork locally \u2014 choose cwd";
14251
+ const intro = opts.intro ?? "Pick a local cwd for this session:";
14252
+ const headerRows = [
14253
+ { label: "session: ", value: shortId2 },
14254
+ ...session.importedFromMachine ? [{ label: "from: ", value: session.importedFromMachine }] : [],
14255
+ { label: "cwd: ", value: originalCwd }
14256
+ ];
14173
14257
  let buffer = defaultCwd;
14174
14258
  let errorLine = null;
14175
14259
  let busy = false;
14176
14260
  let layout = null;
14177
14261
  const render = () => {
14178
- const contentHeight = 9;
14262
+ const contentHeight = headerRows.length + 6;
14179
14263
  layout = drawBox(term, {
14180
14264
  contentHeight,
14181
- title: "Fork locally \u2014 choose cwd"
14265
+ title
14182
14266
  });
14183
14267
  const innerW = layout.contentW;
14184
- const headerRows = [
14185
- { label: "session: ", value: shortId2 },
14186
- { label: "from: ", value: fromMachine },
14187
- { label: "cwd: ", value: originalCwd }
14188
- ];
14189
14268
  let row = 0;
14190
14269
  for (const hr of headerRows) {
14191
14270
  term.moveTo(layout.contentX, layout.contentY + row);
@@ -14195,7 +14274,7 @@ async function promptForImportCwd(term, session, opts = {}) {
14195
14274
  }
14196
14275
  row++;
14197
14276
  term.moveTo(layout.contentX, layout.contentY + row);
14198
- term.noFormat(" Pick a local cwd for this session:");
14277
+ term.noFormat(` ${intro}`);
14199
14278
  row += 2;
14200
14279
  paintInputRow(row);
14201
14280
  row += 2;
@@ -14209,7 +14288,7 @@ async function promptForImportCwd(term, session, opts = {}) {
14209
14288
  );
14210
14289
  }
14211
14290
  };
14212
- const inputRow = () => 6;
14291
+ const inputRow = () => headerRows.length + 3;
14213
14292
  const paintInputRow = (rowOffset) => {
14214
14293
  if (!layout) {
14215
14294
  return;
@@ -14390,7 +14469,7 @@ var init_import_cwd_prompt = __esm({
14390
14469
 
14391
14470
  // src/tui/clipboard.ts
14392
14471
  import { spawn as nodeSpawn } from "child_process";
14393
- import fs22 from "fs/promises";
14472
+ import fs23 from "fs/promises";
14394
14473
  import os7 from "os";
14395
14474
  import path19 from "path";
14396
14475
  async function readClipboard(envIn = {}) {
@@ -14431,7 +14510,7 @@ async function readMacOS(env) {
14431
14510
  return img;
14432
14511
  }
14433
14512
  } catch {
14434
- await fs22.unlink(tmpPath).catch(() => void 0);
14513
+ await fs23.unlink(tmpPath).catch(() => void 0);
14435
14514
  }
14436
14515
  try {
14437
14516
  const buf = await runCapture(env.spawn, "pbpaste", []);
@@ -14546,9 +14625,9 @@ async function which(env, cmd) {
14546
14625
  }
14547
14626
  async function readFileAsAttachment(p, unlinkAfter) {
14548
14627
  try {
14549
- const buf = await fs22.readFile(p);
14628
+ const buf = await fs23.readFile(p);
14550
14629
  if (unlinkAfter) {
14551
- await fs22.unlink(p).catch(() => void 0);
14630
+ await fs23.unlink(p).catch(() => void 0);
14552
14631
  }
14553
14632
  if (buf.length === 0) {
14554
14633
  return { ok: false, reason: "no image on clipboard" };
@@ -14673,7 +14752,7 @@ function parseReattachResponse(result) {
14673
14752
  return out;
14674
14753
  }
14675
14754
  function shouldDriftSnap(args) {
14676
- return !args.replayDraining && args.pendingTurns > 0 && args.queueSize === 0 && !args.ownTurnInFlight && !args.hasInFlightHead;
14755
+ return !args.replayDraining && !args.amended && args.pendingTurns > 0 && args.queueSize === 0 && !args.ownTurnInFlight && !args.hasInFlightHead;
14677
14756
  }
14678
14757
  function computeAttachReconcile(args) {
14679
14758
  if (args.daemonTurnStartedAt !== void 0) {
@@ -14694,10 +14773,10 @@ var init_reconnect_state = __esm({
14694
14773
  });
14695
14774
 
14696
14775
  // src/tui/app.ts
14697
- import { appendFileSync, statSync, renameSync } from "fs";
14776
+ import { appendFileSync, statSync as statSync2, renameSync as renameSync2 } from "fs";
14698
14777
  import { nanoid as nanoid3 } from "nanoid";
14699
14778
  import termkit from "terminal-kit";
14700
- import fs23 from "fs/promises";
14779
+ import fs24 from "fs/promises";
14701
14780
  import path20 from "path";
14702
14781
  function isReadonlyForbiddenEffect(effect) {
14703
14782
  switch (effect.type) {
@@ -14835,6 +14914,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14835
14914
  }
14836
14915
  };
14837
14916
  let pendingTurns = 0;
14917
+ let cancelling = false;
14838
14918
  let currentHeadMessageId;
14839
14919
  let sessionBusySince = null;
14840
14920
  let sessionElapsedTimer = null;
@@ -14845,6 +14925,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14845
14925
  pendingTurns = Math.max(0, pendingTurns + delta);
14846
14926
  const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
14847
14927
  if (before === 0 && pendingTurns > 0) {
14928
+ cancelling = false;
14848
14929
  sessionBusySince = Date.now();
14849
14930
  lastUpdateAt = Date.now();
14850
14931
  dispatcherRef?.setTurnRunning(true);
@@ -14865,6 +14946,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14865
14946
  }, 1e3);
14866
14947
  }
14867
14948
  } else if (before > 0 && pendingTurns === 0) {
14949
+ cancelling = false;
14868
14950
  sessionBusySince = null;
14869
14951
  lastUpdateAt = null;
14870
14952
  dispatcherRef?.setTurnRunning(false);
@@ -14879,6 +14961,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14879
14961
  stalled: false
14880
14962
  });
14881
14963
  }
14964
+ } else if (pendingTurns > 0 && cancelling) {
14965
+ cancelling = false;
14966
+ if (screenReady) {
14967
+ screenRef.setBanner({ status: "busy", stalled: false });
14968
+ }
14882
14969
  }
14883
14970
  void delta;
14884
14971
  };
@@ -15237,18 +15324,19 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15237
15324
  historyPolicy: "full",
15238
15325
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
15239
15326
  ...opts.readonly === true ? { readonly: true } : {},
15240
- // Forward the user-chosen cwd for first-launch imported sessions
15241
- // via a full resume hint. upstreamSessionId is empty so the
15242
- // daemon routes through doResurrectFromImport (session-manager.ts)
15243
- // with the user-supplied cwd instead of silently falling back to
15244
- // $HOME in resolveImportCwd.
15245
- ...ctx.importAttachHint !== void 0 ? {
15327
+ // Forward the user-chosen cwd via a full resume hint. An empty
15328
+ // upstreamSessionId routes through doResurrectFromImport
15329
+ // (first-launch imports); a real one takes the normal session/load
15330
+ // path (repairing a local session whose recorded cwd is gone).
15331
+ // Either way the daemon resurrects with this cwd instead of the
15332
+ // stale recorded one.
15333
+ ...ctx.resumeHint !== void 0 ? {
15246
15334
  _meta: {
15247
15335
  [HYDRA_META_KEY]: {
15248
15336
  resume: {
15249
- upstreamSessionId: "",
15250
- agentId: ctx.importAttachHint.agentId,
15251
- cwd: ctx.importAttachHint.cwd
15337
+ upstreamSessionId: ctx.resumeHint.upstreamSessionId,
15338
+ agentId: ctx.resumeHint.agentId,
15339
+ cwd: ctx.resumeHint.cwd
15252
15340
  }
15253
15341
  }
15254
15342
  }
@@ -15335,9 +15423,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15335
15423
  if (pendingPermission && tryHandlePermissionKey(ev)) {
15336
15424
  continue;
15337
15425
  }
15338
- if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
15339
- continue;
15340
- }
15341
15426
  if (tryHandleHelpKey(ev)) {
15342
15427
  continue;
15343
15428
  }
@@ -15551,86 +15636,35 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15551
15636
  const cancelRemoteTurn = () => {
15552
15637
  conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
15553
15638
  };
15554
- const sigintHandler = () => {
15555
- if (turnInFlight) {
15556
- turnInFlight.cancel();
15639
+ const markCancelling = () => {
15640
+ if (screenRef === null) {
15557
15641
  return;
15558
15642
  }
15559
- if (pendingTurns > 0) {
15560
- cancelRemoteTurn();
15643
+ if (pendingTurns !== 1) {
15561
15644
  return;
15562
15645
  }
15563
- void requestExit();
15646
+ cancelling = true;
15647
+ screenRef.setBanner({
15648
+ status: "cancelling",
15649
+ elapsedMs: void 0,
15650
+ stalled: false
15651
+ });
15564
15652
  };
15565
- let exitConfirmation = null;
15566
- const requestExit = async () => {
15567
- if (exitConfirmation) {
15568
- stop(0);
15569
- return;
15570
- }
15571
- if (pendingTurns === 0) {
15572
- stop(0);
15573
- return;
15574
- }
15575
- let onlyClient = false;
15576
- try {
15577
- const sessions = await listSessions(target);
15578
- const me = sessions.find((s) => s.sessionId === resolvedSessionId);
15579
- onlyClient = !me || me.attachedClients <= 1;
15580
- } catch {
15581
- stop(0);
15653
+ const sigintHandler = () => {
15654
+ if (turnInFlight) {
15655
+ turnInFlight.cancel();
15656
+ markCancelling();
15582
15657
  return;
15583
15658
  }
15584
- if (!onlyClient) {
15585
- stop(0);
15659
+ if (pendingTurns > 0) {
15660
+ cancelRemoteTurn();
15661
+ markCancelling();
15586
15662
  return;
15587
15663
  }
15588
- exitConfirmation = { offered: true };
15589
- screen.setConfirmPrompt({
15590
- question: "Agent is still working. Interrupt it before exit?",
15591
- hint: "y interrupt then exit \xB7 n / Enter detach silently \xB7 Esc cancel"
15592
- });
15664
+ requestExit();
15593
15665
  };
15594
- const dismissExitConfirmation = () => {
15595
- exitConfirmation = null;
15596
- screen.setConfirmPrompt(null);
15597
- };
15598
- const tryHandleExitConfirmKey = (ev) => {
15599
- if (!exitConfirmation) {
15600
- return false;
15601
- }
15602
- if (ev.type === "char") {
15603
- const ch = ev.ch.toLowerCase();
15604
- if (ch === "y") {
15605
- dismissExitConfirmation();
15606
- conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
15607
- stop(0);
15608
- return true;
15609
- }
15610
- if (ch === "n") {
15611
- dismissExitConfirmation();
15612
- stop(0);
15613
- return true;
15614
- }
15615
- return true;
15616
- }
15617
- if (ev.type === "key") {
15618
- if (ev.name === "enter") {
15619
- dismissExitConfirmation();
15620
- stop(0);
15621
- return true;
15622
- }
15623
- if (ev.name === "escape") {
15624
- dismissExitConfirmation();
15625
- return true;
15626
- }
15627
- if (ev.name === "ctrl-c" || ev.name === "ctrl-d") {
15628
- dismissExitConfirmation();
15629
- stop(0);
15630
- return true;
15631
- }
15632
- }
15633
- return true;
15666
+ const requestExit = () => {
15667
+ stop(0);
15634
15668
  };
15635
15669
  const buildHelpEntries = () => {
15636
15670
  const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
@@ -15702,7 +15736,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15702
15736
  let resolvedChoice = null;
15703
15737
  let attachOverrides = null;
15704
15738
  while (resolvedChoice === null) {
15705
- const sessions = await listSessions(target);
15739
+ const sessions = await listSessions(target, { includeNonInteractive: true });
15706
15740
  const choice2 = await pickSession(term, {
15707
15741
  cwd: resolvedCwd,
15708
15742
  sessions,
@@ -15740,14 +15774,43 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15740
15774
  readonly: false,
15741
15775
  cwd: decided2.ctx.cwd
15742
15776
  };
15743
- if (decided2.ctx.importAttachHint !== void 0) {
15744
- attachOverrides.importAttachHint = decided2.ctx.importAttachHint;
15777
+ if (decided2.ctx.resumeHint !== void 0) {
15778
+ attachOverrides.resumeHint = decided2.ctx.resumeHint;
15745
15779
  }
15746
15780
  break;
15747
15781
  }
15748
15782
  const chosen = sessions.find((s) => s.sessionId === choice2.sessionId);
15749
15783
  const isImportedFirstLaunch = chosen !== void 0 && !!chosen.importedFromMachine && !chosen.upstreamSessionId && choice2.readonly !== true;
15750
15784
  if (!isImportedFirstLaunch) {
15785
+ if (target.isLocal && chosen && !chosen.importedFromMachine && choice2.readonly !== true) {
15786
+ const v = await validateLocalCwd(chosen.cwd);
15787
+ if (!v.ok) {
15788
+ const r = await promptForImportCwd(term, chosen, {
15789
+ defaultCwd: expandHome(config.defaultCwd),
15790
+ title: "Working directory missing \u2014 choose cwd",
15791
+ intro: "This session's working directory no longer exists. Pick a new one:"
15792
+ });
15793
+ if (r.kind === "cancel") {
15794
+ screen.start({ skipFullscreen: true });
15795
+ screen.resumeRepaint();
15796
+ return;
15797
+ }
15798
+ if (r.kind === "back") {
15799
+ continue;
15800
+ }
15801
+ resolvedChoice = { choice: choice2, sessions };
15802
+ attachOverrides = {
15803
+ readonly: false,
15804
+ cwd: r.path,
15805
+ resumeHint: {
15806
+ agentId: choice2.agentId ?? chosen.agentId ?? "",
15807
+ cwd: r.path,
15808
+ upstreamSessionId: ""
15809
+ }
15810
+ };
15811
+ break;
15812
+ }
15813
+ }
15751
15814
  resolvedChoice = { choice: choice2, sessions };
15752
15815
  break;
15753
15816
  }
@@ -15766,8 +15829,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15766
15829
  readonly: opsShim.readonly === true,
15767
15830
  cwd: decided.ctx.cwd
15768
15831
  };
15769
- if (decided.ctx.importAttachHint !== void 0) {
15770
- attachOverrides.importAttachHint = decided.ctx.importAttachHint;
15832
+ if (decided.ctx.resumeHint !== void 0) {
15833
+ attachOverrides.resumeHint = decided.ctx.resumeHint;
15771
15834
  }
15772
15835
  }
15773
15836
  const { choice } = resolvedChoice;
@@ -15802,10 +15865,10 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15802
15865
  if (choice.agentId !== void 0) {
15803
15866
  nextOpts.agentId = choice.agentId;
15804
15867
  }
15805
- if (attachOverrides?.importAttachHint !== void 0) {
15806
- nextOpts.importAttachHint = attachOverrides.importAttachHint;
15868
+ if (attachOverrides?.resumeHint !== void 0) {
15869
+ nextOpts.resumeHint = attachOverrides.resumeHint;
15807
15870
  } else {
15808
- delete nextOpts.importAttachHint;
15871
+ delete nextOpts.resumeHint;
15809
15872
  }
15810
15873
  resume(nextOpts);
15811
15874
  };
@@ -15908,10 +15971,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15908
15971
  } else if (pendingTurns > 0) {
15909
15972
  cancelRemoteTurn();
15910
15973
  }
15974
+ markCancelling();
15911
15975
  return;
15912
15976
  }
15913
15977
  case "exit":
15914
- void requestExit();
15978
+ requestExit();
15915
15979
  return;
15916
15980
  case "plan-toggle":
15917
15981
  void handleModeToggle(effect.on);
@@ -15999,7 +16063,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15999
16063
  continue;
16000
16064
  }
16001
16065
  try {
16002
- const buf = await fs23.readFile(token);
16066
+ const buf = await fs24.readFile(token);
16003
16067
  if (buf.length > MAX_ATTACHMENT_BYTES) {
16004
16068
  screen.notify(
16005
16069
  `image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
@@ -16209,7 +16273,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16209
16273
  switch (cmd) {
16210
16274
  case "/quit":
16211
16275
  case "/exit":
16212
- void requestExit();
16276
+ requestExit();
16213
16277
  return true;
16214
16278
  case "/clear":
16215
16279
  toolStates.clear();
@@ -16220,6 +16284,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16220
16284
  toolsBlockStopReason = null;
16221
16285
  toolsExpanded = false;
16222
16286
  lastEditMarkPath = null;
16287
+ turnHasShownProse = false;
16223
16288
  screen.clearScrollback();
16224
16289
  return true;
16225
16290
  case "/demo-plan": {
@@ -16565,6 +16630,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16565
16630
  }
16566
16631
  };
16567
16632
  let lastEditMarkPath = null;
16633
+ let turnHasShownProse = false;
16568
16634
  const maybeRenderEditDiff = (toolCallId) => {
16569
16635
  const mode = config.tui.showFileUpdates;
16570
16636
  if (mode === "none") {
@@ -16581,6 +16647,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16581
16647
  }
16582
16648
  return;
16583
16649
  }
16650
+ if (!turnHasShownProse) {
16651
+ return;
16652
+ }
16584
16653
  const diff = state.editDiff;
16585
16654
  if (diff.path && diff.path === lastEditMarkPath) {
16586
16655
  return;
@@ -16666,6 +16735,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16666
16735
  toolsExpanded = false;
16667
16736
  toolsBlockEndedAt = null;
16668
16737
  lastEditMarkPath = null;
16738
+ turnHasShownProse = false;
16669
16739
  startToolsBlock();
16670
16740
  screen.redraw();
16671
16741
  return;
@@ -16673,6 +16743,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16673
16743
  if (event.kind === "agent-text") {
16674
16744
  closeThought();
16675
16745
  if (event.text.length > 0) {
16746
+ turnHasShownProse = true;
16676
16747
  lastEditMarkPath = null;
16677
16748
  }
16678
16749
  appendAgentText(event.text);
@@ -16681,6 +16752,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16681
16752
  if (event.kind === "agent-thought") {
16682
16753
  closeAgentText();
16683
16754
  if (viewPrefs.showThoughts && event.text.length > 0) {
16755
+ turnHasShownProse = true;
16684
16756
  lastEditMarkPath = null;
16685
16757
  }
16686
16758
  appendThought(event.text);
@@ -16804,13 +16876,15 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16804
16876
  toolsExpanded = false;
16805
16877
  upstreamInterruptedSeen = false;
16806
16878
  lastEditMarkPath = null;
16879
+ turnHasShownProse = false;
16807
16880
  screen.ensureSeparator();
16808
16881
  if (shouldDriftSnap({
16809
16882
  pendingTurns,
16810
16883
  queueSize: queueCache.size,
16811
16884
  ownTurnInFlight: turnInFlight !== null,
16812
16885
  hasInFlightHead: currentHeadMessageId !== void 0,
16813
- replayDraining
16886
+ replayDraining,
16887
+ amended: event.amended === true
16814
16888
  })) {
16815
16889
  adjustPendingTurns(-pendingTurns);
16816
16890
  }
@@ -16889,6 +16963,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16889
16963
  toolsBlockStopReason = null;
16890
16964
  toolsExpanded = false;
16891
16965
  lastEditMarkPath = null;
16966
+ turnHasShownProse = false;
16892
16967
  };
16893
16968
  onDisconnectHook = () => {
16894
16969
  screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
@@ -17038,8 +17113,8 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17038
17113
  agentId: opts.agentId ?? "",
17039
17114
  cwd
17040
17115
  };
17041
- if (opts.importAttachHint !== void 0) {
17042
- ctx.importAttachHint = opts.importAttachHint;
17116
+ if (opts.resumeHint !== void 0) {
17117
+ ctx.resumeHint = opts.resumeHint;
17043
17118
  }
17044
17119
  return ctx;
17045
17120
  }
@@ -17061,7 +17136,7 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17061
17136
  };
17062
17137
  }
17063
17138
  while (true) {
17064
- const sessions = await listSessions(target);
17139
+ const sessions = await listSessions(target, { includeNonInteractive: true });
17065
17140
  const choice = await pickSession(term, {
17066
17141
  cwd,
17067
17142
  sessions,
@@ -17101,6 +17176,33 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17101
17176
  }
17102
17177
  return decided.ctx;
17103
17178
  }
17179
+ if (target.isLocal && chosen && !chosen.importedFromMachine && !opts.readonly) {
17180
+ const v = await validateLocalCwd(chosen.cwd);
17181
+ if (!v.ok) {
17182
+ const r = await promptForImportCwd(term, chosen, {
17183
+ defaultCwd: expandHome(config.defaultCwd),
17184
+ title: "Working directory missing \u2014 choose cwd",
17185
+ intro: "This session's working directory no longer exists. Pick a new one:"
17186
+ });
17187
+ if (r.kind === "cancel") {
17188
+ return null;
17189
+ }
17190
+ if (r.kind === "back") {
17191
+ continue;
17192
+ }
17193
+ const agentId = choice.agentId ?? chosen.agentId ?? "";
17194
+ return {
17195
+ sessionId: choice.sessionId,
17196
+ agentId,
17197
+ cwd: r.path,
17198
+ resumeHint: {
17199
+ agentId,
17200
+ cwd: r.path,
17201
+ upstreamSessionId: ""
17202
+ }
17203
+ };
17204
+ }
17205
+ }
17104
17206
  return {
17105
17207
  sessionId: choice.sessionId,
17106
17208
  agentId: choice.agentId ?? "",
@@ -17143,7 +17245,9 @@ async function runImportedFirstLaunchFlow(term, chosen, choice, opts) {
17143
17245
  sessionId: choice.sessionId,
17144
17246
  agentId,
17145
17247
  cwd: cwdResult.path,
17146
- importAttachHint: { agentId, cwd: cwdResult.path }
17248
+ // Empty upstreamSessionId import-reseed path for a never-launched
17249
+ // imported session.
17250
+ resumeHint: { agentId, cwd: cwdResult.path, upstreamSessionId: "" }
17147
17251
  }
17148
17252
  };
17149
17253
  }
@@ -17187,9 +17291,15 @@ fork failed: ${err.message}
17187
17291
  // For foreign-never-launched forks, the daemon stamped the chosen
17188
17292
  // cwd onto meta.json via the POST body, but the very first attach
17189
17293
  // still goes through the import-reseed path (upstreamSessionId=""),
17190
- // and importAttachHint is what makes attachManagerHooks persist
17294
+ // and the resume hint is what makes attachManagerHooks persist
17191
17295
  // the local cwd over the bundle's recorded one.
17192
- ...isForeignNeverLaunched ? { importAttachHint: { agentId: choice.sourceAgentId ?? "", cwd } } : {}
17296
+ ...isForeignNeverLaunched ? {
17297
+ resumeHint: {
17298
+ agentId: choice.sourceAgentId ?? "",
17299
+ cwd,
17300
+ upstreamSessionId: ""
17301
+ }
17302
+ } : {}
17193
17303
  }
17194
17304
  };
17195
17305
  }
@@ -17315,11 +17425,11 @@ function createInstallStatusLine(term, baseLabel) {
17315
17425
  }
17316
17426
  function rotateIfBig(target) {
17317
17427
  try {
17318
- const stat5 = statSync(target);
17428
+ const stat5 = statSync2(target);
17319
17429
  if (stat5.size < logMaxBytes) {
17320
17430
  return;
17321
17431
  }
17322
- renameSync(target, `${target}.0`);
17432
+ renameSync2(target, `${target}.0`);
17323
17433
  } catch {
17324
17434
  }
17325
17435
  }
@@ -17331,6 +17441,7 @@ var init_app = __esm({
17331
17441
  init_types();
17332
17442
  init_resilient_ws();
17333
17443
  init_config();
17444
+ init_cwd();
17334
17445
  init_remote_target();
17335
17446
  init_daemon_bootstrap();
17336
17447
  init_bin_name();
@@ -18828,10 +18939,17 @@ var SessionRecord = z5.object({
18828
18939
  // ended at. Kept so future UI can show "branched from turn N of session X".
18829
18940
  forkedFromSessionId: z5.string().optional(),
18830
18941
  forkedFromMessageId: z5.string().optional(),
18831
- // clientInfo from the process that issued session/new. Picker and
18832
- // `sessions list` use this to hide cat-style ancillary sessions by
18833
- // default; carried in meta.json so cold sessions filter the same way.
18942
+ // clientInfo from the process that issued session/new. Display only
18943
+ // since the `interactive` flag below; kept on the record for log
18944
+ // attribution and as the legacy hint inside effectiveInteractive
18945
+ // (pre-flag cat sessions can be recognised from this field).
18834
18946
  originatingClient: PersistedOriginatingClient.optional(),
18947
+ // Tristate: true once the session has had a real turn, false when
18948
+ // explicitly created as ancillary (e.g. `hydra cat`), undefined for
18949
+ // pre-flag records / freshly-created sessions that haven't decided
18950
+ // yet. effectiveInteractive() in session-manager.ts is the single
18951
+ // resolver — every filter site goes through it.
18952
+ interactive: z5.boolean().optional(),
18835
18953
  createdAt: z5.string(),
18836
18954
  updatedAt: z5.string()
18837
18955
  });
@@ -18950,6 +19068,7 @@ function recordFromMemorySession(args) {
18950
19068
  forkedFromSessionId: args.forkedFromSessionId,
18951
19069
  forkedFromMessageId: args.forkedFromMessageId,
18952
19070
  originatingClient: args.originatingClient,
19071
+ interactive: args.interactive,
18953
19072
  createdAt: args.createdAt ?? now,
18954
19073
  updatedAt: args.updatedAt ?? now
18955
19074
  };
@@ -19753,6 +19872,7 @@ var HistoryStore = class {
19753
19872
 
19754
19873
  // src/core/session-manager.ts
19755
19874
  init_paths();
19875
+ init_config();
19756
19876
  init_history();
19757
19877
 
19758
19878
  // src/core/bundle.ts
@@ -19788,6 +19908,14 @@ var BundleSession = z6.object({
19788
19908
  currentUsage: PersistedUsage.optional(),
19789
19909
  agentCommands: z6.array(PersistedAgentCommand).optional(),
19790
19910
  agentModes: z6.array(PersistedAgentMode).optional(),
19911
+ // Raw interactive tristate (NOT the resolved effectiveInteractive) so
19912
+ // the value stays promotable on the destination: a cat/empty source
19913
+ // arrives as undefined and a real turn there can still flip it to
19914
+ // true. Carried alongside originatingClient so the importer's
19915
+ // effectiveInteractive can re-apply the cat-name hint at read time
19916
+ // without freezing a sticky `false` into the record.
19917
+ interactive: z6.boolean().optional(),
19918
+ originatingClient: PersistedOriginatingClient.optional(),
19791
19919
  createdAt: z6.string(),
19792
19920
  updatedAt: z6.string()
19793
19921
  });
@@ -19831,6 +19959,8 @@ function encodeBundle(params) {
19831
19959
  ...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
19832
19960
  ...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
19833
19961
  ...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
19962
+ ...params.record.interactive !== void 0 ? { interactive: params.record.interactive } : {},
19963
+ ...params.record.originatingClient !== void 0 ? { originatingClient: params.record.originatingClient } : {},
19834
19964
  createdAt: params.record.createdAt,
19835
19965
  updatedAt: params.record.updatedAt
19836
19966
  },
@@ -19869,6 +19999,7 @@ var SessionManager = class {
19869
19999
  this.logger = options.logger;
19870
20000
  this.npmRegistry = options.npmRegistry;
19871
20001
  this.extensionCommands = options.extensionCommands;
20002
+ this.defaultCwd = options.defaultCwd ?? "~";
19872
20003
  this.synopsisCoordinator = new SynopsisCoordinator({
19873
20004
  registry: this.registry,
19874
20005
  store: this.store,
@@ -19902,6 +20033,7 @@ var SessionManager = class {
19902
20033
  logger;
19903
20034
  npmRegistry;
19904
20035
  extensionCommands;
20036
+ defaultCwd;
19905
20037
  // Background queue for ephemeral-agent synopsis generation. Runs
19906
20038
  // out-of-band so session close is instant; persists synopsis/title
19907
20039
  // via the same enqueueMetaWrite path the in-session handlers used.
@@ -19961,6 +20093,7 @@ var SessionManager = class {
19961
20093
  transformChain: params.transformChain,
19962
20094
  parentSessionId: params.parentSessionId,
19963
20095
  originatingClient: params.originatingClient,
20096
+ interactive: params.interactive,
19964
20097
  extensionCommands: this.extensionCommands,
19965
20098
  scheduleSynopsis: () => this.synopsisCoordinator.schedule(session.sessionId)
19966
20099
  });
@@ -20007,6 +20140,9 @@ var SessionManager = class {
20007
20140
  if (params.upstreamSessionId === "") {
20008
20141
  return this.doResurrectFromImport(params);
20009
20142
  }
20143
+ if (!await this.dirExists(params.cwd)) {
20144
+ return this.doResurrectFromImport(params);
20145
+ }
20010
20146
  const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
20011
20147
  npmRegistry: this.npmRegistry,
20012
20148
  onInstallProgress: params.onInstallProgress
@@ -20134,6 +20270,7 @@ var SessionManager = class {
20134
20270
  firstPromptSeeded: !!params.title,
20135
20271
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
20136
20272
  originatingClient: params.originatingClient,
20273
+ interactive: params.interactive,
20137
20274
  forkedFromSessionId: params.forkedFromSessionId,
20138
20275
  forkedFromMessageId: params.forkedFromMessageId,
20139
20276
  extensionCommands: this.extensionCommands,
@@ -20150,7 +20287,7 @@ var SessionManager = class {
20150
20287
  // so subsequent resurrects of this session use the normal session/load
20151
20288
  // path.
20152
20289
  async doResurrectFromImport(params) {
20153
- const cwd = await this.resolveImportCwd(params.cwd);
20290
+ const cwd = await this.resolveResurrectCwd(params.cwd);
20154
20291
  const fresh = await this.bootstrapAgent({
20155
20292
  agentId: params.agentId,
20156
20293
  cwd,
@@ -20205,6 +20342,7 @@ var SessionManager = class {
20205
20342
  firstPromptSeeded: !!params.title,
20206
20343
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
20207
20344
  originatingClient: params.originatingClient,
20345
+ interactive: params.interactive,
20208
20346
  forkedFromSessionId: params.forkedFromSessionId,
20209
20347
  forkedFromMessageId: params.forkedFromMessageId,
20210
20348
  extensionCommands: this.extensionCommands,
@@ -20214,15 +20352,50 @@ var SessionManager = class {
20214
20352
  void session.seedFromImport().catch(() => void 0);
20215
20353
  return session;
20216
20354
  }
20217
- async resolveImportCwd(cwd) {
20355
+ async dirExists(cwd) {
20218
20356
  try {
20219
- const stat5 = await fs14.stat(cwd);
20220
- if (stat5.isDirectory()) {
20221
- return cwd;
20222
- }
20357
+ return (await fs14.stat(cwd)).isDirectory();
20223
20358
  } catch {
20359
+ return false;
20224
20360
  }
20225
- return os3.homedir();
20361
+ }
20362
+ // When the last client detaches from a session that resolves to
20363
+ // non-interactive — e.g. a `hydra cat` run, born interactive:undefined
20364
+ // with originatingClient hydra-acp-cat, whose every prompt is ancillary
20365
+ // — close it so its agent process doesn't linger until the (default 1h)
20366
+ // idle timeout fires. The cold record is kept, so the rare refine-in-TUI
20367
+ // still works via the resurrect/reseed path. Sessions promoted to
20368
+ // interactive (driven by a real, non-ancillary prompt) resolve to true
20369
+ // and are left running.
20370
+ async reapIfOrphanedNonInteractive(sessionId) {
20371
+ const session = this.sessions.get(sessionId);
20372
+ if (!session || session.attachedCount > 0) {
20373
+ return;
20374
+ }
20375
+ const interactive = effectiveInteractive(
20376
+ {
20377
+ interactive: session.interactive,
20378
+ ...session.originatingClient ? { originatingClient: session.originatingClient } : {}
20379
+ },
20380
+ true
20381
+ );
20382
+ if (interactive !== false) {
20383
+ return;
20384
+ }
20385
+ this.logger?.info(
20386
+ `reaping orphaned non-interactive session ${sessionId} (agent killed, cold record kept)`
20387
+ );
20388
+ await session.close({ deleteRecord: false }).catch(() => void 0);
20389
+ }
20390
+ // Resolve a recorded cwd for resurrect: use it if it still exists,
20391
+ // otherwise fall back to the configured defaultCwd. Covers both bundles
20392
+ // imported from another machine and local sessions (e.g. `cat`) whose
20393
+ // recorded dir was cleaned up, so the reseed spawn never ENOENTs.
20394
+ async resolveResurrectCwd(cwd) {
20395
+ if (await this.dirExists(cwd)) {
20396
+ return cwd;
20397
+ }
20398
+ return expandHome(this.defaultCwd);
20226
20399
  }
20227
20400
  // Pull every session the agent itself remembers (across all cwds) and
20228
20401
  // persist a cold hydra record for each one we don't already track.
@@ -20311,6 +20484,10 @@ var SessionManager = class {
20311
20484
  agentId,
20312
20485
  cwd: entry.cwd,
20313
20486
  pendingHistorySync: true,
20487
+ // `hydra agent sync` is a user-explicit "show me agent-side
20488
+ // sessions" action; the rows are meant to be visible immediately
20489
+ // even before the first resurrect populates history.jsonl.
20490
+ interactive: true,
20314
20491
  createdAt: ts,
20315
20492
  updatedAt: ts
20316
20493
  };
@@ -20477,6 +20654,11 @@ var SessionManager = class {
20477
20654
  () => void 0
20478
20655
  );
20479
20656
  });
20657
+ session.onInteractiveChange((interactive) => {
20658
+ void this.persistSnapshot(session.sessionId, { interactive }).catch(
20659
+ () => void 0
20660
+ );
20661
+ });
20480
20662
  session.onUsageChange((usage) => {
20481
20663
  void this.persistSnapshot(session.sessionId, {
20482
20664
  currentUsage: usageSnapshotToPersisted(usage)
@@ -20570,6 +20752,7 @@ var SessionManager = class {
20570
20752
  createdAt: record.createdAt,
20571
20753
  pendingHistorySync: record.pendingHistorySync,
20572
20754
  originatingClient: record.originatingClient,
20755
+ interactive: record.interactive,
20573
20756
  forkedFromSessionId: record.forkedFromSessionId,
20574
20757
  forkedFromMessageId: record.forkedFromMessageId
20575
20758
  };
@@ -20657,12 +20840,27 @@ var SessionManager = class {
20657
20840
  async list(filter = {}) {
20658
20841
  const entries = [];
20659
20842
  const liveIds = /* @__PURE__ */ new Set();
20843
+ const includeRow = (interactive) => {
20844
+ if (filter.includeNonInteractive) return true;
20845
+ return interactive === true;
20846
+ };
20660
20847
  for (const session of this.sessions.values()) {
20661
20848
  if (filter.cwd && session.cwd !== filter.cwd) {
20662
20849
  continue;
20663
20850
  }
20664
20851
  liveIds.add(session.sessionId);
20665
- const used = await historyMtimeIso(session.sessionId) ?? new Date(session.updatedAt).toISOString();
20852
+ const hist = await historyStatus(session.sessionId);
20853
+ const interactive = effectiveInteractive(
20854
+ {
20855
+ interactive: session.interactive,
20856
+ ...session.originatingClient ? { originatingClient: session.originatingClient } : {}
20857
+ },
20858
+ hist.hasContent
20859
+ );
20860
+ if (!includeRow(interactive)) {
20861
+ continue;
20862
+ }
20863
+ const used = hist.mtime ?? new Date(session.updatedAt).toISOString();
20666
20864
  entries.push({
20667
20865
  sessionId: session.sessionId,
20668
20866
  upstreamSessionId: session.upstreamSessionId,
@@ -20675,6 +20873,7 @@ var SessionManager = class {
20675
20873
  forkedFromSessionId: session.forkedFromSessionId,
20676
20874
  forkedFromMessageId: session.forkedFromMessageId,
20677
20875
  originatingClient: session.originatingClient,
20876
+ interactive,
20678
20877
  updatedAt: used,
20679
20878
  attachedClients: session.attachedCount,
20680
20879
  status: "live",
@@ -20689,7 +20888,12 @@ var SessionManager = class {
20689
20888
  if (filter.cwd && r.cwd !== filter.cwd) {
20690
20889
  continue;
20691
20890
  }
20692
- const used = await historyMtimeIso(r.sessionId) ?? r.updatedAt;
20891
+ const hist = await historyStatus(r.sessionId);
20892
+ const interactive = effectiveInteractive(r, hist.hasContent);
20893
+ if (!includeRow(interactive)) {
20894
+ continue;
20895
+ }
20896
+ const used = hist.mtime ?? r.updatedAt;
20693
20897
  entries.push({
20694
20898
  sessionId: r.sessionId,
20695
20899
  upstreamSessionId: r.upstreamSessionId,
@@ -20707,6 +20911,7 @@ var SessionManager = class {
20707
20911
  forkedFromSessionId: r.forkedFromSessionId,
20708
20912
  forkedFromMessageId: r.forkedFromMessageId,
20709
20913
  originatingClient: r.originatingClient,
20914
+ interactive,
20710
20915
  updatedAt: used,
20711
20916
  attachedClients: 0,
20712
20917
  status: "cold",
@@ -20941,8 +21146,18 @@ var SessionManager = class {
20941
21146
  currentUsage: args.bundle.session.currentUsage,
20942
21147
  agentCommands: args.bundle.session.agentCommands,
20943
21148
  agentModes: args.bundle.session.agentModes,
21149
+ // Carry the source's raw interactive tristate and originating
21150
+ // client rather than forcing true. A real conversation arrives
21151
+ // as true (visible immediately); an empty source arrives as
21152
+ // undefined (hidden until a turn lands here); a cat source
21153
+ // arrives as undefined + cat originatingClient, so
21154
+ // effectiveInteractive hides it via the hint while leaving it
21155
+ // promotable. Legacy bundles (pre-flag) carry neither and fall
21156
+ // back to effectiveInteractive's history-presence inference.
21157
+ interactive: args.bundle.session.interactive,
21158
+ originatingClient: args.bundle.session.originatingClient,
20944
21159
  createdAt: args.preservedCreatedAt ?? now,
20945
- // Fallback path for historyMtimeIso (used when the history file
21160
+ // Fallback path for historyStatus (used when the history file
20946
21161
  // is missing). Keep this consistent with the utimes stamp above.
20947
21162
  updatedAt: args.bundle.session.updatedAt
20948
21163
  });
@@ -21051,6 +21266,8 @@ var SessionManager = class {
21051
21266
  ...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
21052
21267
  ...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
21053
21268
  ...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
21269
+ ...update.interactive !== void 0 ? { interactive: update.interactive } : {},
21270
+ ...update.cwd !== void 0 ? { cwd: update.cwd } : {},
21054
21271
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21055
21272
  });
21056
21273
  });
@@ -21227,6 +21444,7 @@ function mergeForPersistence(session, existing) {
21227
21444
  forkedFromSessionId: session.forkedFromSessionId ?? existing?.forkedFromSessionId,
21228
21445
  forkedFromMessageId: session.forkedFromMessageId ?? existing?.forkedFromMessageId,
21229
21446
  originatingClient: session.originatingClient ?? existing?.originatingClient,
21447
+ interactive: session.interactive ?? existing?.interactive,
21230
21448
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
21231
21449
  });
21232
21450
  }
@@ -21533,13 +21751,25 @@ async function loadPromptHistorySafely(sessionId) {
21533
21751
  return [];
21534
21752
  }
21535
21753
  }
21536
- async function historyMtimeIso(sessionId) {
21754
+ async function historyStatus(sessionId) {
21537
21755
  try {
21538
21756
  const st = await fs14.stat(paths.historyFile(sessionId));
21539
- return new Date(st.mtimeMs).toISOString();
21757
+ return {
21758
+ mtime: new Date(st.mtimeMs).toISOString(),
21759
+ hasContent: st.size > 0
21760
+ };
21540
21761
  } catch {
21541
- return void 0;
21762
+ return { hasContent: false };
21763
+ }
21764
+ }
21765
+ function effectiveInteractive(record, hasContent) {
21766
+ if (record.interactive !== void 0) {
21767
+ return record.interactive;
21542
21768
  }
21769
+ if (record.originatingClient?.name === HYDRA_CAT_CLIENT_NAME) {
21770
+ return false;
21771
+ }
21772
+ return hasContent ? true : void 0;
21543
21773
  }
21544
21774
 
21545
21775
  // src/core/child-supervisor.ts
@@ -23317,17 +23547,21 @@ function resolveHydraHost(defaults) {
23317
23547
  function registerSessionRoutes(app, manager, defaults) {
23318
23548
  app.get("/v1/sessions", async (request) => {
23319
23549
  const query = request.query;
23320
- const sessions = await manager.list({ cwd: query?.cwd });
23550
+ const includeNonInteractive = query?.includeNonInteractive === "1" || query?.includeNonInteractive === "true";
23551
+ const sessions = await manager.list({
23552
+ cwd: query?.cwd,
23553
+ includeNonInteractive
23554
+ });
23321
23555
  return { sessions };
23322
23556
  });
23323
- app.get("/v1/sessions/search", async (request, reply) => {
23324
- const query = request.query;
23325
- const q = query?.q ?? "";
23557
+ app.post("/v1/sessions/search", async (request, reply) => {
23558
+ const body = request.body ?? {};
23559
+ const q = typeof body.q === "string" ? body.q : "";
23326
23560
  if (q.trim().length === 0) {
23327
23561
  reply.code(400).send({ error: "q is required" });
23328
23562
  return reply;
23329
23563
  }
23330
- const ids = query?.sessionIds ? query.sessionIds.split(",").filter((s) => s.length > 0) : void 0;
23564
+ const ids = Array.isArray(body.sessionIds) ? body.sessionIds.filter((s) => typeof s === "string" && s.length > 0) : void 0;
23331
23565
  const out = await searchHistories(manager, q, { sessionIds: ids });
23332
23566
  return out;
23333
23567
  });
@@ -23922,13 +24156,8 @@ function parseRegisterBody2(body) {
23922
24156
  }
23923
24157
 
23924
24158
  // src/daemon/routes/config.ts
23925
- function registerConfigRoutes(app, defaults) {
23926
- app.get("/v1/config", async () => {
23927
- return {
23928
- defaultAgent: defaults.defaultAgent,
23929
- defaultCwd: defaults.defaultCwd
23930
- };
23931
- });
24159
+ function registerConfigRoutes(app, snapshot) {
24160
+ app.get("/v1/config", async () => snapshot);
23932
24161
  }
23933
24162
 
23934
24163
  // src/daemon/routes/auth.ts
@@ -24448,7 +24677,8 @@ function registerAcpWsEndpoint(app, deps) {
24448
24677
  model: hydraMeta.model,
24449
24678
  onInstallProgress: makeInstallProgressForwarder(connection),
24450
24679
  transformChain,
24451
- originatingClient: state.clientInfo
24680
+ originatingClient: state.clientInfo,
24681
+ ...hydraMeta.interactive !== void 0 ? { interactive: hydraMeta.interactive } : {}
24452
24682
  });
24453
24683
  } catch (err) {
24454
24684
  if (stdinReservation !== void 0) {
@@ -24575,8 +24805,9 @@ function registerAcpWsEndpoint(app, deps) {
24575
24805
  err.code = JsonRpcErrorCodes.SessionNotFound;
24576
24806
  throw err;
24577
24807
  }
24808
+ const resurrectWithOriginator = resurrectParams.originatingClient ? resurrectParams : { ...resurrectParams, originatingClient: state.clientInfo };
24578
24809
  session = await deps.manager.resurrect({
24579
- ...resurrectParams,
24810
+ ...resurrectWithOriginator,
24580
24811
  onInstallProgress: makeInstallProgressForwarder(connection)
24581
24812
  });
24582
24813
  wireDefaultTransformers(session, deps);
@@ -24633,6 +24864,9 @@ function registerAcpWsEndpoint(app, deps) {
24633
24864
  const session = deps.manager.get(params.sessionId);
24634
24865
  session?.detach(att.clientId);
24635
24866
  state.attached.delete(params.sessionId);
24867
+ if (session) {
24868
+ void deps.manager.reapIfOrphanedNonInteractive(params.sessionId);
24869
+ }
24636
24870
  return { sessionId: params.sessionId, status: "detached" };
24637
24871
  });
24638
24872
  connection.onRequest("session/list", async (raw) => {
@@ -25880,7 +26114,8 @@ async function startDaemon(config, serviceToken) {
25880
26114
  sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
25881
26115
  logger: agentLogger,
25882
26116
  npmRegistry: config.npmRegistry,
25883
- extensionCommands
26117
+ extensionCommands,
26118
+ defaultCwd: config.defaultCwd
25884
26119
  });
25885
26120
  const extensions = new ExtensionManager(extensionList(config), void 0, {
25886
26121
  tokenRegistry: processRegistry
@@ -25901,7 +26136,12 @@ async function startDaemon(config, serviceToken) {
25901
26136
  registerTransformerRoutes(app, transformers);
25902
26137
  registerConfigRoutes(app, {
25903
26138
  defaultAgent: config.defaultAgent,
25904
- defaultCwd: config.defaultCwd
26139
+ defaultCwd: config.defaultCwd,
26140
+ defaultModels: { ...config.defaultModels },
26141
+ ...config.synopsisAgent !== void 0 ? { synopsisAgent: config.synopsisAgent } : {},
26142
+ ...config.synopsisModel !== void 0 ? { synopsisModel: config.synopsisModel } : {},
26143
+ synopsisOnClose: config.synopsisOnClose,
26144
+ defaultTransformers: [...config.defaultTransformers]
25905
26145
  });
25906
26146
  registerAuthRoutes(app, {
25907
26147
  store: sessionTokenStore,
@@ -26389,7 +26629,6 @@ init_remote_target();
26389
26629
  init_remote_url();
26390
26630
  init_session();
26391
26631
  init_discovery();
26392
- init_hydra_version();
26393
26632
  import * as fs19 from "fs/promises";
26394
26633
  import * as path16 from "path";
26395
26634
  init_session_row();
@@ -26398,6 +26637,9 @@ async function runSessionsList(opts = {}) {
26398
26637
  const serviceToken = await loadServiceToken();
26399
26638
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
26400
26639
  const url = new URL(`${baseUrl}/v1/sessions`);
26640
+ if (opts.includeNonInteractive || opts.all) {
26641
+ url.searchParams.set("includeNonInteractive", "true");
26642
+ }
26401
26643
  const response = await fetch(url.toString(), {
26402
26644
  headers: { Authorization: `Bearer ${serviceToken}` }
26403
26645
  });
@@ -26407,13 +26649,11 @@ async function runSessionsList(opts = {}) {
26407
26649
  process.exit(1);
26408
26650
  }
26409
26651
  const body = await response.json();
26410
- const sessionsAfterCatFilter = opts.includeCat ? body.sessions : body.sessions.filter(
26411
- (s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
26412
- );
26652
+ const sessionsAfterInteractiveFilter = body.sessions;
26413
26653
  const host = opts.host ?? "local";
26414
- const hostFiltered = host === "all" ? sessionsAfterCatFilter : host === "local" ? sessionsAfterCatFilter.filter(
26654
+ const hostFiltered = host === "all" ? sessionsAfterInteractiveFilter : host === "local" ? sessionsAfterInteractiveFilter.filter(
26415
26655
  (s) => !s.importedFromMachine || !!s.upstreamSessionId
26416
- ) : sessionsAfterCatFilter.filter(
26656
+ ) : sessionsAfterInteractiveFilter.filter(
26417
26657
  (s) => s.importedFromMachine === host && !s.upstreamSessionId
26418
26658
  );
26419
26659
  if (opts.json) {
@@ -28380,6 +28620,7 @@ function maxLen3(headerCell, values) {
28380
28620
  init_config();
28381
28621
  init_service_token();
28382
28622
  init_paths();
28623
+ import * as fsp12 from "fs/promises";
28383
28624
  async function runAgentsList() {
28384
28625
  const config = await loadConfig();
28385
28626
  const serviceToken = await loadServiceToken();
@@ -28609,6 +28850,104 @@ async function runAgentsLogs(agentId, rest) {
28609
28850
  const logPath = paths.agentLogFile(agentId);
28610
28851
  await runLogTail(logPath, rest, "No log file (agent never ran?)");
28611
28852
  }
28853
+ async function runAgentsSet(agentId, modelId) {
28854
+ const config = await loadConfig();
28855
+ const serviceToken = await loadServiceToken();
28856
+ const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
28857
+ if (!agentId) {
28858
+ const daemonView2 = await fetchDaemonAgentDefaults(baseUrl, serviceToken);
28859
+ const view = daemonView2 ?? readAgentDefaults(await readRawConfig3());
28860
+ process.stdout.write(`${formatDefaultLine(view)}
28861
+ `);
28862
+ return;
28863
+ }
28864
+ let known;
28865
+ try {
28866
+ const r = await fetch(`${baseUrl}/v1/agents`, {
28867
+ headers: { Authorization: `Bearer ${serviceToken}` }
28868
+ });
28869
+ if (r.ok) {
28870
+ const body = await r.json();
28871
+ known = body.agents.map((a) => a.id);
28872
+ }
28873
+ } catch {
28874
+ }
28875
+ if (known !== void 0 && !known.includes(agentId)) {
28876
+ process.stderr.write(
28877
+ `hydra agent set: '${agentId}' is not in the registry. Known ids: ${known.join(", ")}
28878
+ `
28879
+ );
28880
+ process.exit(1);
28881
+ return;
28882
+ }
28883
+ const raw = await readRawConfig3();
28884
+ if (modelId === void 0) {
28885
+ raw.defaultAgent = agentId;
28886
+ await writeRawConfig3(raw);
28887
+ } else {
28888
+ const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
28889
+ models[agentId] = modelId;
28890
+ raw.defaultModels = models;
28891
+ await writeRawConfig3(raw);
28892
+ }
28893
+ const disk = readAgentDefaults(await readRawConfig3());
28894
+ if (modelId !== void 0 && agentId !== disk.agent) {
28895
+ process.stdout.write(
28896
+ `Default model for ${agentId} is now ${modelId}.
28897
+ `
28898
+ );
28899
+ }
28900
+ process.stdout.write(`${formatDefaultLine(disk)}
28901
+ `);
28902
+ const daemonView = await fetchDaemonAgentDefaults(baseUrl, serviceToken);
28903
+ if (daemonView === void 0) {
28904
+ return;
28905
+ }
28906
+ if (daemonView.agent === disk.agent && daemonView.model === disk.model) {
28907
+ return;
28908
+ }
28909
+ process.stdout.write(
28910
+ `Daemon still has ${formatAgentModel(daemonView)} \u2014 restart with \`hydra-acp daemon restart\` to apply.
28911
+ `
28912
+ );
28913
+ }
28914
+ function formatDefaultLine(view) {
28915
+ return `Default agent is ${formatAgentModel(view)}`;
28916
+ }
28917
+ function formatAgentModel(view) {
28918
+ return view.model !== void 0 ? `${view.agent} with ${view.model}` : view.agent;
28919
+ }
28920
+ function readAgentDefaults(raw) {
28921
+ const agent = typeof raw.defaultAgent === "string" ? raw.defaultAgent : "(unset)";
28922
+ const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
28923
+ const rawModel = models[agent];
28924
+ return typeof rawModel === "string" ? { agent, model: rawModel } : { agent };
28925
+ }
28926
+ async function fetchDaemonAgentDefaults(baseUrl, serviceToken) {
28927
+ try {
28928
+ const r = await fetch(`${baseUrl}/v1/config`, {
28929
+ headers: { Authorization: `Bearer ${serviceToken}` }
28930
+ });
28931
+ if (!r.ok) {
28932
+ return void 0;
28933
+ }
28934
+ const body = await r.json();
28935
+ return readAgentDefaults(body);
28936
+ } catch {
28937
+ return void 0;
28938
+ }
28939
+ }
28940
+ async function readRawConfig3() {
28941
+ const raw = await fsp12.readFile(paths.config(), "utf8");
28942
+ return JSON.parse(raw);
28943
+ }
28944
+ async function writeRawConfig3(raw) {
28945
+ await fsp12.writeFile(
28946
+ paths.config(),
28947
+ JSON.stringify(raw, null, 2) + "\n",
28948
+ { encoding: "utf8", mode: 384 }
28949
+ );
28950
+ }
28612
28951
  async function runAgentsRefresh() {
28613
28952
  const config = await loadConfig();
28614
28953
  const serviceToken = await loadServiceToken();
@@ -28768,6 +29107,7 @@ function maxLen5(headerCell, values) {
28768
29107
  }
28769
29108
 
28770
29109
  // src/shim/proxy.ts
29110
+ import * as fs21 from "fs";
28771
29111
  init_config();
28772
29112
  init_remote_target();
28773
29113
  init_daemon_bootstrap();
@@ -28944,6 +29284,8 @@ function isResponse2(msg) {
28944
29284
 
28945
29285
  // src/shim/proxy.ts
28946
29286
  init_permission_pick();
29287
+ init_hydra_version();
29288
+ init_paths();
28947
29289
 
28948
29290
  // src/core/process-title.ts
28949
29291
  init_bin_name();
@@ -29028,6 +29370,7 @@ function wireShim({
29028
29370
  tracker
29029
29371
  }) {
29030
29372
  upstream.onMessage((msg) => {
29373
+ wireLog("daemon\u2192client", msg);
29031
29374
  tracker.observeFromServer(msg);
29032
29375
  if (opts.dangerouslySkipPermissions === true && isPermissionRequest(msg)) {
29033
29376
  void upstream.send({
@@ -29042,7 +29385,12 @@ function wireShim({
29042
29385
  });
29043
29386
  const namingState = { name: opts.name, used: false };
29044
29387
  downstream.onMessage((msg) => {
29388
+ wireLog("client\u2192daemon", msg);
29045
29389
  tracker.observeFromClient(msg);
29390
+ if (isInitializeRequest(msg)) {
29391
+ void upstream.send(normaliseInitializeClientInfo(msg));
29392
+ return;
29393
+ }
29046
29394
  if (isSessionNewRequest(msg)) {
29047
29395
  if (opts.sessionId) {
29048
29396
  void upstream.send(buildAttachFromNew(msg, opts.sessionId));
@@ -29186,6 +29534,29 @@ async function replayAttach(stream, ctx, afterMessageId) {
29186
29534
  function isSessionNewRequest(msg) {
29187
29535
  return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/new";
29188
29536
  }
29537
+ function isInitializeRequest(msg) {
29538
+ return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "initialize";
29539
+ }
29540
+ function normaliseInitializeClientInfo(msg) {
29541
+ const params = msg.params ?? {};
29542
+ const existing = params.clientInfo;
29543
+ const existingObj = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : void 0;
29544
+ const existingName = existingObj && typeof existingObj.name === "string" ? existingObj.name.trim() : "";
29545
+ if (existingName.length > 0) {
29546
+ return msg;
29547
+ }
29548
+ return {
29549
+ ...msg,
29550
+ params: {
29551
+ ...params,
29552
+ clientInfo: {
29553
+ ...existingObj ?? {},
29554
+ name: "hydra-acp-shim",
29555
+ version: HYDRA_VERSION
29556
+ }
29557
+ }
29558
+ };
29559
+ }
29189
29560
  function isPermissionRequest(msg) {
29190
29561
  return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/request_permission";
29191
29562
  }
@@ -29207,6 +29578,40 @@ function rewriteSessionNewWithAgent(msg, agentId) {
29207
29578
  params: { ...params, agentId }
29208
29579
  };
29209
29580
  }
29581
+ var WIRE_LOG_MAX_BYTES = 25 * 1024 * 1024;
29582
+ var wireLogChecked = false;
29583
+ var wireLogPath = null;
29584
+ function wireLog(direction, msg) {
29585
+ if (!process.env.HYDRA_SHIM_WIRE_LOG) {
29586
+ return;
29587
+ }
29588
+ if (!wireLogChecked) {
29589
+ wireLogChecked = true;
29590
+ try {
29591
+ wireLogPath = paths.shimWireLogFile();
29592
+ fs21.mkdirSync(paths.home(), { recursive: true });
29593
+ const st = fs21.statSync(wireLogPath, { throwIfNoEntry: false });
29594
+ if (st && st.size > WIRE_LOG_MAX_BYTES) {
29595
+ fs21.renameSync(wireLogPath, `${wireLogPath}.1`);
29596
+ }
29597
+ } catch {
29598
+ wireLogPath = null;
29599
+ }
29600
+ }
29601
+ if (!wireLogPath) {
29602
+ return;
29603
+ }
29604
+ try {
29605
+ const line = JSON.stringify({
29606
+ t: (/* @__PURE__ */ new Date()).toISOString(),
29607
+ pid: process.pid,
29608
+ dir: direction,
29609
+ msg
29610
+ }) + "\n";
29611
+ fs21.appendFile(wireLogPath, line, () => void 0);
29612
+ } catch {
29613
+ }
29614
+ }
29210
29615
  function injectHydraMeta(msg, additions) {
29211
29616
  const params = msg.params ?? {};
29212
29617
  const existingMeta = params._meta ?? {};
@@ -29359,6 +29764,13 @@ function applyStyle(text, style) {
29359
29764
  // src/cli/commands/cat.ts
29360
29765
  var DEFAULT_STREAM_THRESHOLD = 1 * 1024 * 1024;
29361
29766
  var HYDRA_STDIN_TOOL_PREFIX = "mcp__hydra-acp-stdin__";
29767
+ function catPromptParams(sessionId, prompt) {
29768
+ return {
29769
+ sessionId,
29770
+ prompt,
29771
+ _meta: { [HYDRA_META_KEY]: { ancillary: true } }
29772
+ };
29773
+ }
29362
29774
  function deriveTitleFromPrompt(prompt) {
29363
29775
  if (!prompt) {
29364
29776
  return void 0;
@@ -29539,10 +29951,7 @@ async function runCatLoop(args) {
29539
29951
  return;
29540
29952
  }
29541
29953
  try {
29542
- await conn.request("session/prompt", {
29543
- sessionId,
29544
- prompt: promptBlocks
29545
- });
29954
+ await conn.request("session/prompt", catPromptParams(sessionId, promptBlocks));
29546
29955
  firstChunkSent = true;
29547
29956
  } catch (err) {
29548
29957
  stderr(`hydra-acp cat: prompt failed: ${err.message}
@@ -29765,10 +30174,10 @@ function runStreamingPath(args) {
29765
30174
  }
29766
30175
  await writeChain.catch(() => void 0);
29767
30176
  const promptText = buildStreamPromptText(opts.prompt, open2.capacityBytes);
29768
- const promptDone = conn.request("session/prompt", {
29769
- sessionId,
29770
- prompt: [{ type: "text", text: promptText }]
29771
- }).catch((err) => {
30177
+ const promptDone = conn.request(
30178
+ "session/prompt",
30179
+ catPromptParams(sessionId, [{ type: "text", text: promptText }])
30180
+ ).catch((err) => {
29772
30181
  args.onPromptFailed(
29773
30182
  new Error(`prompt failed: ${err.message}`)
29774
30183
  );
@@ -30098,7 +30507,7 @@ async function main() {
30098
30507
  all: flags.all === true,
30099
30508
  json: flags.json === true,
30100
30509
  host: typeof flags.host === "string" ? flags.host : void 0,
30101
- includeCat: flags["include-cat"] === true
30510
+ includeNonInteractive: flags["include-non-interactive"] === true
30102
30511
  });
30103
30512
  return;
30104
30513
  }
@@ -30262,6 +30671,10 @@ async function main() {
30262
30671
  await runAgentsSync(positional[2]);
30263
30672
  return;
30264
30673
  }
30674
+ if (sub === "set") {
30675
+ await runAgentsSet(positional[2], positional[3]);
30676
+ return;
30677
+ }
30265
30678
  if (sub === "log" || sub === "logs") {
30266
30679
  const agIdx = argv.indexOf(subcommand);
30267
30680
  const tail = argv.slice(agIdx + 2);
@@ -30467,10 +30880,10 @@ function printHelp() {
30467
30880
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
30468
30881
  " hydra-acp daemon stop|restart",
30469
30882
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
30470
- " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-cat]",
30471
- " List sessions (live + 20 most-recent cold; --all for everything; --json emits JSON for scripts).",
30883
+ " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-non-interactive]",
30884
+ " List sessions (live + 20 most-recent cold; --all lifts the cold cap AND surfaces non-interactive sessions; --json emits JSON for scripts).",
30472
30885
  " --host filters by origin machine: 'local' (default) shows only sessions created here, 'all' shows everything, or pass a hostname (e.g. machine-b) to show only imports from that peer.",
30473
- " --include-cat surfaces sessions spawned by `hydra cat` (hidden by default).",
30886
+ " --include-non-interactive surfaces ancillary (e.g. `hydra cat`) or never-prompted sessions while keeping the cold cap (a narrower --all).",
30474
30887
  " hydra-acp session info <id> [--verbose] [--json] [--diff] [--fold] [--no-color] [--no-pager]",
30475
30888
  " Aggregate one session: turn count, tool histogram, files touched, cost/duration, synopsis. --diff appends the session diff under the summary and pages the whole thing on a TTY (inherits --fold).",
30476
30889
  " hydra-acp session diff <id> [--json] [--no-color] [--no-pager] [--fold]",
@@ -30498,6 +30911,7 @@ function printHelp() {
30498
30911
  " hydra-acp agent [list] List agents in the cached registry",
30499
30912
  " hydra-acp agent refresh Force a registry re-fetch",
30500
30913
  " hydra-acp agent install <id> Pre-install <id> from the registry (else lazy on first session)",
30914
+ " hydra-acp agent set [<id>] [model] With no args, report the daemon's current default agent and its default model. With <id>, set <id> as the default agent (config.defaultAgent). With <id> and [model], set the per-agent default model (config.defaultModels[<id>]).",
30501
30915
  " 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`",
30502
30916
  " hydra-acp agent log <id> [-f] [-n N] Tail or follow an agent's spawn/stderr log",
30503
30917
  " hydra-acp auth password [--force] Set the daemon's master password",