@hydra-acp/cli 0.1.24 → 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") {
@@ -5947,6 +6255,12 @@ function mapKeyName(name) {
5947
6255
  case "ALT_ENTER":
5948
6256
  case "META_ENTER":
5949
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";
5950
6264
  case "ALT_B":
5951
6265
  case "META_B":
5952
6266
  return "alt-b";
@@ -6216,12 +6530,15 @@ var init_screen = __esm({
6216
6530
  this.term.fullscreen(false);
6217
6531
  this.term("\n");
6218
6532
  }
6219
- // Enables bracketed paste mode on the terminal and rewires stdin so we
6220
- // see the \x1b[200~/\x1b[201~ markers BEFORE terminal-kit's key
6221
- // parser. Non-paste data is forwarded to terminal-kit unchanged; paste
6222
- // 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.
6223
6537
  installBracketedPaste() {
6224
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");
6225
6542
  const t = this.term;
6226
6543
  if (!t.stdin || typeof t.onStdin !== "function") {
6227
6544
  return;
@@ -6232,6 +6549,9 @@ var init_screen = __esm({
6232
6549
  }
6233
6550
  uninstallBracketedPaste() {
6234
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");
6235
6555
  const t = this.term;
6236
6556
  if (!t.stdin || this.terminalKitStdinHandler === null) {
6237
6557
  return;
@@ -6244,6 +6564,38 @@ var init_screen = __esm({
6244
6564
  }
6245
6565
  handleRawStdin(chunk) {
6246
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) {
6247
6599
  const startMarker = "\x1B[200~";
6248
6600
  const endMarker = "\x1B[201~";
6249
6601
  while (text.length > 0) {
@@ -7944,6 +8296,9 @@ var init_input = __esm({
7944
8296
  switch (name) {
7945
8297
  case "enter":
7946
8298
  return this.send();
8299
+ case "shift-enter":
8300
+ case "ctrl-enter":
8301
+ return this.amend();
7947
8302
  case "alt-enter":
7948
8303
  this.insertNewline();
7949
8304
  return [];
@@ -8130,22 +8485,64 @@ var init_input = __esm({
8130
8485
  this.setCurrentLine(line + next);
8131
8486
  }
8132
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.
8133
8497
  killLine() {
8134
- const line = this.currentLine();
8135
- const killed = line.slice(0, this.col);
8136
- if (killed.length > 0) {
8137
- 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;
8138
8504
  }
8139
- this.setCurrentLine(line.slice(this.col));
8140
- 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;
8141
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.
8142
8528
  killToEnd() {
8143
8529
  const line = this.currentLine();
8144
- const killed = line.slice(this.col);
8145
- if (killed.length > 0) {
8146
- 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;
8147
8534
  }
8148
- this.setCurrentLine(line.slice(0, this.col));
8535
+ if (this.row >= this.buffer.length - 1) {
8536
+ return;
8537
+ }
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);
8149
8546
  }
8150
8547
  killWord() {
8151
8548
  const line = this.currentLine();
@@ -8494,6 +8891,31 @@ var init_input = __esm({
8494
8891
  this.clearBuffer();
8495
8892
  return [{ type: "send", text, planMode, attachments }];
8496
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
+ }
8497
8919
  // Home: jump to the very start of the prompt buffer. If we're already
8498
8920
  // there, fall through to scrolling the scrollback to its top.
8499
8921
  handleHome() {
@@ -8605,26 +9027,34 @@ async function readLinux(env) {
8605
9027
  reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
8606
9028
  };
8607
9029
  }
8608
- try {
8609
- const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
8610
- if (buf.length > 0) {
8611
- 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
+ }
8612
9046
  return {
8613
- ok: false,
8614
- 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
+ }
8615
9054
  };
8616
9055
  }
8617
- return {
8618
- ok: true,
8619
- kind: "image",
8620
- attachment: {
8621
- mimeType: "image/png",
8622
- data: buf.toString("base64"),
8623
- sizeBytes: buf.length
8624
- }
8625
- };
9056
+ } catch {
8626
9057
  }
8627
- } catch {
8628
9058
  }
8629
9059
  try {
8630
9060
  const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
@@ -8644,7 +9074,8 @@ async function detectLinuxTool(env) {
8644
9074
  if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
8645
9075
  return {
8646
9076
  cmd: "wl-paste",
8647
- imageArgs: ["-t", "image/png"],
9077
+ listTargetsArgs: ["--list-types"],
9078
+ imageArgs: (mime) => ["-t", mime],
8648
9079
  // -n: drop trailing newline wl-paste adds by default. We further
8649
9080
  // normalize line endings below, but this avoids a spurious
8650
9081
  // empty trailing row from a single-line clipboard text.
@@ -8654,12 +9085,30 @@ async function detectLinuxTool(env) {
8654
9085
  if (env.env.DISPLAY && await which(env, "xclip")) {
8655
9086
  return {
8656
9087
  cmd: "xclip",
8657
- imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
9088
+ listTargetsArgs: ["-selection", "clipboard", "-t", "TARGETS", "-o"],
9089
+ imageArgs: (mime) => ["-selection", "clipboard", "-t", mime, "-o"],
8658
9090
  textArgs: ["-selection", "clipboard", "-o"]
8659
9091
  };
8660
9092
  }
8661
9093
  return null;
8662
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
+ }
8663
9112
  function normalizeText(text) {
8664
9113
  return text.replace(/\r\n?/g, "\n");
8665
9114
  }
@@ -8754,7 +9203,7 @@ function runCapture(spawn6, cmd, args) {
8754
9203
  });
8755
9204
  });
8756
9205
  }
8757
- var defaultEnv;
9206
+ var defaultEnv, SUPPORTED_IMAGE_MIMES;
8758
9207
  var init_clipboard = __esm({
8759
9208
  "src/tui/clipboard.ts"() {
8760
9209
  "use strict";
@@ -8765,6 +9214,12 @@ var init_clipboard = __esm({
8765
9214
  spawn: nodeSpawn,
8766
9215
  tmpdir: os4.tmpdir
8767
9216
  };
9217
+ SUPPORTED_IMAGE_MIMES = [
9218
+ "image/png",
9219
+ "image/jpeg",
9220
+ "image/gif",
9221
+ "image/webp"
9222
+ ];
8768
9223
  }
8769
9224
  });
8770
9225
 
@@ -8905,7 +9360,8 @@ function parseAgentMarkdown(text) {
8905
9360
  codeBuffer = [];
8906
9361
  codeLang = "";
8907
9362
  };
8908
- for (const line of lines) {
9363
+ for (let i = 0; i < lines.length; i++) {
9364
+ const line = lines[i];
8909
9365
  const fence = line.match(/^\s*```\s*(\w*)\s*$/);
8910
9366
  if (fence) {
8911
9367
  if (!inCode) {
@@ -8933,6 +9389,19 @@ function parseAgentMarkdown(text) {
8933
9389
  });
8934
9390
  continue;
8935
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
+ }
8936
9405
  const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
8937
9406
  if (bullet) {
8938
9407
  const indent = bullet[1] ?? "";
@@ -8967,6 +9436,70 @@ function parseAgentMarkdown(text) {
8967
9436
  }
8968
9437
  return out;
8969
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
+ }
8970
9503
  function highlightFencedBlock(lang, lines) {
8971
9504
  if (lang.length === 0 || !supportsLanguage(lang)) {
8972
9505
  return lines.map((body) => ({ body, ansi: false }));
@@ -9235,6 +9768,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9235
9768
  }
9236
9769
  };
9237
9770
  let pendingTurns = 0;
9771
+ let currentHeadMessageId;
9238
9772
  let sessionBusySince = null;
9239
9773
  let sessionElapsedTimer = null;
9240
9774
  const adjustPendingTurns = (delta) => {
@@ -9326,13 +9860,29 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9326
9860
  screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
9327
9861
  }
9328
9862
  });
9863
+ const amendPendingPaintTimers = /* @__PURE__ */ new Map();
9864
+ const AMEND_CHIP_DISPLAY_DELAY_MS = 200;
9329
9865
  conn.onNotification("hydra-acp/prompt_queue_added", (params) => {
9330
9866
  if (teardownStarted) return;
9331
9867
  const p = params ?? {};
9332
9868
  if (typeof p.messageId !== "string") return;
9333
- queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
9334
- if (screenRef && dispatcherRef) {
9335
- 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
+ }
9336
9886
  }
9337
9887
  if (ownClientId !== void 0 && p.originator?.clientId === ownClientId) {
9338
9888
  const echo = pendingEchoes.shift();
@@ -9377,6 +9927,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9377
9927
  if (teardownStarted) return;
9378
9928
  const p = params ?? {};
9379
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
+ }
9380
9938
  const hadChip = queueCache.delete(p.messageId);
9381
9939
  if (hadChip && screenRef && dispatcherRef) {
9382
9940
  refreshQueueDisplay();
@@ -9395,6 +9953,22 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9395
9953
  }
9396
9954
  }
9397
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
+ });
9398
9972
  const handlePermissionResolved = (update) => {
9399
9973
  const u = update ?? {};
9400
9974
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
@@ -9502,6 +10076,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9502
10076
  let upstreamSessionId;
9503
10077
  let agentInfoName;
9504
10078
  let agentAcceptsImages = true;
10079
+ let daemonSupportsAmend = false;
9505
10080
  try {
9506
10081
  const initResult = await conn.request("initialize", {
9507
10082
  protocolVersion: ACP_PROTOCOL_VERSION,
@@ -9516,6 +10091,8 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9516
10091
  if (imageCap === false) {
9517
10092
  agentAcceptsImages = false;
9518
10093
  }
10094
+ const hydraMeta = extractHydraMeta(initResult?._meta ?? void 0);
10095
+ daemonSupportsAmend = hydraMeta.promptAmending === true;
9519
10096
  } catch {
9520
10097
  }
9521
10098
  let resolvedSessionId = ctx.sessionId;
@@ -9916,6 +10493,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9916
10493
  }
9917
10494
  return true;
9918
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
+ };
9919
10508
  const toggleHelpModal = () => {
9920
10509
  if (screen.isHelpPromptActive()) {
9921
10510
  screen.setHelpPrompt(null);
@@ -9923,7 +10512,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9923
10512
  }
9924
10513
  screen.setHelpPrompt({
9925
10514
  title: "Hotkeys",
9926
- entries: HELP_ENTRIES2,
10515
+ entries: buildHelpEntries(),
9927
10516
  hint: "any key dismisses \xB7 /help lists commands"
9928
10517
  });
9929
10518
  };
@@ -10026,7 +10615,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10026
10615
  const handleEffect = (effect) => {
10027
10616
  switch (effect.type) {
10028
10617
  case "send":
10029
- 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
+ }
10030
10630
  return;
10031
10631
  case "queue-edit": {
10032
10632
  const mid = queueMessageIdAt(effect.index);
@@ -10234,6 +10834,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10234
10834
  const queueCache = /* @__PURE__ */ new Map();
10235
10835
  const pendingEchoes = [];
10236
10836
  const ownPendingByMid = /* @__PURE__ */ new Map();
10837
+ const amendedMessageIds = /* @__PURE__ */ new Set();
10237
10838
  let currentTurnEcho = null;
10238
10839
  const refreshQueueDisplay = () => {
10239
10840
  const entries = [...queueCache.values()];
@@ -10268,6 +10869,75 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10268
10869
  saveHistory(historyFile, history).catch(() => void 0);
10269
10870
  void runPrompt(text, attachments);
10270
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
+ };
10271
10941
  const handleModeToggle = async (_on) => {
10272
10942
  if (agentModes.length === 0) {
10273
10943
  screen.notify("no modes advertised by agent");
@@ -10492,9 +11162,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10492
11162
  turnInFlight = null;
10493
11163
  adjustPendingTurns(-1);
10494
11164
  if (echo.flushed && currentTurnEcho === echo) {
10495
- appendRender(
10496
- stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
10497
- );
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);
10498
11177
  currentTurnEcho = null;
10499
11178
  }
10500
11179
  if (pendingPrefill !== null) {
@@ -10737,8 +11416,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10737
11416
  screen.appendLines(formatted);
10738
11417
  }
10739
11418
  if (event.kind === "turn-complete") {
11419
+ currentHeadMessageId = void 0;
10740
11420
  closeAgentText();
10741
- 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") {
10742
11423
  const lines = formatEvent({ ...lastPlanEvent, stopped: true });
10743
11424
  if (lines.length > 0) {
10744
11425
  screen.upsertLines("plan", lines);
@@ -10748,15 +11429,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10748
11429
  screen.clearKey("plan");
10749
11430
  if (toolsBlockStartedAt !== null) {
10750
11431
  toolsBlockEndedAt = Date.now();
10751
- toolsBlockStopReason = event.stopReason ?? null;
11432
+ toolsBlockStopReason = effectiveStopReason ?? null;
10752
11433
  renderToolsBlock();
10753
11434
  screen.clearKey("tools");
10754
- } else if (event.stopReason !== void 0 && event.stopReason !== "end_turn") {
11435
+ } else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
10755
11436
  screen.appendLines([
10756
11437
  {
10757
11438
  prefix: "\u26A0 ",
10758
11439
  prefixStyle: "tool-status-fail",
10759
- body: `turn ended: ${event.stopReason}`,
11440
+ body: `turn ended: ${effectiveStopReason}`,
10760
11441
  bodyStyle: "tool-status-fail"
10761
11442
  }
10762
11443
  ]);
@@ -11014,7 +11695,7 @@ function rotateIfBig(target) {
11014
11695
  } catch {
11015
11696
  }
11016
11697
  }
11017
- var HELP_ENTRIES2, logMaxBytes;
11698
+ var HELP_ENTRIES_TAIL, logMaxBytes;
11018
11699
  var init_app = __esm({
11019
11700
  "src/tui/app.ts"() {
11020
11701
  "use strict";
@@ -11038,8 +11719,7 @@ var init_app = __esm({
11038
11719
  init_completion();
11039
11720
  init_render_update();
11040
11721
  init_format();
11041
- HELP_ENTRIES2 = [
11042
- ["Enter", "send prompt (or queue while a turn is running)"],
11722
+ HELP_ENTRIES_TAIL = [
11043
11723
  ["Alt+Enter", "newline in prompt"],
11044
11724
  ["Shift+Tab", "cycle agent modes (plan / accept-edits / etc.)"],
11045
11725
  ["Tab", "indent \xB7 slash-command completion"],
@@ -14711,6 +15391,22 @@ function registerAcpWsEndpoint(app, deps) {
14711
15391
  }
14712
15392
  return session.updateQueuedPrompt(params.messageId, params.prompt);
14713
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
+ });
14714
15410
  connection.onRequest("session/load", async (raw) => {
14715
15411
  const rawObj = raw ?? {};
14716
15412
  const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
@@ -14863,10 +15559,17 @@ function buildInitializeResult() {
14863
15559
  ],
14864
15560
  // Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
14865
15561
  // ACP clients ignore the field; capability-aware clients learn here
14866
- // that hydra accepts concurrent session/prompt requests and emits
14867
- // prompt_queue_* notifications so they can stop running their own
14868
- // local queue.
14869
- _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
+ })
14870
15573
  };
14871
15574
  }
14872
15575
  function bindClientToSession(connection, session, state, clientInfo, callerClientId) {