@hydra-acp/cli 0.1.23 → 0.1.25

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
@@ -289,7 +289,15 @@ var init_config = __esm({
289
289
  // on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
290
290
  // running. Set false if your terminal renders this obnoxiously or you
291
291
  // just don't want it.
292
- progressIndicator: z.boolean().default(true)
292
+ progressIndicator: z.boolean().default(true),
293
+ // What the unmodified Enter key does in the prompt composer.
294
+ // "enqueue" (default) — Enter enqueues the prompt (sends immediately
295
+ // when idle, queues behind an in-flight turn); Shift+Enter amends
296
+ // the in-flight turn.
297
+ // "amend" — flips the two: Enter amends the in-flight turn,
298
+ // Shift+Enter enqueues. With no turn in flight either key just
299
+ // enqueues, since there's nothing to amend.
300
+ defaultEnterAction: z.enum(["enqueue", "amend"]).default("enqueue")
293
301
  });
294
302
  ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
295
303
  ExtensionBody = z.object({
@@ -333,7 +341,8 @@ var init_config = __esm({
333
341
  mouse: true,
334
342
  logMaxBytes: 5 * 1024 * 1024,
335
343
  cwdColumnMaxWidth: 24,
336
- progressIndicator: true
344
+ progressIndicator: true,
345
+ defaultEnterAction: "enqueue"
337
346
  })
338
347
  });
339
348
  }
@@ -413,6 +422,18 @@ function extractHydraMeta(meta) {
413
422
  if (typeof obj.promptQueueing === "boolean") {
414
423
  out.promptQueueing = obj.promptQueueing;
415
424
  }
425
+ if (typeof obj.promptCancelling === "boolean") {
426
+ out.promptCancelling = obj.promptCancelling;
427
+ }
428
+ if (typeof obj.promptUpdating === "boolean") {
429
+ out.promptUpdating = obj.promptUpdating;
430
+ }
431
+ if (typeof obj.promptAmending === "boolean") {
432
+ out.promptAmending = obj.promptAmending;
433
+ }
434
+ if (typeof obj.promptPipelining === "boolean") {
435
+ out.promptPipelining = obj.promptPipelining;
436
+ }
416
437
  if (Array.isArray(obj.queue)) {
417
438
  const entries = [];
418
439
  for (const raw of obj.queue) {
@@ -467,7 +488,7 @@ function extractHydraMeta(meta) {
467
488
  function mergeMeta(passthrough, ours) {
468
489
  return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
469
490
  }
470
- var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, ProxyInitializeParams;
491
+ var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, AmendPromptParams, AmendPromptResult, PromptAmendedParams, ProxyInitializeParams;
471
492
  var init_types = __esm({
472
493
  "src/acp/types.ts"() {
473
494
  "use strict";
@@ -633,6 +654,34 @@ var init_types = __esm({
633
654
  updated: z3.boolean(),
634
655
  reason: z3.enum(["ok", "not_found", "already_running"])
635
656
  });
657
+ AmendPromptParams = z3.object({
658
+ sessionId: z3.string(),
659
+ targetMessageId: z3.string(),
660
+ prompt: z3.array(z3.unknown()),
661
+ replaceQueue: z3.boolean().optional(),
662
+ onTargetCompleted: z3.enum(["reject", "send_anyway"]).optional()
663
+ });
664
+ AmendPromptResult = z3.object({
665
+ amended: z3.boolean(),
666
+ reason: z3.enum([
667
+ "ok",
668
+ "target_completed",
669
+ "target_cancelled",
670
+ "target_not_found"
671
+ ]),
672
+ // Present when a prompt was sent or replaced: the amendment's id on
673
+ // success, or the regular follow-up's id when onTargetCompleted is
674
+ // "send_anyway" and the daemon forwarded the prompt anyway.
675
+ messageId: z3.string().optional()
676
+ });
677
+ PromptAmendedParams = z3.object({
678
+ sessionId: z3.string(),
679
+ cancelledMessageId: z3.string(),
680
+ newMessageId: z3.string(),
681
+ prompt: z3.array(z3.unknown()),
682
+ originator: PromptOriginatorSchema,
683
+ amendedAt: z3.number()
684
+ });
636
685
  ProxyInitializeParams = z3.object({
637
686
  protocolVersion: z3.number().optional(),
638
687
  proxyInfo: z3.object({
@@ -1154,7 +1203,7 @@ function firstLine(text, max) {
1154
1203
  }
1155
1204
  return void 0;
1156
1205
  }
1157
- var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, DEFAULT_HISTORY_MAX_ENTRIES, Session, STATE_UPDATE_KINDS;
1206
+ var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, DEFAULT_HISTORY_MAX_ENTRIES, RECENTLY_TERMINAL_LIMIT, Session, STATE_UPDATE_KINDS;
1158
1207
  var init_session = __esm({
1159
1208
  "src/core/session.ts"() {
1160
1209
  "use strict";
@@ -1165,6 +1214,7 @@ var init_session = __esm({
1165
1214
  generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
1166
1215
  HYDRA_SESSION_PREFIX = "hydra_session_";
1167
1216
  DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
1217
+ RECENTLY_TERMINAL_LIMIT = 64;
1168
1218
  Session = class {
1169
1219
  sessionId;
1170
1220
  cwd;
@@ -1264,6 +1314,20 @@ var init_session = __esm({
1264
1314
  modelHandlers = [];
1265
1315
  modeHandlers = [];
1266
1316
  usageHandlers = [];
1317
+ // Set by amendPrompt at the start of a cancel-and-resubmit dance.
1318
+ // broadcastTurnComplete reads it to attach the _meta.amended marker
1319
+ // to the cancelled turn's turn_complete notification, and to fire the
1320
+ // dedicated prompt_amended notification. Cleared when the cancelled
1321
+ // turn's task completes (runQueueEntry) OR if the amendment is
1322
+ // cancelled mid-window via cancel_prompt(M2) before drainQueue picks
1323
+ // it up.
1324
+ amendInProgress;
1325
+ // LRU of recently-terminal messageIds → stopReason. Used by
1326
+ // amendPrompt to resolve targets that completed/cancelled before
1327
+ // the amend arrived. Capped at RECENTLY_TERMINAL_LIMIT entries;
1328
+ // older entries fall out and resolve to target_not_found, which is
1329
+ // the correct behavior.
1330
+ recentlyTerminal = /* @__PURE__ */ new Map();
1267
1331
  constructor(init) {
1268
1332
  this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
1269
1333
  this.cwd = init.cwd;
@@ -1693,7 +1757,7 @@ var init_session = __esm({
1693
1757
  );
1694
1758
  }
1695
1759
  }
1696
- broadcastTurnComplete(originatorClientId, response) {
1760
+ broadcastTurnComplete(originatorClientId, response, promptMessageId, wasAmend) {
1697
1761
  const stopReason = response && typeof response === "object" && "stopReason" in response && typeof response.stopReason === "string" ? response.stopReason : void 0;
1698
1762
  const update = {
1699
1763
  sessionUpdate: "turn_complete",
@@ -1702,15 +1766,83 @@ var init_session = __esm({
1702
1766
  if (stopReason !== void 0) {
1703
1767
  update.stopReason = stopReason;
1704
1768
  }
1769
+ const amend = this.amendInProgress;
1770
+ if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
1771
+ update._meta = {
1772
+ "hydra-acp": {
1773
+ amended: {
1774
+ cancelledMessageId: amend.cancelledMessageId,
1775
+ newMessageId: amend.newMessageId
1776
+ }
1777
+ }
1778
+ };
1779
+ }
1705
1780
  this.promptStartedAt = void 0;
1781
+ if (promptMessageId !== void 0 && stopReason !== void 0) {
1782
+ this.recordTerminal(promptMessageId, stopReason);
1783
+ }
1706
1784
  this.recordAndBroadcast(
1707
1785
  "session/update",
1708
1786
  {
1709
1787
  sessionId: this.sessionId,
1710
1788
  update
1711
1789
  },
1712
- originatorClientId
1790
+ wasAmend ? void 0 : originatorClientId
1791
+ );
1792
+ if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
1793
+ this.broadcastPromptAmended(amend);
1794
+ }
1795
+ }
1796
+ // Record that a prompt's turn has ended, with its terminal stopReason.
1797
+ // Used by amendPrompt to resolve targetMessageIds that completed/cancelled
1798
+ // before the amend arrived. LRU-trimmed at RECENTLY_TERMINAL_LIMIT.
1799
+ recordTerminal(messageId, stopReason) {
1800
+ this.recentlyTerminal.set(messageId, {
1801
+ stopReason,
1802
+ terminatedAt: Date.now()
1803
+ });
1804
+ while (this.recentlyTerminal.size > RECENTLY_TERMINAL_LIMIT) {
1805
+ const oldest = this.recentlyTerminal.keys().next().value;
1806
+ if (oldest === void 0) {
1807
+ break;
1808
+ }
1809
+ this.recentlyTerminal.delete(oldest);
1810
+ }
1811
+ }
1812
+ // Fire hydra-acp/prompt_amended for the M1→M2 linkage. The amendment's
1813
+ // current content is read live from the queue entry so any update_prompt
1814
+ // calls during the amend window are reflected. Best-effort: if M2 has
1815
+ // already been cancelled out of the queue by the time we get here, we
1816
+ // skip — the amendInProgress clearing in cancelQueuedPrompt should have
1817
+ // prevented this code path from running in that case.
1818
+ broadcastPromptAmended(amend) {
1819
+ const entry = this.findUserEntry(amend.newMessageId);
1820
+ if (!entry) {
1821
+ return;
1822
+ }
1823
+ const params = {
1824
+ sessionId: this.sessionId,
1825
+ cancelledMessageId: amend.cancelledMessageId,
1826
+ newMessageId: amend.newMessageId,
1827
+ prompt: entry.prompt,
1828
+ originator: entry.originator,
1829
+ amendedAt: Date.now()
1830
+ };
1831
+ this.broadcastQueueNotification(
1832
+ "hydra-acp/prompt_amended",
1833
+ params
1834
+ );
1835
+ }
1836
+ // Look up a user-prompt queue entry by messageId, searching both the
1837
+ // currentEntry slot and the waiting queue.
1838
+ findUserEntry(messageId) {
1839
+ if (this.currentEntry?.messageId === messageId && this.currentEntry.kind === "user") {
1840
+ return this.currentEntry;
1841
+ }
1842
+ const queued = this.promptQueue.find(
1843
+ (e) => e.messageId === messageId && e.kind === "user"
1713
1844
  );
1845
+ return queued?.kind === "user" ? queued : void 0;
1714
1846
  }
1715
1847
  // Total visible-or-running entries: the in-flight head (if any) plus
1716
1848
  // the queue's user-visible waiting entries. Internal entries don't
@@ -1723,9 +1855,9 @@ var init_session = __esm({
1723
1855
  }
1724
1856
  return count;
1725
1857
  }
1726
- broadcastQueueAdded(entry) {
1858
+ broadcastQueueAdded(entry, options) {
1727
1859
  const depth = this.visibleQueueDepth();
1728
- const position = Math.max(0, depth - 1);
1860
+ const position = options?.position ?? Math.max(0, depth - 1);
1729
1861
  const params = {
1730
1862
  sessionId: this.sessionId,
1731
1863
  messageId: entry.messageId,
@@ -1735,6 +1867,11 @@ var init_session = __esm({
1735
1867
  queueDepth: depth,
1736
1868
  enqueuedAt: entry.enqueuedAt
1737
1869
  };
1870
+ if (options?.amending !== void 0) {
1871
+ params._meta = {
1872
+ "hydra-acp": { amending: options.amending }
1873
+ };
1874
+ }
1738
1875
  this.broadcastQueueNotification("hydra-acp/prompt_queue_added", params);
1739
1876
  }
1740
1877
  broadcastQueueUpdated(messageId, prompt) {
@@ -1857,6 +1994,9 @@ var init_session = __esm({
1857
1994
  this.broadcastQueueRemoved(messageId, "cancelled");
1858
1995
  this.persistRewrite();
1859
1996
  }
1997
+ if (this.amendInProgress?.newMessageId === messageId) {
1998
+ this.amendInProgress = void 0;
1999
+ }
1860
2000
  entry.resolve({ stopReason: "cancelled" });
1861
2001
  return { cancelled: true, reason: "ok" };
1862
2002
  }
@@ -1878,6 +2018,143 @@ var init_session = __esm({
1878
2018
  this.persistRewrite();
1879
2019
  return { updated: true, reason: "ok" };
1880
2020
  }
2021
+ // Amend the head prompt: cancel the in-flight turn and submit a
2022
+ // replacement that sits at the head of the queue. Resolves the
2023
+ // request immediately (the caller doesn't wait on cancel-settle).
2024
+ // Honours race outcomes — if the target finished or was cancelled
2025
+ // before this arrived, the request resolves with an outcome explaining
2026
+ // why and (depending on onTargetCompleted) optionally forwards as a
2027
+ // plain prompt. Queued targets are edited in place (same machinery
2028
+ // as updateQueuedPrompt).
2029
+ amendPrompt(clientId, params) {
2030
+ const client = this.clients.get(clientId);
2031
+ if (!client) {
2032
+ throw withCode(
2033
+ new Error("client not attached"),
2034
+ JsonRpcErrorCodes.SessionNotFound
2035
+ );
2036
+ }
2037
+ const { targetMessageId, prompt, replaceQueue, onTargetCompleted } = params;
2038
+ if (this.currentEntry?.messageId === targetMessageId && this.currentEntry.kind === "user" && !this.currentEntry.cancelled && this.amendInProgress === void 0) {
2039
+ return this.amendOnHead(client, prompt, targetMessageId, replaceQueue);
2040
+ }
2041
+ const queuedEntry = this.promptQueue.find(
2042
+ (e) => e.messageId === targetMessageId && e.kind === "user"
2043
+ );
2044
+ if (queuedEntry && queuedEntry.kind === "user" && !queuedEntry.cancelled) {
2045
+ queuedEntry.prompt = prompt;
2046
+ this.broadcastQueueUpdated(targetMessageId, prompt);
2047
+ this.persistRewrite();
2048
+ return { amended: true, reason: "ok", messageId: targetMessageId };
2049
+ }
2050
+ const terminal = this.recentlyTerminal.get(targetMessageId);
2051
+ if (terminal) {
2052
+ if (terminal.stopReason === "cancelled") {
2053
+ return { amended: false, reason: "target_cancelled" };
2054
+ }
2055
+ if (onTargetCompleted === "send_anyway") {
2056
+ const newMessageId = this.enqueueAmendmentAsFollowUp(client, prompt);
2057
+ return {
2058
+ amended: false,
2059
+ reason: "target_completed",
2060
+ messageId: newMessageId
2061
+ };
2062
+ }
2063
+ return { amended: false, reason: "target_completed" };
2064
+ }
2065
+ return { amended: false, reason: "target_not_found" };
2066
+ }
2067
+ // Head-of-queue amendment: splice M2 in front of any waiting entries,
2068
+ // broadcast the amend window's queue_added with the amending hint,
2069
+ // mark amendInProgress so the cancelled turn's broadcastTurnComplete
2070
+ // attaches the _meta marker and fires prompt_amended, then fire the
2071
+ // upstream session/cancel without awaiting it. drainQueue is already
2072
+ // running on the head; when its session/prompt returns, it advances
2073
+ // to M2 in the normal way.
2074
+ amendOnHead(client, prompt, targetMessageId, replaceQueue) {
2075
+ const newMessageId = generateMessageId();
2076
+ const originator = { clientId: client.clientId };
2077
+ if (client.clientInfo?.name) {
2078
+ originator.name = client.clientInfo.name;
2079
+ }
2080
+ if (client.clientInfo?.version) {
2081
+ originator.version = client.clientInfo.version;
2082
+ }
2083
+ if (replaceQueue) {
2084
+ const survivors = [];
2085
+ for (const entry2 of this.promptQueue) {
2086
+ if (entry2.kind === "user" && !entry2.cancelled) {
2087
+ entry2.cancelled = true;
2088
+ this.broadcastQueueRemoved(entry2.messageId, "cancelled");
2089
+ entry2.resolve({ stopReason: "cancelled" });
2090
+ continue;
2091
+ }
2092
+ survivors.push(entry2);
2093
+ }
2094
+ this.promptQueue = survivors;
2095
+ }
2096
+ const entry = {
2097
+ kind: "user",
2098
+ messageId: newMessageId,
2099
+ originator,
2100
+ clientId: client.clientId,
2101
+ prompt,
2102
+ enqueuedAt: Date.now(),
2103
+ cancelled: false,
2104
+ wasAmend: true,
2105
+ // No-op resolve/reject: there's no client request awaiting M2's
2106
+ // session/prompt response. The amend_prompt request has already
2107
+ // returned by this point. drainQueue calls these unconditionally
2108
+ // when runQueueEntry settles; making them no-ops is safe.
2109
+ resolve: () => void 0,
2110
+ reject: () => void 0
2111
+ };
2112
+ this.promptQueue.unshift(entry);
2113
+ this.persistRewrite();
2114
+ this.broadcastQueueAdded(entry, {
2115
+ amending: targetMessageId,
2116
+ position: 1
2117
+ });
2118
+ this.amendInProgress = {
2119
+ cancelledMessageId: targetMessageId,
2120
+ newMessageId
2121
+ };
2122
+ void this.agent.connection.notify("session/cancel", { sessionId: this.upstreamSessionId }).catch(() => void 0);
2123
+ return {
2124
+ amended: true,
2125
+ reason: "ok",
2126
+ messageId: newMessageId
2127
+ };
2128
+ }
2129
+ // Send the amendment as a plain follow-up prompt — used when the
2130
+ // target already completed and the caller opted in to send_anyway.
2131
+ // Returns the new prompt's messageId so the result can surface it.
2132
+ enqueueAmendmentAsFollowUp(client, prompt) {
2133
+ const messageId = generateMessageId();
2134
+ const originator = { clientId: client.clientId };
2135
+ if (client.clientInfo?.name) {
2136
+ originator.name = client.clientInfo.name;
2137
+ }
2138
+ if (client.clientInfo?.version) {
2139
+ originator.version = client.clientInfo.version;
2140
+ }
2141
+ const entry = {
2142
+ kind: "user",
2143
+ messageId,
2144
+ originator,
2145
+ clientId: client.clientId,
2146
+ prompt,
2147
+ enqueuedAt: Date.now(),
2148
+ cancelled: false,
2149
+ resolve: () => void 0,
2150
+ reject: () => void 0
2151
+ };
2152
+ this.promptQueue.push(entry);
2153
+ this.persistRewrite();
2154
+ this.broadcastQueueAdded(entry);
2155
+ void this.drainQueue();
2156
+ return messageId;
2157
+ }
1881
2158
  async cancel(clientId) {
1882
2159
  const client = this.clients.get(clientId);
1883
2160
  if (!client) {
@@ -2736,6 +3013,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2736
3013
  try {
2737
3014
  const result = await this.runQueueEntry(next);
2738
3015
  next.resolve(result);
3016
+ await Promise.resolve();
2739
3017
  } catch (err) {
2740
3018
  next.reject(err);
2741
3019
  } finally {
@@ -2772,12 +3050,33 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2772
3050
  }
2773
3051
  );
2774
3052
  } catch (err) {
2775
- this.broadcastTurnComplete(entry.clientId, { stopReason: "error" });
3053
+ this.broadcastTurnComplete(
3054
+ entry.clientId,
3055
+ { stopReason: "error" },
3056
+ entry.messageId,
3057
+ entry.wasAmend
3058
+ );
3059
+ this.clearAmendIfMatches(entry.messageId);
2776
3060
  throw err;
2777
3061
  }
2778
- this.broadcastTurnComplete(entry.clientId, response);
3062
+ this.broadcastTurnComplete(
3063
+ entry.clientId,
3064
+ response,
3065
+ entry.messageId,
3066
+ entry.wasAmend
3067
+ );
3068
+ this.clearAmendIfMatches(entry.messageId);
2779
3069
  return response;
2780
3070
  }
3071
+ // Clear amendInProgress once the cancelled turn's task has fully
3072
+ // settled. broadcastTurnComplete needs the marker still set when it
3073
+ // fires, so the clear must happen *after*. Called from runQueueEntry's
3074
+ // settle path for both success and error.
3075
+ clearAmendIfMatches(messageId) {
3076
+ if (this.amendInProgress?.cancelledMessageId === messageId) {
3077
+ this.amendInProgress = void 0;
3078
+ }
3079
+ }
2781
3080
  };
2782
3081
  STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
2783
3082
  "session_info_update",
@@ -3427,7 +3726,16 @@ function mapModel(u) {
3427
3726
  }
3428
3727
  function mapTurnComplete(u) {
3429
3728
  const stopReason = readString(u, "stopReason");
3430
- return stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" };
3729
+ const meta = u._meta;
3730
+ const amended = meta?.["hydra-acp"]?.amended !== void 0 && meta["hydra-acp"].amended !== null;
3731
+ const out = { kind: "turn-complete" };
3732
+ if (stopReason !== void 0) {
3733
+ out.stopReason = stopReason;
3734
+ }
3735
+ if (amended) {
3736
+ out.amended = true;
3737
+ }
3738
+ return out;
3431
3739
  }
3432
3740
  function extractContentText(content) {
3433
3741
  if (typeof content === "string") {
@@ -4067,7 +4375,7 @@ var init_session_row = __esm({
4067
4375
 
4068
4376
  // src/cli/commands/sessions.ts
4069
4377
  import * as fs17 from "fs/promises";
4070
- import * as path10 from "path";
4378
+ import * as path11 from "path";
4071
4379
  async function runSessionsList(opts = {}) {
4072
4380
  const config = await loadConfig();
4073
4381
  const serviceToken = await loadServiceToken();
@@ -4195,7 +4503,7 @@ async function runSessionsExport(id, outPath) {
4195
4503
  return;
4196
4504
  }
4197
4505
  const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
4198
- await fs17.mkdir(path10.dirname(path10.resolve(resolved)), { recursive: true });
4506
+ await fs17.mkdir(path11.dirname(path11.resolve(resolved)), { recursive: true });
4199
4507
  await fs17.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
4200
4508
  process.stdout.write(`Wrote ${resolved}
4201
4509
  `);
@@ -4214,7 +4522,7 @@ async function runSessionsTranscript(idOrFile, outPath) {
4214
4522
  const bundle = decodeBundleOrExit(localFile.raw);
4215
4523
  body = bundleToMarkdown(bundle);
4216
4524
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4217
- defaultName = `${path10.basename(idOrFile, path10.extname(idOrFile))}-${stamp}.md`;
4525
+ defaultName = `${path11.basename(idOrFile, path11.extname(idOrFile))}-${stamp}.md`;
4218
4526
  } else {
4219
4527
  const config = await loadConfig();
4220
4528
  const serviceToken = await loadServiceToken();
@@ -4243,7 +4551,7 @@ async function runSessionsTranscript(idOrFile, outPath) {
4243
4551
  return;
4244
4552
  }
4245
4553
  const resolved = outPath === "." ? defaultName : outPath;
4246
- await fs17.mkdir(path10.dirname(path10.resolve(resolved)), { recursive: true });
4554
+ await fs17.mkdir(path11.dirname(path11.resolve(resolved)), { recursive: true });
4247
4555
  await fs17.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
4248
4556
  process.stdout.write(`Wrote ${resolved}
4249
4557
  `);
@@ -4284,7 +4592,7 @@ async function runSessionsImport(file, opts = {}) {
4284
4592
  }
4285
4593
  let cwdOverride;
4286
4594
  if (opts.cwd !== void 0) {
4287
- const resolved = path10.resolve(opts.cwd);
4595
+ const resolved = path11.resolve(opts.cwd);
4288
4596
  try {
4289
4597
  const stat4 = await fs17.stat(resolved);
4290
4598
  if (!stat4.isDirectory()) {
@@ -4955,6 +5263,10 @@ async function pickSession(term, opts) {
4955
5263
  const indicatorRow = () => startRow + 3 + viewportSize;
4956
5264
  const sessionRow = (sessionIdx) => startRow + 3 + (sessionIdx - scrollOffset);
4957
5265
  const renderFromScratch = () => {
5266
+ if (mode === "help") {
5267
+ renderHelp();
5268
+ return;
5269
+ }
4958
5270
  computeLayout();
4959
5271
  adjustScroll();
4960
5272
  startRow = 1;
@@ -4969,6 +5281,21 @@ async function pickSession(term, opts) {
4969
5281
  paintIndicator();
4970
5282
  term("\n");
4971
5283
  };
5284
+ const renderHelp = () => {
5285
+ term.moveTo(1, 1).eraseDisplayBelow();
5286
+ term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
5287
+ for (const entry of HELP_ENTRIES) {
5288
+ if (entry === null) {
5289
+ term("\n");
5290
+ continue;
5291
+ }
5292
+ const [keys, desc] = entry;
5293
+ term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
5294
+ term.noFormat(desc)("\n");
5295
+ }
5296
+ term("\n");
5297
+ term.dim.noFormat(" press any key to dismiss")("\n");
5298
+ };
4972
5299
  const repaintNewItem = () => {
4973
5300
  term.moveTo(1, startRow).eraseLineAfter();
4974
5301
  paintNewItem();
@@ -5099,6 +5426,16 @@ async function pickSession(term, opts) {
5099
5426
  if (mode === "busy") {
5100
5427
  return;
5101
5428
  }
5429
+ if (mode === "help") {
5430
+ if (name === "CTRL_C") {
5431
+ cleanup();
5432
+ resolve5({ kind: "abort" });
5433
+ return;
5434
+ }
5435
+ mode = "normal";
5436
+ renderFromScratch();
5437
+ return;
5438
+ }
5102
5439
  if (mode === "confirm-kill" || mode === "confirm-delete") {
5103
5440
  if (data?.isCharacter && (name === "y" || name === "Y")) {
5104
5441
  const kind = mode === "confirm-kill" ? "kill" : "delete";
@@ -5114,6 +5451,11 @@ async function pickSession(term, opts) {
5114
5451
  return;
5115
5452
  }
5116
5453
  clearTransient();
5454
+ if (!searchActive && data?.isCharacter && name === "?") {
5455
+ mode = "help";
5456
+ renderHelp();
5457
+ return;
5458
+ }
5117
5459
  if (searchActive) {
5118
5460
  if (data?.isCharacter) {
5119
5461
  searchTerm += name;
@@ -5262,6 +5604,7 @@ async function pickSession(term, opts) {
5262
5604
  }
5263
5605
  case "ESCAPE":
5264
5606
  case "CTRL_C":
5607
+ case "CTRL_D":
5265
5608
  cleanup();
5266
5609
  resolve5({ kind: "abort" });
5267
5610
  return;
@@ -5303,7 +5646,7 @@ function matchesSearch(s, term) {
5303
5646
  }
5304
5647
  return false;
5305
5648
  }
5306
- var ROW_PREFIX_WIDTH;
5649
+ var ROW_PREFIX_WIDTH, HELP_KEYS_WIDTH, HELP_ENTRIES;
5307
5650
  var init_picker = __esm({
5308
5651
  "src/tui/picker.ts"() {
5309
5652
  "use strict";
@@ -5312,13 +5655,31 @@ var init_picker = __esm({
5312
5655
  init_session();
5313
5656
  init_discovery();
5314
5657
  ROW_PREFIX_WIDTH = 2;
5658
+ HELP_KEYS_WIDTH = 20;
5659
+ HELP_ENTRIES = [
5660
+ ["\u2191 / \u2193 or n / p", "navigate"],
5661
+ ["PgUp / PgDn", "page up / page down"],
5662
+ ["Home / End", "first / last"],
5663
+ ["Enter", "open selected session (or create new)"],
5664
+ null,
5665
+ ["/", "search sessions"],
5666
+ ["o", "toggle cwd-only filter"],
5667
+ ["r", "refresh from daemon"],
5668
+ null,
5669
+ ["k", "kill the selected live session"],
5670
+ ["d", "delete the selected cold session"],
5671
+ null,
5672
+ ["c", "create new session"],
5673
+ ["?", "toggle this help"],
5674
+ ["q / Esc / ^C / ^D", "quit picker (detach)"]
5675
+ ];
5315
5676
  }
5316
5677
  });
5317
5678
 
5318
5679
  // src/tui/attachments.ts
5319
- import path11 from "path";
5680
+ import path12 from "path";
5320
5681
  function mimeFromExtension(p) {
5321
- return EXTENSION_TO_MIME[path11.extname(p).toLowerCase()] ?? null;
5682
+ return EXTENSION_TO_MIME[path12.extname(p).toLowerCase()] ?? null;
5322
5683
  }
5323
5684
  function isSupportedImagePath(p) {
5324
5685
  return mimeFromExtension(p) !== null;
@@ -5894,6 +6255,18 @@ function mapKeyName(name) {
5894
6255
  case "ALT_ENTER":
5895
6256
  case "META_ENTER":
5896
6257
  return "alt-enter";
6258
+ case "SHIFT_ENTER":
6259
+ return "shift-enter";
6260
+ case "CTRL_ENTER":
6261
+ return "ctrl-enter";
6262
+ case "CTRL_J":
6263
+ return "ctrl-enter";
6264
+ case "ALT_B":
6265
+ case "META_B":
6266
+ return "alt-b";
6267
+ case "ALT_F":
6268
+ case "META_F":
6269
+ return "alt-f";
5897
6270
  case "CTRL_T":
5898
6271
  return "ctrl-t";
5899
6272
  case "SHIFT_TAB":
@@ -5928,6 +6301,8 @@ function mapKeyName(name) {
5928
6301
  return "ctrl-e";
5929
6302
  case "CTRL_F":
5930
6303
  return "ctrl-f";
6304
+ case "CTRL_G":
6305
+ return "ctrl-g";
5931
6306
  case "CTRL_K":
5932
6307
  return "ctrl-k";
5933
6308
  case "CTRL_L":
@@ -5956,7 +6331,7 @@ function mapKeyName(name) {
5956
6331
  return null;
5957
6332
  }
5958
6333
  }
5959
- var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
6334
+ var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_HELP_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
5960
6335
  var init_screen = __esm({
5961
6336
  "src/tui/screen.ts"() {
5962
6337
  "use strict";
@@ -5970,6 +6345,7 @@ var init_screen = __esm({
5970
6345
  MAX_PROMPT_ROWS = 8;
5971
6346
  MAX_QUEUED_ROWS = 5;
5972
6347
  MAX_PERMISSION_ROWS = 12;
6348
+ MAX_HELP_ROWS = 30;
5973
6349
  MAX_COMPLETION_ROWS = 6;
5974
6350
  MAX_CHIP_ROWS = 4;
5975
6351
  CONFIRM_PROMPT_ROWS = 2;
@@ -6032,6 +6408,7 @@ var init_screen = __esm({
6032
6408
  lastFrameH = 0;
6033
6409
  permissionPrompt = null;
6034
6410
  confirmPrompt = null;
6411
+ helpPrompt = null;
6035
6412
  completions = [];
6036
6413
  // Scrollback offset: 0 = pinned to bottom (live), N = N wrapped lines
6037
6414
  // above the bottom. Mouse wheel and PgUp/PgDn adjust this; new content
@@ -6060,7 +6437,7 @@ var init_screen = __esm({
6060
6437
  banner = {
6061
6438
  status: "ready",
6062
6439
  currentMode: void 0,
6063
- hint: "\u21E7\u21E5 mode \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
6440
+ hint: "\u21E7\u21E5 mode \xB7 \u2303P pick \xB7 \u2303G guide \xB7 \u2303D detach",
6064
6441
  queued: 0
6065
6442
  };
6066
6443
  sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
@@ -6153,12 +6530,15 @@ var init_screen = __esm({
6153
6530
  this.term.fullscreen(false);
6154
6531
  this.term("\n");
6155
6532
  }
6156
- // Enables bracketed paste mode on the terminal and rewires stdin so we
6157
- // see the \x1b[200~/\x1b[201~ markers BEFORE terminal-kit's key
6158
- // parser. Non-paste data is forwarded to terminal-kit unchanged; paste
6159
- // content is buffered and dispatched as a single "paste" KeyEvent.
6533
+ // Enables bracketed paste mode + modifyOtherKeys on the terminal and
6534
+ // rewires stdin so we see the \x1b[200~/\x1b[201~ paste markers and
6535
+ // CSI-u modified-key sequences (Shift+Enter etc.) BEFORE terminal-kit's
6536
+ // key parser. Non-special data is forwarded to terminal-kit unchanged.
6160
6537
  installBracketedPaste() {
6161
6538
  process.stdout.write("\x1B[?2004h");
6539
+ process.stdout.write("\x1B[>4;2m");
6540
+ process.stdout.write("\x1B[>5;1m");
6541
+ process.stdout.write("\x1B[>1u");
6162
6542
  const t = this.term;
6163
6543
  if (!t.stdin || typeof t.onStdin !== "function") {
6164
6544
  return;
@@ -6169,6 +6549,9 @@ var init_screen = __esm({
6169
6549
  }
6170
6550
  uninstallBracketedPaste() {
6171
6551
  process.stdout.write("\x1B[?2004l");
6552
+ process.stdout.write("\x1B[>4;0m");
6553
+ process.stdout.write("\x1B[>5;0m");
6554
+ process.stdout.write("\x1B[<u");
6172
6555
  const t = this.term;
6173
6556
  if (!t.stdin || this.terminalKitStdinHandler === null) {
6174
6557
  return;
@@ -6181,6 +6564,38 @@ var init_screen = __esm({
6181
6564
  }
6182
6565
  handleRawStdin(chunk) {
6183
6566
  let text = chunk.toString("binary");
6567
+ if (!this.pasteActive) {
6568
+ const markers = [
6569
+ { seq: "\x1B[13;2u", name: "shift-enter" },
6570
+ { seq: "\x1B[27;2;13~", name: "shift-enter" },
6571
+ { seq: "\x1B[13;5u", name: "ctrl-enter" },
6572
+ { seq: "\x1B[27;5;13~", name: "ctrl-enter" },
6573
+ // Bare LF — universal fallback for terminals without
6574
+ // modifyOtherKeys / kitty protocol. Last so the longer escape
6575
+ // sequences match first and we don't double-fire.
6576
+ { seq: "\n", name: "ctrl-enter" }
6577
+ ];
6578
+ for (const { seq, name } of markers) {
6579
+ if (text.includes(seq)) {
6580
+ const parts = text.split(seq);
6581
+ for (let i = 0; i < parts.length; i++) {
6582
+ if (parts[i].length > 0) {
6583
+ this.handleRawStdin(Buffer.from(parts[i], "binary"));
6584
+ }
6585
+ if (i < parts.length - 1) {
6586
+ this.onKey([{ type: "key", name }]);
6587
+ }
6588
+ }
6589
+ return;
6590
+ }
6591
+ }
6592
+ }
6593
+ this.handleRawStdinSegment(text);
6594
+ }
6595
+ // Inner stdin-segment handler — paste-marker detection and forwarding
6596
+ // to terminal-kit. Split out so shift-enter interception can call it
6597
+ // for the non-shift-enter portions of a mixed chunk.
6598
+ handleRawStdinSegment(text) {
6184
6599
  const startMarker = "\x1B[200~";
6185
6600
  const endMarker = "\x1B[201~";
6186
6601
  while (text.length > 0) {
@@ -6623,6 +7038,16 @@ var init_screen = __esm({
6623
7038
  this.confirmPrompt = spec ? { ...spec } : null;
6624
7039
  this.repaint();
6625
7040
  }
7041
+ // Multi-row help cheatsheet that takes over the prompt area. Used by
7042
+ // the ^G hotkey to surface every binding without dropping the user
7043
+ // out of the session. Pass null to dismiss.
7044
+ setHelpPrompt(spec) {
7045
+ this.helpPrompt = spec ? { ...spec, entries: [...spec.entries] } : null;
7046
+ this.repaint();
7047
+ }
7048
+ isHelpPromptActive() {
7049
+ return this.helpPrompt !== null;
7050
+ }
6626
7051
  // Slash-command completion list shown directly above the separator. App
6627
7052
  // calls this after each keystroke; pass [] to dismiss. Suppressed when
6628
7053
  // the permission modal is active (the modal owns the prompt area).
@@ -7158,7 +7583,7 @@ var init_screen = __esm({
7158
7583
  this.repaint();
7159
7584
  }
7160
7585
  completionRows() {
7161
- if (this.permissionPrompt) {
7586
+ if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
7162
7587
  return 0;
7163
7588
  }
7164
7589
  return Math.min(MAX_COMPLETION_ROWS, this.completions.length);
@@ -7302,6 +7727,10 @@ var init_screen = __esm({
7302
7727
  this.drawConfirmPrompt();
7303
7728
  return;
7304
7729
  }
7730
+ if (this.helpPrompt) {
7731
+ this.drawHelpPrompt();
7732
+ return;
7733
+ }
7305
7734
  const w = this.term.width;
7306
7735
  const room = Math.max(1, w - 2);
7307
7736
  const state = this.dispatcher.state();
@@ -7351,6 +7780,58 @@ var init_screen = __esm({
7351
7780
  this.term.dim(` ${truncate(spec.hint, w - 2)}`);
7352
7781
  });
7353
7782
  }
7783
+ drawHelpPrompt() {
7784
+ const spec = this.helpPrompt;
7785
+ if (!spec) {
7786
+ return;
7787
+ }
7788
+ const w = this.term.width;
7789
+ const rows = this.helpRows();
7790
+ const top = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
7791
+ let row = top;
7792
+ const writeRow = (sig, paint) => {
7793
+ if (row >= top + rows) {
7794
+ return;
7795
+ }
7796
+ this.paintRow(row, sig, paint);
7797
+ row += 1;
7798
+ };
7799
+ writeRow(`help|t|${w}|${spec.title}`, () => {
7800
+ this.term.brightYellow(` \u2753 ${truncate(spec.title, w - 5)}`);
7801
+ });
7802
+ const keysWidth = Math.min(
7803
+ 24,
7804
+ Math.max(
7805
+ ...spec.entries.map((e) => e === null ? 0 : e[0].length),
7806
+ 4
7807
+ )
7808
+ );
7809
+ for (const entry of spec.entries) {
7810
+ if (row >= top + rows - 1) {
7811
+ break;
7812
+ }
7813
+ if (entry === null) {
7814
+ writeRow(`help|sep|${w}|${row}`, () => void 0);
7815
+ continue;
7816
+ }
7817
+ const [keys, desc] = entry;
7818
+ const paddedKeys = keys.padEnd(keysWidth);
7819
+ writeRow(`help|e|${w}|${keys}|${desc}`, () => {
7820
+ this.term(" ");
7821
+ this.term.brightCyan.noFormat(paddedKeys);
7822
+ this.term.noFormat(` ${truncate(desc, w - 2 - keysWidth - 1)}`);
7823
+ });
7824
+ }
7825
+ writeRow(`help|hint|${w}|${spec.hint}`, () => {
7826
+ this.term.dim(` ${truncate(spec.hint, w - 2)}`);
7827
+ });
7828
+ }
7829
+ helpRows() {
7830
+ if (!this.helpPrompt) {
7831
+ return 0;
7832
+ }
7833
+ return Math.min(MAX_HELP_ROWS, 2 + this.helpPrompt.entries.length);
7834
+ }
7354
7835
  drawPermissionPrompt() {
7355
7836
  const spec = this.permissionPrompt;
7356
7837
  if (!spec) {
@@ -7460,6 +7941,12 @@ var init_screen = __esm({
7460
7941
  this.term.moveTo(2, top2);
7461
7942
  return;
7462
7943
  }
7944
+ if (this.helpPrompt) {
7945
+ const rows = this.helpRows();
7946
+ const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
7947
+ this.term.moveTo(2, top2);
7948
+ return;
7949
+ }
7463
7950
  if (this.scrollbackSearch) {
7464
7951
  this.term.hideCursor(true);
7465
7952
  return;
@@ -7486,6 +7973,9 @@ var init_screen = __esm({
7486
7973
  if (this.confirmPrompt) {
7487
7974
  return CONFIRM_PROMPT_ROWS;
7488
7975
  }
7976
+ if (this.helpPrompt) {
7977
+ return this.helpRows();
7978
+ }
7489
7979
  const w = this.term.width;
7490
7980
  const room = Math.max(1, w - 2);
7491
7981
  const state = this.dispatcher.state();
@@ -7806,6 +8296,9 @@ var init_input = __esm({
7806
8296
  switch (name) {
7807
8297
  case "enter":
7808
8298
  return this.send();
8299
+ case "shift-enter":
8300
+ case "ctrl-enter":
8301
+ return this.amend();
7809
8302
  case "alt-enter":
7810
8303
  this.insertNewline();
7811
8304
  return [];
@@ -7844,6 +8337,14 @@ var init_input = __esm({
7844
8337
  case "ctrl-f":
7845
8338
  this.moveRight();
7846
8339
  return [];
8340
+ case "ctrl-g":
8341
+ return [{ type: "show-help" }];
8342
+ case "alt-b":
8343
+ this.moveWordBackward();
8344
+ return [];
8345
+ case "alt-f":
8346
+ this.moveWordForward();
8347
+ return [];
7847
8348
  case "ctrl-k":
7848
8349
  this.killToEnd();
7849
8350
  return [];
@@ -7984,22 +8485,64 @@ var init_input = __esm({
7984
8485
  this.setCurrentLine(line + next);
7985
8486
  }
7986
8487
  }
8488
+ // ^U: kill from cursor to start of current line. At col 0 with a line
8489
+ // above:
8490
+ // - If the current line is empty, collapse it (kill just the
8491
+ // newline) so the cursor lands at the end of the previous line.
8492
+ // Don't slurp that line's contents.
8493
+ // - Otherwise, kill the previous line entirely + the joining
8494
+ // newline, so ^U from the start of a non-empty line walks up
8495
+ // line-by-line.
8496
+ // Single-line behavior is unchanged.
7987
8497
  killLine() {
7988
- const line = this.currentLine();
7989
- const killed = line.slice(0, this.col);
7990
- if (killed.length > 0) {
7991
- this.killBuffer = killed;
8498
+ if (this.col > 0) {
8499
+ const line = this.currentLine();
8500
+ this.killBuffer = line.slice(0, this.col);
8501
+ this.setCurrentLine(line.slice(this.col));
8502
+ this.col = 0;
8503
+ return;
7992
8504
  }
7993
- this.setCurrentLine(line.slice(this.col));
7994
- this.col = 0;
8505
+ if (this.row === 0) {
8506
+ return;
8507
+ }
8508
+ if (this.currentLine().length === 0) {
8509
+ this.killBuffer = "\n";
8510
+ this.buffer.splice(this.row, 1);
8511
+ this.row -= 1;
8512
+ this.col = this.currentLine().length;
8513
+ return;
8514
+ }
8515
+ const prev = this.buffer[this.row - 1] ?? "";
8516
+ this.killBuffer = prev + "\n";
8517
+ this.buffer.splice(this.row - 1, 1);
8518
+ this.row -= 1;
7995
8519
  }
8520
+ // ^K: kill from cursor to end of current line. At end-of-line with a
8521
+ // line below:
8522
+ // - If the current line is empty, collapse it (kill just the
8523
+ // newline) so what was the next line takes its place. Don't slurp
8524
+ // that line's contents.
8525
+ // - Otherwise, kill the joining newline + the entire next line, so
8526
+ // ^K from the end of a non-empty line walks down line-by-line.
8527
+ // Single-line behavior is unchanged.
7996
8528
  killToEnd() {
7997
8529
  const line = this.currentLine();
7998
- const killed = line.slice(this.col);
7999
- if (killed.length > 0) {
8000
- this.killBuffer = killed;
8530
+ if (this.col < line.length) {
8531
+ this.killBuffer = line.slice(this.col);
8532
+ this.setCurrentLine(line.slice(0, this.col));
8533
+ return;
8534
+ }
8535
+ if (this.row >= this.buffer.length - 1) {
8536
+ return;
8001
8537
  }
8002
- this.setCurrentLine(line.slice(0, this.col));
8538
+ if (line.length === 0) {
8539
+ this.killBuffer = "\n";
8540
+ this.buffer.splice(this.row, 1);
8541
+ return;
8542
+ }
8543
+ const next = this.buffer[this.row + 1] ?? "";
8544
+ this.killBuffer = "\n" + next;
8545
+ this.buffer.splice(this.row + 1, 1);
8003
8546
  }
8004
8547
  killWord() {
8005
8548
  const line = this.currentLine();
@@ -8047,6 +8590,44 @@ var init_input = __esm({
8047
8590
  this.col = 0;
8048
8591
  }
8049
8592
  }
8593
+ moveWordBackward() {
8594
+ if (this.col === 0) {
8595
+ if (this.row === 0) {
8596
+ return;
8597
+ }
8598
+ this.row -= 1;
8599
+ this.col = this.currentLine().length;
8600
+ return;
8601
+ }
8602
+ const line = this.currentLine();
8603
+ let i = this.col;
8604
+ while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
8605
+ i -= 1;
8606
+ }
8607
+ while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
8608
+ i -= 1;
8609
+ }
8610
+ this.col = i;
8611
+ }
8612
+ moveWordForward() {
8613
+ const line = this.currentLine();
8614
+ if (this.col >= line.length) {
8615
+ if (this.row >= this.buffer.length - 1) {
8616
+ return;
8617
+ }
8618
+ this.row += 1;
8619
+ this.col = 0;
8620
+ return;
8621
+ }
8622
+ let i = this.col;
8623
+ while (i < line.length && /\s/.test(line[i] ?? "")) {
8624
+ i += 1;
8625
+ }
8626
+ while (i < line.length && !/\s/.test(line[i] ?? "")) {
8627
+ i += 1;
8628
+ }
8629
+ this.col = i;
8630
+ }
8050
8631
  // Up walks the navigation stack from newest to oldest: pending queue
8051
8632
  // items first (so the user can edit something they just enqueued),
8052
8633
  // then prompt history. Cursor movement within a multi-line buffer
@@ -8310,6 +8891,31 @@ var init_input = __esm({
8310
8891
  this.clearBuffer();
8311
8892
  return [{ type: "send", text, planMode, attachments }];
8312
8893
  }
8894
+ // Shift+Enter: amend the in-flight turn. Editing a queued slot
8895
+ // delegates to the existing queue-edit / queue-remove path — Shift+Enter
8896
+ // there has no special meaning since the entry is already queued (not
8897
+ // running). With an empty draft and no attachments we emit nothing
8898
+ // (no-op). Otherwise emit an "amend" effect; the app decides whether
8899
+ // to route through amend_prompt or fall through to a regular send.
8900
+ amend() {
8901
+ const text = this.bufferText();
8902
+ if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
8903
+ const index = this.queueIndex;
8904
+ const attachments2 = [...this.attachments];
8905
+ this.clearBuffer();
8906
+ if (text.trim().length === 0) {
8907
+ return [{ type: "queue-remove", index }];
8908
+ }
8909
+ return [{ type: "queue-edit", index, text, attachments: attachments2 }];
8910
+ }
8911
+ if (text.trim().length === 0 && this.attachments.length === 0) {
8912
+ return [];
8913
+ }
8914
+ const planMode = this.planMode;
8915
+ const attachments = [...this.attachments];
8916
+ this.clearBuffer();
8917
+ return [{ type: "amend", text, planMode, attachments }];
8918
+ }
8313
8919
  // Home: jump to the very start of the prompt buffer. If we're already
8314
8920
  // there, fall through to scrolling the scrollback to its top.
8315
8921
  handleHome() {
@@ -8362,7 +8968,7 @@ var init_input = __esm({
8362
8968
  import { spawn as nodeSpawn } from "child_process";
8363
8969
  import fs18 from "fs/promises";
8364
8970
  import os4 from "os";
8365
- import path12 from "path";
8971
+ import path13 from "path";
8366
8972
  async function readClipboard(envIn = {}) {
8367
8973
  const env = { ...defaultEnv, ...envIn };
8368
8974
  if (env.platform === "darwin") {
@@ -8377,7 +8983,7 @@ async function readClipboard(envIn = {}) {
8377
8983
  };
8378
8984
  }
8379
8985
  async function readMacOS(env) {
8380
- const tmpPath = path12.join(
8986
+ const tmpPath = path13.join(
8381
8987
  env.tmpdir(),
8382
8988
  `hydra-clipboard-${Date.now()}-${process.pid}.png`
8383
8989
  );
@@ -8421,26 +9027,34 @@ async function readLinux(env) {
8421
9027
  reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
8422
9028
  };
8423
9029
  }
8424
- try {
8425
- const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
8426
- if (buf.length > 0) {
8427
- if (buf.length > MAX_ATTACHMENT_BYTES) {
9030
+ const targets = await listTargets(env, tool);
9031
+ const imageMime = pickImageTarget(targets);
9032
+ if (imageMime) {
9033
+ try {
9034
+ const buf = await runCapture(
9035
+ env.spawn,
9036
+ tool.cmd,
9037
+ tool.imageArgs(imageMime)
9038
+ );
9039
+ if (buf.length > 0) {
9040
+ if (buf.length > MAX_ATTACHMENT_BYTES) {
9041
+ return {
9042
+ ok: false,
9043
+ reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
9044
+ };
9045
+ }
8428
9046
  return {
8429
- ok: false,
8430
- reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
9047
+ ok: true,
9048
+ kind: "image",
9049
+ attachment: {
9050
+ mimeType: imageMime,
9051
+ data: buf.toString("base64"),
9052
+ sizeBytes: buf.length
9053
+ }
8431
9054
  };
8432
9055
  }
8433
- return {
8434
- ok: true,
8435
- kind: "image",
8436
- attachment: {
8437
- mimeType: "image/png",
8438
- data: buf.toString("base64"),
8439
- sizeBytes: buf.length
8440
- }
8441
- };
9056
+ } catch {
8442
9057
  }
8443
- } catch {
8444
9058
  }
8445
9059
  try {
8446
9060
  const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
@@ -8460,7 +9074,8 @@ async function detectLinuxTool(env) {
8460
9074
  if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
8461
9075
  return {
8462
9076
  cmd: "wl-paste",
8463
- imageArgs: ["-t", "image/png"],
9077
+ listTargetsArgs: ["--list-types"],
9078
+ imageArgs: (mime) => ["-t", mime],
8464
9079
  // -n: drop trailing newline wl-paste adds by default. We further
8465
9080
  // normalize line endings below, but this avoids a spurious
8466
9081
  // empty trailing row from a single-line clipboard text.
@@ -8470,12 +9085,30 @@ async function detectLinuxTool(env) {
8470
9085
  if (env.env.DISPLAY && await which(env, "xclip")) {
8471
9086
  return {
8472
9087
  cmd: "xclip",
8473
- imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
9088
+ listTargetsArgs: ["-selection", "clipboard", "-t", "TARGETS", "-o"],
9089
+ imageArgs: (mime) => ["-selection", "clipboard", "-t", mime, "-o"],
8474
9090
  textArgs: ["-selection", "clipboard", "-o"]
8475
9091
  };
8476
9092
  }
8477
9093
  return null;
8478
9094
  }
9095
+ function pickImageTarget(targets) {
9096
+ const offered = new Set(targets.map((t) => t.toLowerCase()));
9097
+ for (const mime of SUPPORTED_IMAGE_MIMES) {
9098
+ if (offered.has(mime)) {
9099
+ return mime;
9100
+ }
9101
+ }
9102
+ return null;
9103
+ }
9104
+ async function listTargets(env, tool) {
9105
+ try {
9106
+ const buf = await runCapture(env.spawn, tool.cmd, tool.listTargetsArgs);
9107
+ return buf.toString("utf-8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9108
+ } catch {
9109
+ return [];
9110
+ }
9111
+ }
8479
9112
  function normalizeText(text) {
8480
9113
  return text.replace(/\r\n?/g, "\n");
8481
9114
  }
@@ -8570,7 +9203,7 @@ function runCapture(spawn6, cmd, args) {
8570
9203
  });
8571
9204
  });
8572
9205
  }
8573
- var defaultEnv;
9206
+ var defaultEnv, SUPPORTED_IMAGE_MIMES;
8574
9207
  var init_clipboard = __esm({
8575
9208
  "src/tui/clipboard.ts"() {
8576
9209
  "use strict";
@@ -8581,6 +9214,12 @@ var init_clipboard = __esm({
8581
9214
  spawn: nodeSpawn,
8582
9215
  tmpdir: os4.tmpdir
8583
9216
  };
9217
+ SUPPORTED_IMAGE_MIMES = [
9218
+ "image/png",
9219
+ "image/jpeg",
9220
+ "image/gif",
9221
+ "image/webp"
9222
+ ];
8584
9223
  }
8585
9224
  });
8586
9225
 
@@ -8721,7 +9360,8 @@ function parseAgentMarkdown(text) {
8721
9360
  codeBuffer = [];
8722
9361
  codeLang = "";
8723
9362
  };
8724
- for (const line of lines) {
9363
+ for (let i = 0; i < lines.length; i++) {
9364
+ const line = lines[i];
8725
9365
  const fence = line.match(/^\s*```\s*(\w*)\s*$/);
8726
9366
  if (fence) {
8727
9367
  if (!inCode) {
@@ -8749,6 +9389,19 @@ function parseAgentMarkdown(text) {
8749
9389
  });
8750
9390
  continue;
8751
9391
  }
9392
+ const next = lines[i + 1];
9393
+ if (line.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(line).length === parseTableRow(next).length) {
9394
+ const header = parseTableRow(line);
9395
+ const body = [];
9396
+ let j = i + 2;
9397
+ while (j < lines.length && lines[j].includes("|")) {
9398
+ body.push(parseTableRow(lines[j]));
9399
+ j++;
9400
+ }
9401
+ out.push(...formatTable(header, body));
9402
+ i = j - 1;
9403
+ continue;
9404
+ }
8752
9405
  const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
8753
9406
  if (bullet) {
8754
9407
  const indent = bullet[1] ?? "";
@@ -8783,6 +9436,70 @@ function parseAgentMarkdown(text) {
8783
9436
  }
8784
9437
  return out;
8785
9438
  }
9439
+ function parseTableRow(line) {
9440
+ let s = line.trim();
9441
+ if (s.startsWith("|")) {
9442
+ s = s.slice(1);
9443
+ }
9444
+ if (s.endsWith("|")) {
9445
+ s = s.slice(0, -1);
9446
+ }
9447
+ return s.split("|").map((c) => c.trim());
9448
+ }
9449
+ function isTableSeparatorLine(line) {
9450
+ if (!line.includes("|")) {
9451
+ return false;
9452
+ }
9453
+ const cells = parseTableRow(line);
9454
+ if (cells.length === 0) {
9455
+ return false;
9456
+ }
9457
+ return cells.every((c) => /^:?-+:?$/.test(c));
9458
+ }
9459
+ function formatTable(header, body) {
9460
+ const cols = header.length;
9461
+ const widths = new Array(cols).fill(0);
9462
+ for (let c = 0; c < cols; c++) {
9463
+ widths[c] = header[c]?.length ?? 0;
9464
+ }
9465
+ for (const row of body) {
9466
+ for (let c = 0; c < cols; c++) {
9467
+ const cell = row[c] ?? "";
9468
+ if (cell.length > widths[c]) {
9469
+ widths[c] = cell.length;
9470
+ }
9471
+ }
9472
+ }
9473
+ const renderRow = (cells, style) => {
9474
+ const padded = [];
9475
+ for (let c = 0; c < cols; c++) {
9476
+ const cell = cells[c] ?? "";
9477
+ const w = widths[c];
9478
+ const marked = applyInlineMarkup(cell);
9479
+ padded.push(marked + " ".repeat(Math.max(0, w - cell.length)));
9480
+ }
9481
+ return {
9482
+ prefix: " ",
9483
+ body: padded.join(" \u2502 "),
9484
+ bodyStyle: style
9485
+ };
9486
+ };
9487
+ const out = [];
9488
+ out.push(renderRow(header, "heading-3"));
9489
+ const rules = [];
9490
+ for (let c = 0; c < cols; c++) {
9491
+ rules.push("\u2500".repeat(widths[c]));
9492
+ }
9493
+ out.push({
9494
+ prefix: " ",
9495
+ body: rules.join("\u2500\u253C\u2500"),
9496
+ bodyStyle: "dim"
9497
+ });
9498
+ for (const row of body) {
9499
+ out.push(renderRow(row, "agent"));
9500
+ }
9501
+ return out;
9502
+ }
8786
9503
  function highlightFencedBlock(lang, lines) {
8787
9504
  if (lang.length === 0 || !supportsLanguage(lang)) {
8788
9505
  return lines.map((body) => ({ body, ansi: false }));
@@ -8981,7 +9698,7 @@ import { appendFileSync, statSync, renameSync } from "fs";
8981
9698
  import { nanoid as nanoid3 } from "nanoid";
8982
9699
  import termkit from "terminal-kit";
8983
9700
  import fs19 from "fs/promises";
8984
- import path13 from "path";
9701
+ import path14 from "path";
8985
9702
  async function runTuiApp(opts) {
8986
9703
  const config = await loadConfig();
8987
9704
  const serviceToken = await ensureServiceToken();
@@ -9051,6 +9768,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9051
9768
  }
9052
9769
  };
9053
9770
  let pendingTurns = 0;
9771
+ let currentHeadMessageId;
9054
9772
  let sessionBusySince = null;
9055
9773
  let sessionElapsedTimer = null;
9056
9774
  const adjustPendingTurns = (delta) => {
@@ -9087,14 +9805,27 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9087
9805
  };
9088
9806
  let screenRef = null;
9089
9807
  let dispatcherRef = null;
9090
- conn.onNotification("session/update", (params) => {
9091
- if (teardownStarted) {
9092
- return;
9093
- }
9808
+ let lastSeenMessageId = void 0;
9809
+ let reconnectReplayBuffer = null;
9810
+ const STATE_UPDATE_KINDS2 = /* @__PURE__ */ new Set([
9811
+ "session_info_update",
9812
+ "current_model_update",
9813
+ "current_mode_update",
9814
+ "available_commands_update",
9815
+ "available_modes_update",
9816
+ "usage_update"
9817
+ ]);
9818
+ const handleSessionUpdate = (params) => {
9094
9819
  const { update } = params ?? {};
9095
9820
  const event = mapUpdate(update);
9096
9821
  debugLogUpdate(update, event);
9097
9822
  const rawTag = update?.sessionUpdate;
9823
+ if (typeof rawTag === "string" && !STATE_UPDATE_KINDS2.has(rawTag)) {
9824
+ const u = update ?? {};
9825
+ if (typeof u.messageId === "string") {
9826
+ lastSeenMessageId = u.messageId;
9827
+ }
9828
+ }
9098
9829
  if (rawTag === "prompt_received") {
9099
9830
  adjustPendingTurns(1);
9100
9831
  } else if (event?.kind === "turn-complete") {
@@ -9106,6 +9837,16 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9106
9837
  }
9107
9838
  appendRender(event);
9108
9839
  maybeDismissPermissionByToolUpdate(update);
9840
+ };
9841
+ conn.onNotification("session/update", (params) => {
9842
+ if (teardownStarted) {
9843
+ return;
9844
+ }
9845
+ if (reconnectReplayBuffer !== null) {
9846
+ reconnectReplayBuffer.push(params);
9847
+ return;
9848
+ }
9849
+ handleSessionUpdate(params);
9109
9850
  });
9110
9851
  conn.onNotification("hydra-acp/session_closed", () => {
9111
9852
  if (teardownStarted) {
@@ -9119,13 +9860,29 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9119
9860
  screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
9120
9861
  }
9121
9862
  });
9863
+ const amendPendingPaintTimers = /* @__PURE__ */ new Map();
9864
+ const AMEND_CHIP_DISPLAY_DELAY_MS = 200;
9122
9865
  conn.onNotification("hydra-acp/prompt_queue_added", (params) => {
9123
9866
  if (teardownStarted) return;
9124
9867
  const p = params ?? {};
9125
9868
  if (typeof p.messageId !== "string") return;
9126
- queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
9127
- if (screenRef && dispatcherRef) {
9128
- refreshQueueDisplay();
9869
+ const isAmendPending = typeof p._meta?.["hydra-acp"]?.amending === "string";
9870
+ if (isAmendPending) {
9871
+ const mid = p.messageId;
9872
+ const prompt = p.prompt;
9873
+ const timer = setTimeout(() => {
9874
+ amendPendingPaintTimers.delete(mid);
9875
+ queueCache.set(mid, chipFromPrompt(mid, prompt));
9876
+ if (screenRef && dispatcherRef) {
9877
+ refreshQueueDisplay();
9878
+ }
9879
+ }, AMEND_CHIP_DISPLAY_DELAY_MS);
9880
+ amendPendingPaintTimers.set(mid, timer);
9881
+ } else {
9882
+ queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
9883
+ if (screenRef && dispatcherRef) {
9884
+ refreshQueueDisplay();
9885
+ }
9129
9886
  }
9130
9887
  if (ownClientId !== void 0 && p.originator?.clientId === ownClientId) {
9131
9888
  const echo = pendingEchoes.shift();
@@ -9170,6 +9927,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9170
9927
  if (teardownStarted) return;
9171
9928
  const p = params ?? {};
9172
9929
  if (typeof p.messageId !== "string") return;
9930
+ if (p.reason === "started") {
9931
+ currentHeadMessageId = p.messageId;
9932
+ }
9933
+ const pendingTimer = amendPendingPaintTimers.get(p.messageId);
9934
+ if (pendingTimer !== void 0) {
9935
+ clearTimeout(pendingTimer);
9936
+ amendPendingPaintTimers.delete(p.messageId);
9937
+ }
9173
9938
  const hadChip = queueCache.delete(p.messageId);
9174
9939
  if (hadChip && screenRef && dispatcherRef) {
9175
9940
  refreshQueueDisplay();
@@ -9184,9 +9949,26 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9184
9949
  text: echo.text,
9185
9950
  attachments: echo.attachments
9186
9951
  });
9952
+ currentTurnEcho = echo;
9187
9953
  }
9188
9954
  }
9189
9955
  });
9956
+ conn.onNotification("hydra-acp/prompt_amended", (params) => {
9957
+ if (teardownStarted) return;
9958
+ const p = params ?? {};
9959
+ if (typeof p.cancelledMessageId !== "string") return;
9960
+ const cancelledId = p.cancelledMessageId;
9961
+ amendedMessageIds.add(cancelledId);
9962
+ if (currentTurnEcho !== null && currentTurnEcho.messageId !== void 0 && currentTurnEcho.messageId === cancelledId) {
9963
+ appendRender({
9964
+ kind: "turn-complete",
9965
+ stopReason: "cancelled",
9966
+ amended: true
9967
+ });
9968
+ currentTurnEcho = null;
9969
+ amendedMessageIds.delete(cancelledId);
9970
+ }
9971
+ });
9190
9972
  const handlePermissionResolved = (update) => {
9191
9973
  const u = update ?? {};
9192
9974
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
@@ -9294,6 +10076,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9294
10076
  let upstreamSessionId;
9295
10077
  let agentInfoName;
9296
10078
  let agentAcceptsImages = true;
10079
+ let daemonSupportsAmend = false;
9297
10080
  try {
9298
10081
  const initResult = await conn.request("initialize", {
9299
10082
  protocolVersion: ACP_PROTOCOL_VERSION,
@@ -9308,6 +10091,8 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9308
10091
  if (imageCap === false) {
9309
10092
  agentAcceptsImages = false;
9310
10093
  }
10094
+ const hydraMeta = extractHydraMeta(initResult?._meta ?? void 0);
10095
+ daemonSupportsAmend = hydraMeta.promptAmending === true;
9311
10096
  } catch {
9312
10097
  }
9313
10098
  let resolvedSessionId = ctx.sessionId;
@@ -9420,6 +10205,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9420
10205
  if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
9421
10206
  continue;
9422
10207
  }
10208
+ if (tryHandleHelpKey(ev)) {
10209
+ continue;
10210
+ }
9423
10211
  if (tryHandleScrollbackSearchKey(ev)) {
9424
10212
  continue;
9425
10213
  }
@@ -9705,6 +10493,40 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9705
10493
  }
9706
10494
  return true;
9707
10495
  };
10496
+ const buildHelpEntries = () => {
10497
+ const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
10498
+ const amendDesc = "amend the in-flight turn (cancel + replace)";
10499
+ const head = config.tui.defaultEnterAction === "amend" ? [
10500
+ ["Enter", amendDesc],
10501
+ ["Ctrl+Enter / Shift+Enter", enqueueDesc]
10502
+ ] : [
10503
+ ["Enter", enqueueDesc],
10504
+ ["Ctrl+Enter / Shift+Enter", amendDesc]
10505
+ ];
10506
+ return [...head, ...HELP_ENTRIES_TAIL];
10507
+ };
10508
+ const toggleHelpModal = () => {
10509
+ if (screen.isHelpPromptActive()) {
10510
+ screen.setHelpPrompt(null);
10511
+ return;
10512
+ }
10513
+ screen.setHelpPrompt({
10514
+ title: "Hotkeys",
10515
+ entries: buildHelpEntries(),
10516
+ hint: "any key dismisses \xB7 /help lists commands"
10517
+ });
10518
+ };
10519
+ const tryHandleHelpKey = (ev) => {
10520
+ if (!screen.isHelpPromptActive()) {
10521
+ return false;
10522
+ }
10523
+ if (ev.type === "key" && ev.name === "ctrl-g") {
10524
+ screen.setHelpPrompt(null);
10525
+ return true;
10526
+ }
10527
+ screen.setHelpPrompt(null);
10528
+ return true;
10529
+ };
9708
10530
  const teardown = () => {
9709
10531
  teardownStarted = true;
9710
10532
  process.off("SIGINT", sigintHandler);
@@ -9793,7 +10615,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9793
10615
  const handleEffect = (effect) => {
9794
10616
  switch (effect.type) {
9795
10617
  case "send":
9796
- enqueuePrompt(effect.text, effect.attachments);
10618
+ if (config.tui.defaultEnterAction === "amend") {
10619
+ amendPrompt(effect.text, effect.attachments);
10620
+ } else {
10621
+ enqueuePrompt(effect.text, effect.attachments);
10622
+ }
10623
+ return;
10624
+ case "amend":
10625
+ if (config.tui.defaultEnterAction === "amend") {
10626
+ enqueuePrompt(effect.text, effect.attachments);
10627
+ } else {
10628
+ amendPrompt(effect.text, effect.attachments);
10629
+ }
9797
10630
  return;
9798
10631
  case "queue-edit": {
9799
10632
  const mid = queueMessageIdAt(effect.index);
@@ -9885,6 +10718,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9885
10718
  toolsExpanded = !toolsExpanded;
9886
10719
  renderToolsBlock();
9887
10720
  return;
10721
+ case "show-help":
10722
+ toggleHelpModal();
10723
+ return;
9888
10724
  case "escalate-search":
9889
10725
  screen.enterScrollbackSearch();
9890
10726
  screen.updateScrollbackSearchTerm(effect.query);
@@ -9924,7 +10760,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9924
10760
  }
9925
10761
  const mimeType = mimeFromExtension(token);
9926
10762
  if (!mimeType) {
9927
- screen.notify(`unsupported image type: ${path13.basename(token)}`);
10763
+ screen.notify(`unsupported image type: ${path14.basename(token)}`);
9928
10764
  continue;
9929
10765
  }
9930
10766
  try {
@@ -9938,13 +10774,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9938
10774
  dispatcher.addAttachment({
9939
10775
  mimeType,
9940
10776
  data: buf.toString("base64"),
9941
- name: path13.basename(token),
10777
+ name: path14.basename(token),
9942
10778
  sizeBytes: buf.length
9943
10779
  });
9944
10780
  added++;
9945
10781
  } catch (err) {
9946
10782
  screen.notify(
9947
- `cannot read ${path13.basename(token)}: ${err.message}`
10783
+ `cannot read ${path14.basename(token)}: ${err.message}`
9948
10784
  );
9949
10785
  }
9950
10786
  }
@@ -9998,6 +10834,8 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9998
10834
  const queueCache = /* @__PURE__ */ new Map();
9999
10835
  const pendingEchoes = [];
10000
10836
  const ownPendingByMid = /* @__PURE__ */ new Map();
10837
+ const amendedMessageIds = /* @__PURE__ */ new Set();
10838
+ let currentTurnEcho = null;
10001
10839
  const refreshQueueDisplay = () => {
10002
10840
  const entries = [...queueCache.values()];
10003
10841
  const displayTexts = entries.map(formatQueueChipText);
@@ -10031,6 +10869,75 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10031
10869
  saveHistory(historyFile, history).catch(() => void 0);
10032
10870
  void runPrompt(text, attachments);
10033
10871
  };
10872
+ const amendPrompt = (text, attachments) => {
10873
+ screen.scrollToBottom();
10874
+ if (handleBuiltinCommand(text)) {
10875
+ return;
10876
+ }
10877
+ history = appendEntry(history, text);
10878
+ dispatcher.setHistory(history);
10879
+ saveHistory(historyFile, history).catch(() => void 0);
10880
+ if (!daemonSupportsAmend || currentHeadMessageId === void 0) {
10881
+ void runPrompt(text, attachments);
10882
+ return;
10883
+ }
10884
+ const target = currentHeadMessageId;
10885
+ const blocks = [];
10886
+ if (text.length > 0) {
10887
+ blocks.push({ type: "text", text });
10888
+ }
10889
+ for (const a of attachments) {
10890
+ blocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
10891
+ }
10892
+ const echo = { text, attachments, flushed: false };
10893
+ pendingEchoes.push(echo);
10894
+ const popEcho = () => {
10895
+ const idx = pendingEchoes.indexOf(echo);
10896
+ if (idx >= 0) {
10897
+ pendingEchoes.splice(idx, 1);
10898
+ }
10899
+ if (echo.messageId !== void 0) {
10900
+ ownPendingByMid.delete(echo.messageId);
10901
+ }
10902
+ };
10903
+ conn.request("hydra-acp/amend_prompt", {
10904
+ sessionId: resolvedSessionId,
10905
+ targetMessageId: target,
10906
+ prompt: blocks
10907
+ }).then((raw) => {
10908
+ const res = raw;
10909
+ if (res.amended && res.reason === "ok") {
10910
+ adjustPendingTurns(1);
10911
+ return;
10912
+ }
10913
+ popEcho();
10914
+ if (res.reason === "target_completed") {
10915
+ screen.notify(
10916
+ "previous response finished \u2014 press Enter to send as a new turn"
10917
+ );
10918
+ dispatcher.setBuffer(text, attachments);
10919
+ screen.refreshPrompt();
10920
+ return;
10921
+ }
10922
+ if (res.reason === "target_cancelled") {
10923
+ screen.notify("amend skipped \u2014 previous turn was cancelled");
10924
+ dispatcher.setBuffer(text, attachments);
10925
+ screen.refreshPrompt();
10926
+ return;
10927
+ }
10928
+ if (res.reason === "target_not_found") {
10929
+ screen.notify("amend skipped \u2014 no matching prompt");
10930
+ dispatcher.setBuffer(text, attachments);
10931
+ screen.refreshPrompt();
10932
+ return;
10933
+ }
10934
+ }).catch((err) => {
10935
+ popEcho();
10936
+ screen.notify(`amend failed: ${err.message}`);
10937
+ dispatcher.setBuffer(text, attachments);
10938
+ screen.refreshPrompt();
10939
+ });
10940
+ };
10034
10941
  const handleModeToggle = async (_on) => {
10035
10942
  if (agentModes.length === 0) {
10036
10943
  screen.notify("no modes advertised by agent");
@@ -10254,10 +11161,20 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10254
11161
  } finally {
10255
11162
  turnInFlight = null;
10256
11163
  adjustPendingTurns(-1);
10257
- if (echo.flushed) {
10258
- appendRender(
10259
- stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
10260
- );
11164
+ if (echo.flushed && currentTurnEcho === echo) {
11165
+ const wasAmended = echo.messageId !== void 0 && amendedMessageIds.has(echo.messageId);
11166
+ if (wasAmended && echo.messageId !== void 0) {
11167
+ amendedMessageIds.delete(echo.messageId);
11168
+ }
11169
+ const tc = { kind: "turn-complete" };
11170
+ if (stopReason !== void 0) {
11171
+ tc.stopReason = stopReason;
11172
+ }
11173
+ if (wasAmended) {
11174
+ tc.amended = true;
11175
+ }
11176
+ appendRender(tc);
11177
+ currentTurnEcho = null;
10261
11178
  }
10262
11179
  if (pendingPrefill !== null) {
10263
11180
  const { text: pt, attachments: pa } = pendingPrefill;
@@ -10440,6 +11357,11 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10440
11357
  }
10441
11358
  if (event.kind === "user-text") {
10442
11359
  closeAgentText();
11360
+ if (toolsBlockStartedAt !== null) {
11361
+ toolsBlockEndedAt = Date.now();
11362
+ renderToolsBlock();
11363
+ }
11364
+ currentTurnEcho = null;
10443
11365
  screen.ensureSeparator();
10444
11366
  const formatted2 = formatEvent(event);
10445
11367
  if (formatted2.length > 0) {
@@ -10494,8 +11416,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10494
11416
  screen.appendLines(formatted);
10495
11417
  }
10496
11418
  if (event.kind === "turn-complete") {
11419
+ currentHeadMessageId = void 0;
10497
11420
  closeAgentText();
10498
- if (lastPlanEvent !== null && event.stopReason !== void 0 && event.stopReason !== "end_turn") {
11421
+ const effectiveStopReason = event.amended ? "amended" : event.stopReason;
11422
+ if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
10499
11423
  const lines = formatEvent({ ...lastPlanEvent, stopped: true });
10500
11424
  if (lines.length > 0) {
10501
11425
  screen.upsertLines("plan", lines);
@@ -10505,9 +11429,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10505
11429
  screen.clearKey("plan");
10506
11430
  if (toolsBlockStartedAt !== null) {
10507
11431
  toolsBlockEndedAt = Date.now();
10508
- toolsBlockStopReason = event.stopReason ?? null;
11432
+ toolsBlockStopReason = effectiveStopReason ?? null;
10509
11433
  renderToolsBlock();
10510
11434
  screen.clearKey("tools");
11435
+ } else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
11436
+ screen.appendLines([
11437
+ {
11438
+ prefix: "\u26A0 ",
11439
+ prefixStyle: "tool-status-fail",
11440
+ body: `turn ended: ${effectiveStopReason}`,
11441
+ bodyStyle: "tool-status-fail"
11442
+ }
11443
+ ]);
10511
11444
  }
10512
11445
  toolStates.clear();
10513
11446
  toolCallOrder.length = 0;
@@ -10555,23 +11488,21 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10555
11488
  resolve5({ outcome: { outcome: "cancelled" } });
10556
11489
  }
10557
11490
  closeAgentText();
10558
- if (toolsBlockStartedAt !== null) {
10559
- toolsBlockEndedAt = Date.now();
10560
- toolsBlockStopReason = null;
10561
- renderToolsBlock();
10562
- screen.clearKey("tools");
10563
- toolStates.clear();
10564
- toolCallOrder.length = 0;
10565
- toolsBlockStartedAt = null;
10566
- toolsBlockEndedAt = null;
10567
- toolsBlockStopReason = null;
10568
- toolsExpanded = false;
10569
- }
10570
- screen.clearKey("plan");
10571
- lastPlanEvent = null;
10572
- if (pendingTurns > 0) {
10573
- adjustPendingTurns(-pendingTurns);
11491
+ };
11492
+ const markToolsBlockRecoveryFailed = () => {
11493
+ if (toolsBlockStartedAt === null) {
11494
+ return;
10574
11495
  }
11496
+ toolsBlockEndedAt = Date.now();
11497
+ toolsBlockStopReason = "reconnect-recovery-failed";
11498
+ renderToolsBlock();
11499
+ screen.clearKey("tools");
11500
+ toolStates.clear();
11501
+ toolCallOrder.length = 0;
11502
+ toolsBlockStartedAt = null;
11503
+ toolsBlockEndedAt = null;
11504
+ toolsBlockStopReason = null;
11505
+ toolsExpanded = false;
10575
11506
  };
10576
11507
  onDisconnectHook = () => {
10577
11508
  screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
@@ -10595,13 +11526,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10595
11526
  await stream.request(initReq);
10596
11527
  } catch {
10597
11528
  }
11529
+ const useAfterMessage = lastSeenMessageId !== void 0;
10598
11530
  const attachReq = {
10599
11531
  jsonrpc: "2.0",
10600
11532
  id: `tui-reattach-${nanoid3()}`,
10601
11533
  method: "session/attach",
10602
11534
  params: {
10603
11535
  sessionId: resolvedSessionId,
10604
- historyPolicy: "none",
11536
+ historyPolicy: useAfterMessage ? "after_message" : "none",
11537
+ ...useAfterMessage ? { afterMessageId: lastSeenMessageId } : {},
10605
11538
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
10606
11539
  ...upstreamSessionId !== void 0 ? {
10607
11540
  _meta: {
@@ -10616,19 +11549,46 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10616
11549
  } : {}
10617
11550
  }
10618
11551
  };
11552
+ reconnectReplayBuffer = [];
11553
+ let appliedPolicy;
11554
+ let attachErr;
10619
11555
  try {
10620
11556
  const resp = await stream.request(attachReq);
10621
11557
  if (resp.error) {
10622
11558
  throw new Error(resp.error.message);
10623
11559
  }
11560
+ const result = resp.result ?? {};
11561
+ if (typeof result.historyPolicy === "string") {
11562
+ appliedPolicy = result.historyPolicy;
11563
+ }
10624
11564
  } catch (err) {
11565
+ attachErr = err;
11566
+ }
11567
+ const buffered2 = reconnectReplayBuffer ?? [];
11568
+ reconnectReplayBuffer = null;
11569
+ if (attachErr) {
11570
+ markToolsBlockRecoveryFailed();
10625
11571
  screen.appendLines([
10626
11572
  {
10627
11573
  prefix: " ",
10628
- body: `reattach failed: ${err.message}`,
11574
+ body: `reattach failed: ${attachErr.message}`,
11575
+ bodyStyle: "tool-status-fail"
11576
+ }
11577
+ ]);
11578
+ } else if (useAfterMessage && appliedPolicy !== "after_message") {
11579
+ markToolsBlockRecoveryFailed();
11580
+ screen.appendLines([
11581
+ {
11582
+ prefix: "\u26A0 ",
11583
+ prefixStyle: "tool-status-fail",
11584
+ body: "reconnect couldn't replay events since last seen \u2014 scrollback may be incomplete",
10629
11585
  bodyStyle: "tool-status-fail"
10630
11586
  }
10631
11587
  ]);
11588
+ } else {
11589
+ for (const params of buffered2) {
11590
+ handleSessionUpdate(params);
11591
+ }
10632
11592
  }
10633
11593
  screen.setBanner({
10634
11594
  status: pendingTurns > 0 ? "busy" : "ready",
@@ -10735,7 +11695,7 @@ function rotateIfBig(target) {
10735
11695
  } catch {
10736
11696
  }
10737
11697
  }
10738
- var logMaxBytes;
11698
+ var HELP_ENTRIES_TAIL, logMaxBytes;
10739
11699
  var init_app = __esm({
10740
11700
  "src/tui/app.ts"() {
10741
11701
  "use strict";
@@ -10759,6 +11719,33 @@ var init_app = __esm({
10759
11719
  init_completion();
10760
11720
  init_render_update();
10761
11721
  init_format();
11722
+ HELP_ENTRIES_TAIL = [
11723
+ ["Alt+Enter", "newline in prompt"],
11724
+ ["Shift+Tab", "cycle agent modes (plan / accept-edits / etc.)"],
11725
+ ["Tab", "indent \xB7 slash-command completion"],
11726
+ null,
11727
+ ["\u2191 / \u2193", "prompt history \xB7 queue navigation"],
11728
+ ["\u2190/\u2192 Home/End", "cursor movement"],
11729
+ ["Alt+B / Alt+F", "word back / forward"],
11730
+ ["^A / ^E", "line start / end"],
11731
+ ["^W / ^U / ^K", "kill word / line / to end"],
11732
+ ["^Y", "yank last kill"],
11733
+ null,
11734
+ ["^P", "switch session (picker)"],
11735
+ ["^T", "next live session"],
11736
+ ["^V", "paste image from clipboard"],
11737
+ ["^O", "expand / collapse tools block"],
11738
+ null,
11739
+ ["^R / ^S", "history reverse / forward search"],
11740
+ ["PgUp / PgDn", "scroll scrollback"],
11741
+ ["Mouse wheel", "scroll scrollback (when mouse capture is on)"],
11742
+ null,
11743
+ ["^C", "cancel turn (twice to exit)"],
11744
+ ["Esc", "cancel turn and prefill draft"],
11745
+ ["^D", "exit (or delete-forward in prompt)"],
11746
+ ["^L", "force full redraw"],
11747
+ ["^G", "toggle this help"]
11748
+ ];
10762
11749
  logMaxBytes = 5 * 1024 * 1024;
10763
11750
  }
10764
11751
  });
@@ -10891,13 +11878,13 @@ New token: ${newToken}
10891
11878
  init_paths();
10892
11879
  init_config();
10893
11880
  init_service_token();
10894
- import * as fsp6 from "fs/promises";
11881
+ import * as fsp7 from "fs/promises";
10895
11882
  import { setTimeout as sleep2 } from "timers/promises";
10896
11883
 
10897
11884
  // src/daemon/server.ts
10898
11885
  init_config();
10899
11886
  import * as fs15 from "fs";
10900
- import * as fsp4 from "fs/promises";
11887
+ import * as fsp5 from "fs/promises";
10901
11888
  import Fastify from "fastify";
10902
11889
  import websocketPlugin from "@fastify/websocket";
10903
11890
  import pino from "pino";
@@ -11284,10 +12271,12 @@ var RegistryDocument = z2.object({
11284
12271
  extensions: z2.array(z2.unknown()).optional()
11285
12272
  });
11286
12273
  var Registry = class {
11287
- constructor(config) {
12274
+ constructor(config, options = {}) {
11288
12275
  this.config = config;
12276
+ this.options = options;
11289
12277
  }
11290
12278
  config;
12279
+ options;
11291
12280
  cache;
11292
12281
  async load() {
11293
12282
  if (this.cache && this.isFresh(this.cache.fetchedAt)) {
@@ -11337,7 +12326,12 @@ var Registry = class {
11337
12326
  }
11338
12327
  const raw = await response.json();
11339
12328
  const data = RegistryDocument.parse(raw);
11340
- return { fetchedAt: Date.now(), raw, data };
12329
+ const cached2 = { fetchedAt: Date.now(), raw, data };
12330
+ const hook = this.options.onFetched;
12331
+ if (hook) {
12332
+ void Promise.resolve().then(() => hook(data)).catch(() => void 0);
12333
+ }
12334
+ return cached2;
11341
12335
  }
11342
12336
  async readDiskCache() {
11343
12337
  let text;
@@ -11399,6 +12393,7 @@ function npxPackageBasename(agent) {
11399
12393
  return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
11400
12394
  }
11401
12395
  async function planSpawn(agent, callerArgs = [], options = {}) {
12396
+ const version = agent.version ?? "current";
11402
12397
  if (agent.distribution.npx) {
11403
12398
  const npx = agent.distribution.npx;
11404
12399
  const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
@@ -11406,13 +12401,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
11406
12401
  return {
11407
12402
  command: "npx",
11408
12403
  args: ["-y", npx.package, ...tail],
11409
- env: npx.env ?? {}
12404
+ env: npx.env ?? {},
12405
+ version
11410
12406
  };
11411
12407
  }
11412
12408
  const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
11413
12409
  const binPath = await ensureNpmPackage({
11414
12410
  agentId: agent.id,
11415
- version: agent.version ?? "current",
12411
+ version,
11416
12412
  packageSpec: npx.package,
11417
12413
  bin,
11418
12414
  registry: options.npmRegistry
@@ -11420,7 +12416,8 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
11420
12416
  return {
11421
12417
  command: binPath,
11422
12418
  args: tail,
11423
- env: npx.env ?? {}
12419
+ env: npx.env ?? {},
12420
+ version
11424
12421
  };
11425
12422
  }
11426
12423
  if (agent.distribution.binary) {
@@ -11432,14 +12429,15 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
11432
12429
  }
11433
12430
  const cmdPath = await ensureBinary({
11434
12431
  agentId: agent.id,
11435
- version: agent.version ?? "current",
12432
+ version,
11436
12433
  target
11437
12434
  });
11438
12435
  const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
11439
12436
  return {
11440
12437
  command: cmdPath,
11441
12438
  args: tail,
11442
- env: target.env ?? {}
12439
+ env: target.env ?? {},
12440
+ version
11443
12441
  };
11444
12442
  }
11445
12443
  if (agent.distribution.uvx) {
@@ -11448,7 +12446,8 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
11448
12446
  return {
11449
12447
  command: "uvx",
11450
12448
  args: [uvx.package, ...tail],
11451
- env: uvx.env ?? {}
12449
+ env: uvx.env ?? {},
12450
+ version
11452
12451
  };
11453
12452
  }
11454
12453
  throw new Error(`Agent ${agent.id} has no usable distribution method.`);
@@ -11539,6 +12538,9 @@ init_connection();
11539
12538
  var DEFAULT_STDERR_TAIL_BYTES = 4096;
11540
12539
  var AgentInstance = class _AgentInstance {
11541
12540
  agentId;
12541
+ // Version this process was spawned from — used by the registry-fetch
12542
+ // prune sweep to skip install dirs belonging to a live agent.
12543
+ version;
11542
12544
  cwd;
11543
12545
  connection;
11544
12546
  child;
@@ -11550,6 +12552,7 @@ var AgentInstance = class _AgentInstance {
11550
12552
  exitHandlers = [];
11551
12553
  constructor(opts, child) {
11552
12554
  this.agentId = opts.agentId;
12555
+ this.version = opts.plan.version;
11553
12556
  this.cwd = opts.cwd;
11554
12557
  this.child = child;
11555
12558
  this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
@@ -12213,6 +13216,23 @@ var SessionManager = class {
12213
13216
  get(sessionId) {
12214
13217
  return this.sessions.get(sessionId);
12215
13218
  }
13219
+ // Snapshot of which agent versions are currently in use by live
13220
+ // sessions, keyed by agentId. Read by the registry-fetch prune sweep
13221
+ // so it can skip install dirs that still back a running process.
13222
+ activeAgentVersions() {
13223
+ const out = /* @__PURE__ */ new Map();
13224
+ for (const session of this.sessions.values()) {
13225
+ const id = session.agent.agentId;
13226
+ const version = session.agent.version;
13227
+ let set = out.get(id);
13228
+ if (!set) {
13229
+ set = /* @__PURE__ */ new Set();
13230
+ out.set(id, set);
13231
+ }
13232
+ set.add(version);
13233
+ }
13234
+ return out;
13235
+ }
12216
13236
  // Resolve a user-typed session id (which may have the hydra_session_
12217
13237
  // prefix stripped — that's what `sessions list` and the picker show) to
12218
13238
  // the canonical form that actually exists. Tries the input as-given
@@ -13189,12 +14209,91 @@ function withCode2(err, code) {
13189
14209
 
13190
14210
  // src/daemon/server.ts
13191
14211
  init_paths();
14212
+
14213
+ // src/core/agent-prune.ts
14214
+ init_paths();
14215
+ import * as fsp4 from "fs/promises";
14216
+ import * as path8 from "path";
14217
+ var logSink3 = (msg) => {
14218
+ process.stderr.write(msg + "\n");
14219
+ };
14220
+ function setAgentPruneLogger(log) {
14221
+ logSink3 = log ?? ((msg) => process.stderr.write(msg + "\n"));
14222
+ }
14223
+ async function pruneStaleAgentVersions(registry, sessionManager) {
14224
+ const platformKey = currentPlatformKey();
14225
+ if (!platformKey) {
14226
+ return;
14227
+ }
14228
+ const doc = await registry.load();
14229
+ const desiredByAgent = /* @__PURE__ */ new Map();
14230
+ for (const a of doc.agents) {
14231
+ desiredByAgent.set(a.id, a.version ?? "current");
14232
+ }
14233
+ const activeByAgent = sessionManager.activeAgentVersions();
14234
+ const platformDir = path8.join(paths.agentsDir(), platformKey);
14235
+ let agentEntries;
14236
+ try {
14237
+ agentEntries = await fsp4.readdir(platformDir, { withFileTypes: true });
14238
+ } catch (err) {
14239
+ const e = err;
14240
+ if (e.code === "ENOENT") {
14241
+ return;
14242
+ }
14243
+ logSink3(`hydra-acp: prune: failed to read ${platformDir}: ${e.message}`);
14244
+ return;
14245
+ }
14246
+ for (const agentEntry of agentEntries) {
14247
+ if (!agentEntry.isDirectory()) {
14248
+ continue;
14249
+ }
14250
+ const agentId = agentEntry.name;
14251
+ const desired = desiredByAgent.get(agentId);
14252
+ if (desired === void 0) {
14253
+ continue;
14254
+ }
14255
+ const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
14256
+ const agentDir = path8.join(platformDir, agentId);
14257
+ let versionEntries;
14258
+ try {
14259
+ versionEntries = await fsp4.readdir(agentDir, { withFileTypes: true });
14260
+ } catch (err) {
14261
+ logSink3(
14262
+ `hydra-acp: prune: failed to read ${agentDir}: ${err.message}`
14263
+ );
14264
+ continue;
14265
+ }
14266
+ for (const versionEntry of versionEntries) {
14267
+ if (!versionEntry.isDirectory()) {
14268
+ continue;
14269
+ }
14270
+ const version = versionEntry.name;
14271
+ if (version === desired) {
14272
+ continue;
14273
+ }
14274
+ if (activeVersions.has(version)) {
14275
+ continue;
14276
+ }
14277
+ const versionDir = path8.join(agentDir, version);
14278
+ try {
14279
+ await fsp4.rm(versionDir, { recursive: true, force: true });
14280
+ logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
14281
+ } catch (err) {
14282
+ logSink3(
14283
+ `hydra-acp: prune: failed to remove ${versionDir}: ${err.message}`
14284
+ );
14285
+ }
14286
+ }
14287
+ }
14288
+ }
14289
+
14290
+ // src/daemon/server.ts
13192
14291
  init_hydra_version();
13193
14292
 
13194
14293
  // src/core/session-tokens.ts
13195
14294
  init_paths();
13196
14295
  import * as fs13 from "fs/promises";
13197
- import * as path8 from "path";
14296
+ import * as path9 from "path";
13198
14297
  import { createHash, randomBytes, timingSafeEqual } from "crypto";
13199
14298
  var TOKEN_PREFIX = "hydra_session_";
13200
14299
  var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
@@ -13202,7 +14301,7 @@ var ID_LENGTH = 12;
13202
14301
  var TOKEN_BYTES = 32;
13203
14302
  var WRITE_DEBOUNCE_MS = 50;
13204
14303
  function tokensFilePath() {
13205
- return path8.join(paths.home(), "session-tokens.json");
14304
+ return path9.join(paths.home(), "session-tokens.json");
13206
14305
  }
13207
14306
  function sha256Hex(input) {
13208
14307
  return createHash("sha256").update(input).digest("hex");
@@ -13872,12 +14971,12 @@ import { z as z6 } from "zod";
13872
14971
  // src/core/password.ts
13873
14972
  init_paths();
13874
14973
  import * as fs14 from "fs/promises";
13875
- import * as path9 from "path";
14974
+ import * as path10 from "path";
13876
14975
  import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
13877
14976
  import { promisify } from "util";
13878
14977
  var scryptAsync = promisify(scrypt);
13879
14978
  function passwordHashPath() {
13880
- return path9.join(paths.home(), "password-hash");
14979
+ return path10.join(paths.home(), "password-hash");
13881
14980
  }
13882
14981
  var DEFAULT_N = 1 << 15;
13883
14982
  var DEFAULT_R = 8;
@@ -14292,6 +15391,22 @@ function registerAcpWsEndpoint(app, deps) {
14292
15391
  }
14293
15392
  return session.updateQueuedPrompt(params.messageId, params.prompt);
14294
15393
  });
15394
+ connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
15395
+ const params = AmendPromptParams.parse(raw);
15396
+ const att = state.attached.get(params.sessionId);
15397
+ if (!att) {
15398
+ const err = new Error("not attached to session");
15399
+ err.code = JsonRpcErrorCodes.SessionNotFound;
15400
+ throw err;
15401
+ }
15402
+ const session = deps.manager.get(params.sessionId);
15403
+ if (!session) {
15404
+ const err = new Error(`session ${params.sessionId} not found`);
15405
+ err.code = JsonRpcErrorCodes.SessionNotFound;
15406
+ throw err;
15407
+ }
15408
+ return session.amendPrompt(att.clientId, params);
15409
+ });
14295
15410
  connection.onRequest("session/load", async (raw) => {
14296
15411
  const rawObj = raw ?? {};
14297
15412
  const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
@@ -14444,10 +15559,17 @@ function buildInitializeResult() {
14444
15559
  ],
14445
15560
  // Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
14446
15561
  // ACP clients ignore the field; capability-aware clients learn here
14447
- // that hydra accepts concurrent session/prompt requests and emits
14448
- // prompt_queue_* notifications so they can stop running their own
14449
- // local queue.
14450
- _meta: mergeMeta(void 0, { promptQueueing: true })
15562
+ // which hydra-acp extensions the daemon supports so they can gate
15563
+ // UI surface accordingly. promptPipelining is false until the
15564
+ // streaming-input probe lands (Option A in the steering brief);
15565
+ // the others are unconditional method-availability flags.
15566
+ _meta: mergeMeta(void 0, {
15567
+ promptQueueing: true,
15568
+ promptCancelling: true,
15569
+ promptUpdating: true,
15570
+ promptAmending: true,
15571
+ promptPipelining: false
15572
+ })
14451
15573
  };
14452
15574
  }
14453
15575
  function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
@@ -14464,10 +15586,10 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
14464
15586
  async function startDaemon(config, serviceToken) {
14465
15587
  ensureLoopbackOrTls(config);
14466
15588
  const httpsOptions = config.daemon.tls ? {
14467
- key: await fsp4.readFile(config.daemon.tls.key),
14468
- cert: await fsp4.readFile(config.daemon.tls.cert)
15589
+ key: await fsp5.readFile(config.daemon.tls.key),
15590
+ cert: await fsp5.readFile(config.daemon.tls.cert)
14469
15591
  } : void 0;
14470
- await fsp4.mkdir(paths.home(), { recursive: true });
15592
+ await fsp5.mkdir(paths.home(), { recursive: true });
14471
15593
  const { stream: logStream, fileStream } = await buildLogStream(
14472
15594
  config.daemon.logLevel
14473
15595
  );
@@ -14511,7 +15633,12 @@ async function startDaemon(config, serviceToken) {
14511
15633
  5 * 60 * 1e3
14512
15634
  );
14513
15635
  sweepInterval.unref();
14514
- const registry = new Registry(config);
15636
+ const registry = new Registry(config, {
15637
+ onFetched: () => {
15638
+ void pruneStaleAgentVersions(registry, manager);
15639
+ }
15640
+ });
15641
+ setAgentPruneLogger((msg) => app.log.info(msg));
14515
15642
  const agentLogger = {
14516
15643
  info: (msg) => app.log.info(msg),
14517
15644
  warn: (msg) => app.log.warn(msg)
@@ -14552,8 +15679,8 @@ async function startDaemon(config, serviceToken) {
14552
15679
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
14553
15680
  const address = app.server.address();
14554
15681
  const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
14555
- await fsp4.mkdir(paths.home(), { recursive: true });
14556
- await fsp4.writeFile(
15682
+ await fsp5.mkdir(paths.home(), { recursive: true });
15683
+ await fsp5.writeFile(
14557
15684
  paths.pidFile(),
14558
15685
  JSON.stringify({
14559
15686
  pid: process.pid,
@@ -14587,6 +15714,7 @@ async function startDaemon(config, serviceToken) {
14587
15714
  await manager.flushMetaWrites();
14588
15715
  setBinaryInstallLogger(null);
14589
15716
  setNpmInstallLogger(null);
15717
+ setAgentPruneLogger(null);
14590
15718
  await app.close();
14591
15719
  try {
14592
15720
  fs15.unlinkSync(paths.pidFile());
@@ -14629,12 +15757,12 @@ init_daemon_bootstrap();
14629
15757
 
14630
15758
  // src/cli/commands/log-tail.ts
14631
15759
  import * as fs16 from "fs";
14632
- import * as fsp5 from "fs/promises";
15760
+ import * as fsp6 from "fs/promises";
14633
15761
  async function runLogTail(logPath, argv, notFoundMessage) {
14634
15762
  const opts = parseLogTailFlags(argv);
14635
15763
  let stat4;
14636
15764
  try {
14637
- stat4 = await fsp5.stat(logPath);
15765
+ stat4 = await fsp6.stat(logPath);
14638
15766
  } catch (err) {
14639
15767
  const e = err;
14640
15768
  if (e.code === "ENOENT") {
@@ -14660,14 +15788,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
14660
15788
  setImmediate(async () => {
14661
15789
  pending = false;
14662
15790
  try {
14663
- const s = await fsp5.stat(logPath);
15791
+ const s = await fsp6.stat(logPath);
14664
15792
  if (s.size <= position) {
14665
15793
  if (s.size < position) {
14666
15794
  position = s.size;
14667
15795
  }
14668
15796
  return;
14669
15797
  }
14670
- const fd = await fsp5.open(logPath, "r");
15798
+ const fd = await fsp6.open(logPath, "r");
14671
15799
  try {
14672
15800
  const buf = Buffer.alloc(s.size - position);
14673
15801
  await fd.read(buf, 0, buf.length, position);
@@ -14694,7 +15822,7 @@ async function printTail(logPath, fileSize, lines) {
14694
15822
  return fileSize;
14695
15823
  }
14696
15824
  const CHUNK = 64 * 1024;
14697
- const fd = await fsp5.open(logPath, "r");
15825
+ const fd = await fsp6.open(logPath, "r");
14698
15826
  try {
14699
15827
  let position = fileSize;
14700
15828
  let collected = "";
@@ -14866,7 +15994,7 @@ async function runDaemonStatus() {
14866
15994
  }
14867
15995
  async function readPidFile() {
14868
15996
  try {
14869
- const raw = await fsp6.readFile(paths.pidFile(), "utf8");
15997
+ const raw = await fsp7.readFile(paths.pidFile(), "utf8");
14870
15998
  return JSON.parse(raw);
14871
15999
  } catch (err) {
14872
16000
  const e = err;
@@ -14892,7 +16020,7 @@ init_sessions();
14892
16020
  init_config();
14893
16021
  init_service_token();
14894
16022
  init_paths();
14895
- import * as fsp7 from "fs/promises";
16023
+ import * as fsp8 from "fs/promises";
14896
16024
  init_sessions();
14897
16025
  async function runExtensionsList() {
14898
16026
  const config = await loadConfig();
@@ -15091,11 +16219,11 @@ async function runExtensionsRemove(name) {
15091
16219
  }
15092
16220
  }
15093
16221
  async function readRawConfig() {
15094
- const raw = await fsp7.readFile(paths.config(), "utf8");
16222
+ const raw = await fsp8.readFile(paths.config(), "utf8");
15095
16223
  return JSON.parse(raw);
15096
16224
  }
15097
16225
  async function writeRawConfig(raw) {
15098
- await fsp7.writeFile(
16226
+ await fsp8.writeFile(
15099
16227
  paths.config(),
15100
16228
  JSON.stringify(raw, null, 2) + "\n",
15101
16229
  { encoding: "utf8", mode: 384 }