@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 +1256 -128
- package/dist/index.d.ts +56 -1
- package/dist/index.js +478 -32
- package/package.json +1 -1
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 = `${
|
|
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(
|
|
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 =
|
|
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
|
|
5680
|
+
import path12 from "path";
|
|
5320
5681
|
function mimeFromExtension(p) {
|
|
5321
|
-
return EXTENSION_TO_MIME[
|
|
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 \
|
|
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
|
|
6157
|
-
// see the \x1b[200~/\x1b[201~ markers
|
|
6158
|
-
//
|
|
6159
|
-
//
|
|
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
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
this.
|
|
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
|
-
|
|
7994
|
-
|
|
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
|
-
|
|
7999
|
-
|
|
8000
|
-
this.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
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:
|
|
8430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
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
|
-
|
|
9127
|
-
if (
|
|
9128
|
-
|
|
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
|
-
|
|
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: ${
|
|
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:
|
|
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 ${
|
|
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
|
-
|
|
10259
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
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: ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
14448
|
-
//
|
|
14449
|
-
//
|
|
14450
|
-
|
|
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
|
|
14468
|
-
cert: await
|
|
15589
|
+
key: await fsp5.readFile(config.daemon.tls.key),
|
|
15590
|
+
cert: await fsp5.readFile(config.daemon.tls.cert)
|
|
14469
15591
|
} : void 0;
|
|
14470
|
-
await
|
|
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
|
|
14556
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16226
|
+
await fsp8.writeFile(
|
|
15099
16227
|
paths.config(),
|
|
15100
16228
|
JSON.stringify(raw, null, 2) + "\n",
|
|
15101
16229
|
{ encoding: "utf8", mode: 384 }
|