@hydra-acp/cli 0.1.7 → 0.1.9
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/README.md +4 -4
- package/dist/cli.js +2089 -539
- package/dist/index.d.ts +82 -9
- package/dist/index.js +587 -105
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,19 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/core/paths.ts
|
|
13
13
|
import * as path from "path";
|
|
14
14
|
import * as os from "os";
|
|
15
|
+
function shortenHomePath(p) {
|
|
16
|
+
const home = os.homedir();
|
|
17
|
+
if (!home) {
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
if (p === home) {
|
|
21
|
+
return "~";
|
|
22
|
+
}
|
|
23
|
+
if (p.startsWith(home + "/")) {
|
|
24
|
+
return "~" + p.slice(home.length);
|
|
25
|
+
}
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
15
28
|
function hydraHome() {
|
|
16
29
|
const override = process.env[ROOT_ENV];
|
|
17
30
|
if (override && override.length > 0) {
|
|
@@ -45,6 +58,18 @@ var init_paths = __esm({
|
|
|
45
58
|
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
46
59
|
// shows which platforms have ever installed anything.
|
|
47
60
|
agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
|
|
61
|
+
// npm install cache for npx-distributed agents. The trailing
|
|
62
|
+
// node<ABI> segment keys on process.versions.modules so a Node
|
|
63
|
+
// major bump (different ABI → native modules incompatible) yields
|
|
64
|
+
// a fresh install rather than failing at require() time.
|
|
65
|
+
agentNpmInstallDir: (id, platformKey, version) => path.join(
|
|
66
|
+
hydraHome(),
|
|
67
|
+
"agents",
|
|
68
|
+
platformKey,
|
|
69
|
+
id,
|
|
70
|
+
version,
|
|
71
|
+
`node${process.versions.modules}`
|
|
72
|
+
),
|
|
48
73
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
49
74
|
// One directory per session id under sessions/. Co-locates the
|
|
50
75
|
// session record, its transcript, and any future per-session state
|
|
@@ -149,6 +174,9 @@ async function loadConfig() {
|
|
|
149
174
|
daemon.authToken = token;
|
|
150
175
|
return HydraConfig.parse(raw);
|
|
151
176
|
}
|
|
177
|
+
async function loadConfigReadOnly() {
|
|
178
|
+
return HydraConfigReadOnly.parse(await readConfigFile());
|
|
179
|
+
}
|
|
152
180
|
async function ensureConfig() {
|
|
153
181
|
if (!await loadAuthToken()) {
|
|
154
182
|
const token = generateAuthToken();
|
|
@@ -181,7 +209,7 @@ function expandHome(p) {
|
|
|
181
209
|
}
|
|
182
210
|
return p;
|
|
183
211
|
}
|
|
184
|
-
var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig;
|
|
212
|
+
var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig, HydraConfigReadOnly;
|
|
185
213
|
var init_config = __esm({
|
|
186
214
|
"src/core/config.ts"() {
|
|
187
215
|
"use strict";
|
|
@@ -197,7 +225,16 @@ var init_config = __esm({
|
|
|
197
225
|
authToken: z.string().min(16),
|
|
198
226
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
199
227
|
tls: TlsConfig.optional(),
|
|
200
|
-
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
|
|
228
|
+
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
|
|
229
|
+
// Cap on entries kept in a session's on-disk replay log (history.jsonl).
|
|
230
|
+
// Compaction trims to this many on a periodic basis; reads also slice
|
|
231
|
+
// to the tail at this length as a defensive measure against older
|
|
232
|
+
// daemons that may have written unbounded files.
|
|
233
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e3),
|
|
234
|
+
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
235
|
+
// daemon can include it in the diagnostic message when a spawn fails.
|
|
236
|
+
// Bump if your agents emit large tracebacks you want surfaced.
|
|
237
|
+
agentStderrTailBytes: z.number().int().positive().default(4096)
|
|
201
238
|
});
|
|
202
239
|
RegistryConfig = z.object({
|
|
203
240
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -220,7 +257,14 @@ var init_config = __esm({
|
|
|
220
257
|
// text selection requires shift+drag to bypass mouse reporting. Set
|
|
221
258
|
// false to disable capture — wheel scrollback stops working, but
|
|
222
259
|
// plain click-drag selects text via the terminal emulator.
|
|
223
|
-
mouse: z.boolean().default(true)
|
|
260
|
+
mouse: z.boolean().default(true),
|
|
261
|
+
// Size at which the TUI's session/update debug log (tui.log) rotates
|
|
262
|
+
// to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
|
|
263
|
+
logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
|
|
264
|
+
// Width cap on the cwd column in the `sessions list` output and the
|
|
265
|
+
// TUI picker. Set higher if you keep deeply-nested working directories
|
|
266
|
+
// and want them visible; the elastic title column shrinks to make room.
|
|
267
|
+
cwdColumnMaxWidth: z.number().int().positive().default(24)
|
|
224
268
|
});
|
|
225
269
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
226
270
|
ExtensionBody = z.object({
|
|
@@ -256,9 +300,14 @@ var init_config = __esm({
|
|
|
256
300
|
tui: TuiConfig.default({
|
|
257
301
|
repaintThrottleMs: 1e3,
|
|
258
302
|
maxScrollbackLines: 1e4,
|
|
259
|
-
mouse: true
|
|
303
|
+
mouse: true,
|
|
304
|
+
logMaxBytes: 5 * 1024 * 1024,
|
|
305
|
+
cwdColumnMaxWidth: 24
|
|
260
306
|
})
|
|
261
307
|
});
|
|
308
|
+
HydraConfigReadOnly = HydraConfig.extend({
|
|
309
|
+
daemon: DaemonConfig.omit({ authToken: true })
|
|
310
|
+
});
|
|
262
311
|
}
|
|
263
312
|
});
|
|
264
313
|
|
|
@@ -332,21 +381,33 @@ function extractHydraMeta(meta) {
|
|
|
332
381
|
function mergeMeta(passthrough, ours) {
|
|
333
382
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
334
383
|
}
|
|
335
|
-
var JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
384
|
+
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
336
385
|
var init_types = __esm({
|
|
337
386
|
"src/acp/types.ts"() {
|
|
338
387
|
"use strict";
|
|
388
|
+
ACP_PROTOCOL_VERSION = 1;
|
|
339
389
|
JsonRpcErrorCodes = {
|
|
340
390
|
ParseError: -32700,
|
|
341
391
|
InvalidRequest: -32600,
|
|
342
392
|
MethodNotFound: -32601,
|
|
343
393
|
InvalidParams: -32602,
|
|
344
394
|
InternalError: -32603,
|
|
395
|
+
// -32001…-32003 reserved for RFD #533 attach semantics:
|
|
396
|
+
// -32001 Session not found
|
|
397
|
+
// -32002 Not authorised to attach
|
|
398
|
+
// -32003 Session does not support multi-client attach
|
|
399
|
+
// We emit -32001 (matching); the other two are reserved for spec
|
|
400
|
+
// alignment even though we don't currently emit them (we bearer-auth
|
|
401
|
+
// at WS upgrade time and always support multi-client attach).
|
|
345
402
|
SessionNotFound: -32001,
|
|
346
|
-
|
|
347
|
-
|
|
403
|
+
NotAuthorisedToAttach: -32002,
|
|
404
|
+
MultiClientNotSupported: -32003,
|
|
348
405
|
AgentNotInstalled: -32005,
|
|
349
|
-
|
|
406
|
+
// Hydra-internal codes — outside the RFD's reserved range so they
|
|
407
|
+
// can't collide with future spec assignments.
|
|
408
|
+
BundleAlreadyImported: -32010,
|
|
409
|
+
PermissionDenied: -32011,
|
|
410
|
+
AlreadyAttached: -32012
|
|
350
411
|
};
|
|
351
412
|
InitializeParams = z3.object({
|
|
352
413
|
protocolVersion: z3.number().optional(),
|
|
@@ -356,7 +417,12 @@ var init_types = __esm({
|
|
|
356
417
|
version: z3.string().optional()
|
|
357
418
|
}).optional()
|
|
358
419
|
});
|
|
359
|
-
HistoryPolicy = z3.enum([
|
|
420
|
+
HistoryPolicy = z3.enum([
|
|
421
|
+
"full",
|
|
422
|
+
"pending_only",
|
|
423
|
+
"none",
|
|
424
|
+
"after_message"
|
|
425
|
+
]);
|
|
360
426
|
SessionNewParams = z3.object({
|
|
361
427
|
cwd: z3.string(),
|
|
362
428
|
agentId: z3.string().optional(),
|
|
@@ -372,6 +438,18 @@ var init_types = __esm({
|
|
|
372
438
|
SessionAttachParams = z3.object({
|
|
373
439
|
sessionId: z3.string(),
|
|
374
440
|
historyPolicy: HistoryPolicy.default("full"),
|
|
441
|
+
// Required when historyPolicy is "after_message"; ignored otherwise.
|
|
442
|
+
// The proxy replays history entries strictly after the entry whose
|
|
443
|
+
// messageId matches this value. If the id isn't found in the buffer,
|
|
444
|
+
// the response.historyPolicy field surfaces "full" so the caller
|
|
445
|
+
// knows we fell back. Per RFD #533.
|
|
446
|
+
afterMessageId: z3.string().optional(),
|
|
447
|
+
// Caller-assigned opaque id (e.g. a UUID). When provided, the proxy
|
|
448
|
+
// echoes it in resolvedBy/sentBy and lifecycle events so other
|
|
449
|
+
// clients can disambiguate multiple instances of the same
|
|
450
|
+
// clientInfo.name. When omitted, the proxy assigns one and returns
|
|
451
|
+
// it in the response. Per RFD #533.
|
|
452
|
+
clientId: z3.string().optional(),
|
|
375
453
|
clientInfo: z3.object({
|
|
376
454
|
name: z3.string(),
|
|
377
455
|
version: z3.string().optional()
|
|
@@ -528,6 +606,13 @@ var init_connection = __esm({
|
|
|
528
606
|
}
|
|
529
607
|
await this.stream.close();
|
|
530
608
|
}
|
|
609
|
+
// Force-close with an error. Rejects all pending requests and fires
|
|
610
|
+
// close handlers carrying `err`. Used by transports that detect a
|
|
611
|
+
// failure (e.g. child process crash, spawn ENOENT) the stream itself
|
|
612
|
+
// can't surface as a stdout/stdin error.
|
|
613
|
+
fail(err) {
|
|
614
|
+
this.handleClose(err);
|
|
615
|
+
}
|
|
531
616
|
handleIncoming(message) {
|
|
532
617
|
if ("method" in message) {
|
|
533
618
|
if ("id" in message && message.id !== void 0) {
|
|
@@ -651,6 +736,9 @@ var init_hydra_commands = __esm({
|
|
|
651
736
|
|
|
652
737
|
// src/core/session.ts
|
|
653
738
|
import { customAlphabet } from "nanoid";
|
|
739
|
+
function generateMessageId() {
|
|
740
|
+
return `m_${generateHydraId()}`;
|
|
741
|
+
}
|
|
654
742
|
function stripHydraSessionPrefix(id) {
|
|
655
743
|
return id.startsWith(HYDRA_SESSION_PREFIX) ? id.slice(HYDRA_SESSION_PREFIX.length) : id;
|
|
656
744
|
}
|
|
@@ -715,6 +803,97 @@ function extractAdvertisedCommands(params) {
|
|
|
715
803
|
}
|
|
716
804
|
return out;
|
|
717
805
|
}
|
|
806
|
+
function ensureMessageIdOnUpdate(method, params) {
|
|
807
|
+
if (method !== "session/update" || !params || typeof params !== "object") {
|
|
808
|
+
return params;
|
|
809
|
+
}
|
|
810
|
+
const p = params;
|
|
811
|
+
if (!p.update || typeof p.update !== "object" || Array.isArray(p.update)) {
|
|
812
|
+
return params;
|
|
813
|
+
}
|
|
814
|
+
const u = p.update;
|
|
815
|
+
if (typeof u.messageId === "string") {
|
|
816
|
+
return params;
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
...params,
|
|
820
|
+
update: { ...p.update, messageId: generateMessageId() }
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function findMessageIdIndex(history, target) {
|
|
824
|
+
for (let i = 0; i < history.length; i++) {
|
|
825
|
+
const entry = history[i];
|
|
826
|
+
if (!entry || entry.method !== "session/update") {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const params = entry.params;
|
|
830
|
+
if (params?.update?.messageId === target) {
|
|
831
|
+
return i;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return -1;
|
|
835
|
+
}
|
|
836
|
+
function extractToolCallId(params) {
|
|
837
|
+
if (!params || typeof params !== "object") {
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
const toolCall = params.toolCall;
|
|
841
|
+
if (!toolCall || typeof toolCall !== "object") {
|
|
842
|
+
return void 0;
|
|
843
|
+
}
|
|
844
|
+
const id = toolCall.toolCallId;
|
|
845
|
+
return typeof id === "string" ? id : void 0;
|
|
846
|
+
}
|
|
847
|
+
function buildPermissionResolvedUpdate(args) {
|
|
848
|
+
const outcome = extractOutcome(args.result);
|
|
849
|
+
const update = {
|
|
850
|
+
sessionUpdate: "permission_resolved"
|
|
851
|
+
};
|
|
852
|
+
if (args.toolCallId !== void 0) {
|
|
853
|
+
update.toolCallId = args.toolCallId;
|
|
854
|
+
}
|
|
855
|
+
if (outcome) {
|
|
856
|
+
update.outcome = outcome;
|
|
857
|
+
if (outcome.kind === "selected" && typeof outcome.optionId === "string") {
|
|
858
|
+
update.chosenOptionId = outcome.optionId;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
update.resolvedBy = buildResolvedBy(args.resolver);
|
|
862
|
+
return update;
|
|
863
|
+
}
|
|
864
|
+
function extractOutcome(result) {
|
|
865
|
+
if (!result || typeof result !== "object") {
|
|
866
|
+
return void 0;
|
|
867
|
+
}
|
|
868
|
+
const raw = result.outcome;
|
|
869
|
+
if (!raw || typeof raw !== "object") {
|
|
870
|
+
return void 0;
|
|
871
|
+
}
|
|
872
|
+
const kind = raw.kind;
|
|
873
|
+
if (typeof kind !== "string") {
|
|
874
|
+
return void 0;
|
|
875
|
+
}
|
|
876
|
+
const out = { kind };
|
|
877
|
+
const optionId = raw.optionId;
|
|
878
|
+
if (typeof optionId === "string") {
|
|
879
|
+
out.optionId = optionId;
|
|
880
|
+
}
|
|
881
|
+
const reason = raw.reason;
|
|
882
|
+
if (typeof reason === "string") {
|
|
883
|
+
out.reason = reason;
|
|
884
|
+
}
|
|
885
|
+
return out;
|
|
886
|
+
}
|
|
887
|
+
function buildResolvedBy(client) {
|
|
888
|
+
const out = { clientId: client.clientId };
|
|
889
|
+
if (client.clientInfo?.name) {
|
|
890
|
+
out.name = client.clientInfo.name;
|
|
891
|
+
}
|
|
892
|
+
if (client.clientInfo?.version) {
|
|
893
|
+
out.version = client.clientInfo.version;
|
|
894
|
+
}
|
|
895
|
+
return out;
|
|
896
|
+
}
|
|
718
897
|
function extractPromptText(prompt) {
|
|
719
898
|
if (typeof prompt === "string") {
|
|
720
899
|
return prompt;
|
|
@@ -739,7 +918,7 @@ function firstLine(text, max) {
|
|
|
739
918
|
}
|
|
740
919
|
return void 0;
|
|
741
920
|
}
|
|
742
|
-
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX,
|
|
921
|
+
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, DEFAULT_HISTORY_MAX_ENTRIES, Session, STATE_UPDATE_KINDS;
|
|
743
922
|
var init_session = __esm({
|
|
744
923
|
"src/core/session.ts"() {
|
|
745
924
|
"use strict";
|
|
@@ -748,8 +927,7 @@ var init_session = __esm({
|
|
|
748
927
|
HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
749
928
|
generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
750
929
|
HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
751
|
-
|
|
752
|
-
COMPACT_EVERY = 200;
|
|
930
|
+
DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
753
931
|
Session = class {
|
|
754
932
|
sessionId;
|
|
755
933
|
cwd;
|
|
@@ -791,11 +969,13 @@ var init_session = __esm({
|
|
|
791
969
|
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
792
970
|
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
793
971
|
promptStartedAt;
|
|
794
|
-
// Counts appends since the last compaction. When it hits
|
|
972
|
+
// Counts appends since the last compaction. When it hits compactEvery
|
|
795
973
|
// we ask the history store to trim the file to the most recent
|
|
796
|
-
//
|
|
974
|
+
// historyMaxEntries. Keeps file growth bounded without per-append
|
|
797
975
|
// file-size checks.
|
|
798
976
|
appendCount = 0;
|
|
977
|
+
historyMaxEntries;
|
|
978
|
+
compactEvery;
|
|
799
979
|
// Permission requests that have been broadcast to one or more
|
|
800
980
|
// clients but have not yet resolved. Replayed to clients that
|
|
801
981
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -852,6 +1032,8 @@ var init_session = __esm({
|
|
|
852
1032
|
this.firstPromptSeeded = true;
|
|
853
1033
|
}
|
|
854
1034
|
this.historyStore = init.historyStore;
|
|
1035
|
+
this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
|
|
1036
|
+
this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
|
|
855
1037
|
this.updatedAt = Date.now();
|
|
856
1038
|
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
857
1039
|
this.lastRecordedAt = this.updatedAt;
|
|
@@ -919,6 +1101,30 @@ var init_session = __esm({
|
|
|
919
1101
|
get attachedCount() {
|
|
920
1102
|
return this.clients.size;
|
|
921
1103
|
}
|
|
1104
|
+
// Roster of currently-attached clients, optionally excluding one
|
|
1105
|
+
// clientId. Used by the daemon to populate connectedClients on the
|
|
1106
|
+
// session/attach response (per RFD #533) — the freshly-attaching
|
|
1107
|
+
// client wants to see who else is on the session but not itself in
|
|
1108
|
+
// the list.
|
|
1109
|
+
connectedClients(excludeClientId) {
|
|
1110
|
+
const out = [];
|
|
1111
|
+
for (const client of this.clients.values()) {
|
|
1112
|
+
if (excludeClientId && client.clientId === excludeClientId) {
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const entry = {
|
|
1116
|
+
clientId: client.clientId
|
|
1117
|
+
};
|
|
1118
|
+
if (client.clientInfo?.name) {
|
|
1119
|
+
entry.name = client.clientInfo.name;
|
|
1120
|
+
}
|
|
1121
|
+
if (client.clientInfo?.version) {
|
|
1122
|
+
entry.version = client.clientInfo.version;
|
|
1123
|
+
}
|
|
1124
|
+
out.push(entry);
|
|
1125
|
+
}
|
|
1126
|
+
return out;
|
|
1127
|
+
}
|
|
922
1128
|
// Wall-clock when the in-flight agent turn began, or undefined when
|
|
923
1129
|
// idle. Tracked in-memory by broadcastPromptReceived/broadcastTurnComplete
|
|
924
1130
|
// so the daemon can hand a fresh attacher mid-turn the right elapsed
|
|
@@ -950,10 +1156,12 @@ var init_session = __esm({
|
|
|
950
1156
|
};
|
|
951
1157
|
}
|
|
952
1158
|
// Register a client and (asynchronously) load the replay slice it
|
|
953
|
-
// should receive.
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
|
|
1159
|
+
// should receive. Returns both the slice to replay and the actual
|
|
1160
|
+
// historyPolicy applied (which may differ from the requested one
|
|
1161
|
+
// when after_message falls back to full). Validation errors throw
|
|
1162
|
+
// synchronously so callers can rely on either the registration being
|
|
1163
|
+
// in effect or having thrown; the disk-load is the only async work.
|
|
1164
|
+
attach(client, historyPolicy, opts = {}) {
|
|
957
1165
|
if (this.closed) {
|
|
958
1166
|
throw withCode(
|
|
959
1167
|
new Error("session is closed"),
|
|
@@ -969,9 +1177,20 @@ var init_session = __esm({
|
|
|
969
1177
|
this.clients.set(client.clientId, client);
|
|
970
1178
|
this.updatedAt = Date.now();
|
|
971
1179
|
if (historyPolicy === "none" || historyPolicy === "pending_only") {
|
|
972
|
-
return Promise.resolve([]);
|
|
1180
|
+
return Promise.resolve({ entries: [], appliedPolicy: historyPolicy });
|
|
973
1181
|
}
|
|
974
|
-
return this.
|
|
1182
|
+
return this.loadReplay(historyPolicy, opts);
|
|
1183
|
+
}
|
|
1184
|
+
async loadReplay(historyPolicy, opts) {
|
|
1185
|
+
const all = await this.getHistorySnapshot();
|
|
1186
|
+
if (historyPolicy === "after_message") {
|
|
1187
|
+
const cutoff = opts.afterMessageId ? findMessageIdIndex(all, opts.afterMessageId) : -1;
|
|
1188
|
+
if (cutoff < 0) {
|
|
1189
|
+
return { entries: all, appliedPolicy: "full" };
|
|
1190
|
+
}
|
|
1191
|
+
return { entries: all.slice(cutoff + 1), appliedPolicy: "after_message" };
|
|
1192
|
+
}
|
|
1193
|
+
return { entries: all, appliedPolicy: "full" };
|
|
975
1194
|
}
|
|
976
1195
|
// Dispatch in-flight permission requests to a freshly-attached
|
|
977
1196
|
// client. Called by the daemon's WS handler *after* it finishes
|
|
@@ -983,8 +1202,39 @@ var init_session = __esm({
|
|
|
983
1202
|
}
|
|
984
1203
|
}
|
|
985
1204
|
detach(clientId) {
|
|
986
|
-
|
|
987
|
-
|
|
1205
|
+
const leaving = this.clients.get(clientId);
|
|
1206
|
+
if (!leaving) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
this.clients.delete(clientId);
|
|
1210
|
+
this.updatedAt = Date.now();
|
|
1211
|
+
this.broadcastClientDisconnected(leaving);
|
|
1212
|
+
}
|
|
1213
|
+
// Notify remaining attached clients that a peer just left, per
|
|
1214
|
+
// RFD #533. Fires for both explicit session/detach and ws-close
|
|
1215
|
+
// teardown (acp-ws calls Session.detach() in both paths). The
|
|
1216
|
+
// notification is broadcast (not recorded) — peer presence is
|
|
1217
|
+
// transient, not part of conversation history.
|
|
1218
|
+
broadcastClientDisconnected(client) {
|
|
1219
|
+
const info = {
|
|
1220
|
+
clientId: client.clientId
|
|
1221
|
+
};
|
|
1222
|
+
if (client.clientInfo?.name) {
|
|
1223
|
+
info.name = client.clientInfo.name;
|
|
1224
|
+
}
|
|
1225
|
+
if (client.clientInfo?.version) {
|
|
1226
|
+
info.version = client.clientInfo.version;
|
|
1227
|
+
}
|
|
1228
|
+
const update = {
|
|
1229
|
+
sessionUpdate: "client_disconnected",
|
|
1230
|
+
client: info,
|
|
1231
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1232
|
+
};
|
|
1233
|
+
for (const peer of this.clients.values()) {
|
|
1234
|
+
void peer.connection.notify("session/update", {
|
|
1235
|
+
sessionId: this.sessionId,
|
|
1236
|
+
update
|
|
1237
|
+
}).catch(() => void 0);
|
|
988
1238
|
}
|
|
989
1239
|
}
|
|
990
1240
|
async prompt(clientId, params) {
|
|
@@ -1037,6 +1287,7 @@ var init_session = __esm({
|
|
|
1037
1287
|
sessionId: this.sessionId,
|
|
1038
1288
|
update: {
|
|
1039
1289
|
sessionUpdate: "prompt_received",
|
|
1290
|
+
messageId: generateMessageId(),
|
|
1040
1291
|
prompt: promptParams.prompt,
|
|
1041
1292
|
sentBy
|
|
1042
1293
|
}
|
|
@@ -1062,7 +1313,8 @@ var init_session = __esm({
|
|
|
1062
1313
|
broadcastTurnComplete(originatorClientId, response) {
|
|
1063
1314
|
const stopReason = response && typeof response === "object" && "stopReason" in response && typeof response.stopReason === "string" ? response.stopReason : void 0;
|
|
1064
1315
|
const update = {
|
|
1065
|
-
sessionUpdate: "turn_complete"
|
|
1316
|
+
sessionUpdate: "turn_complete",
|
|
1317
|
+
messageId: generateMessageId()
|
|
1066
1318
|
};
|
|
1067
1319
|
if (stopReason !== void 0) {
|
|
1068
1320
|
update.stopReason = stopReason;
|
|
@@ -1679,10 +1931,11 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1679
1931
|
recordAndBroadcast(method, params, excludeClientId) {
|
|
1680
1932
|
const rewritten = this.rewriteForClient(params);
|
|
1681
1933
|
const recordable = !isStateUpdate(method, rewritten);
|
|
1934
|
+
const broadcast = recordable ? ensureMessageIdOnUpdate(method, rewritten) : rewritten;
|
|
1682
1935
|
if (recordable) {
|
|
1683
1936
|
const entry = {
|
|
1684
1937
|
method,
|
|
1685
|
-
params:
|
|
1938
|
+
params: broadcast,
|
|
1686
1939
|
recordedAt: Date.now()
|
|
1687
1940
|
};
|
|
1688
1941
|
this.lastRecordedAt = entry.recordedAt;
|
|
@@ -1690,9 +1943,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1690
1943
|
if (this.historyStore) {
|
|
1691
1944
|
const store = this.historyStore;
|
|
1692
1945
|
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
1693
|
-
if (this.appendCount >=
|
|
1946
|
+
if (this.appendCount >= this.compactEvery) {
|
|
1694
1947
|
this.appendCount = 0;
|
|
1695
|
-
void store.compact(this.sessionId,
|
|
1948
|
+
void store.compact(this.sessionId, this.historyMaxEntries).catch(
|
|
1696
1949
|
() => void 0
|
|
1697
1950
|
);
|
|
1698
1951
|
}
|
|
@@ -1710,7 +1963,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1710
1963
|
if (excludeClientId && client.clientId === excludeClientId) {
|
|
1711
1964
|
continue;
|
|
1712
1965
|
}
|
|
1713
|
-
void client.connection.notify(method,
|
|
1966
|
+
void client.connection.notify(method, broadcast).catch(() => void 0);
|
|
1714
1967
|
}
|
|
1715
1968
|
}
|
|
1716
1969
|
async handlePermissionRequest(params) {
|
|
@@ -1722,11 +1975,13 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1722
1975
|
);
|
|
1723
1976
|
}
|
|
1724
1977
|
const clientParams = this.rewriteForClient(params);
|
|
1978
|
+
const toolCallId = extractToolCallId(clientParams);
|
|
1725
1979
|
return new Promise((resolve5, reject) => {
|
|
1726
1980
|
let settled = false;
|
|
1727
1981
|
const outbound = [];
|
|
1728
1982
|
const entry = { addClient: sendTo };
|
|
1729
1983
|
this.inFlightPermissions.add(entry);
|
|
1984
|
+
const sessionId = this.sessionId;
|
|
1730
1985
|
const settle = (fn) => {
|
|
1731
1986
|
if (settled) {
|
|
1732
1987
|
return;
|
|
@@ -1739,22 +1994,25 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1739
1994
|
if (settled) {
|
|
1740
1995
|
return;
|
|
1741
1996
|
}
|
|
1742
|
-
const
|
|
1997
|
+
const response = client.connection.request(
|
|
1743
1998
|
"session/request_permission",
|
|
1744
1999
|
clientParams
|
|
1745
2000
|
);
|
|
1746
|
-
outbound.push({ client
|
|
2001
|
+
outbound.push({ client });
|
|
1747
2002
|
void response.then((result) => {
|
|
1748
2003
|
settle(() => {
|
|
2004
|
+
const update = buildPermissionResolvedUpdate({
|
|
2005
|
+
toolCallId,
|
|
2006
|
+
result,
|
|
2007
|
+
resolver: client
|
|
2008
|
+
});
|
|
1749
2009
|
for (const o of outbound) {
|
|
1750
2010
|
if (o.client.clientId === client.clientId) {
|
|
1751
2011
|
continue;
|
|
1752
2012
|
}
|
|
1753
|
-
void o.client.connection.notify("session/
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
resolvedBy: client.clientId,
|
|
1757
|
-
result
|
|
2013
|
+
void o.client.connection.notify("session/update", {
|
|
2014
|
+
sessionId,
|
|
2015
|
+
update
|
|
1758
2016
|
}).catch(() => void 0);
|
|
1759
2017
|
}
|
|
1760
2018
|
resolve5(result);
|
|
@@ -1809,9 +2067,187 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1809
2067
|
}
|
|
1810
2068
|
});
|
|
1811
2069
|
|
|
2070
|
+
// src/core/session-store.ts
|
|
2071
|
+
import * as fs5 from "fs/promises";
|
|
2072
|
+
import * as path4 from "path";
|
|
2073
|
+
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
2074
|
+
import { z as z4 } from "zod";
|
|
2075
|
+
function generateLineageId() {
|
|
2076
|
+
return `${HYDRA_LINEAGE_PREFIX}${generateRawId()}`;
|
|
2077
|
+
}
|
|
2078
|
+
function assertSafeId(id) {
|
|
2079
|
+
if (!SESSION_ID_PATTERN.test(id)) {
|
|
2080
|
+
throw new Error(`unsafe session id: ${id}`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
function recordFromMemorySession(args) {
|
|
2084
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2085
|
+
return {
|
|
2086
|
+
sessionId: args.sessionId,
|
|
2087
|
+
lineageId: args.lineageId,
|
|
2088
|
+
upstreamSessionId: args.upstreamSessionId,
|
|
2089
|
+
importedFromSessionId: args.importedFromSessionId,
|
|
2090
|
+
agentId: args.agentId,
|
|
2091
|
+
cwd: args.cwd,
|
|
2092
|
+
title: args.title,
|
|
2093
|
+
agentArgs: args.agentArgs,
|
|
2094
|
+
currentModel: args.currentModel,
|
|
2095
|
+
currentMode: args.currentMode,
|
|
2096
|
+
currentUsage: args.currentUsage,
|
|
2097
|
+
agentCommands: args.agentCommands,
|
|
2098
|
+
createdAt: args.createdAt ?? now,
|
|
2099
|
+
updatedAt: args.updatedAt ?? now
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
2103
|
+
var init_session_store = __esm({
|
|
2104
|
+
"src/core/session-store.ts"() {
|
|
2105
|
+
"use strict";
|
|
2106
|
+
init_paths();
|
|
2107
|
+
HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2108
|
+
generateRawId = customAlphabet2(HYDRA_ID_ALPHABET2, 16);
|
|
2109
|
+
HYDRA_LINEAGE_PREFIX = "hydra_lineage_";
|
|
2110
|
+
PersistedAgentCommand = z4.object({
|
|
2111
|
+
name: z4.string(),
|
|
2112
|
+
description: z4.string().optional()
|
|
2113
|
+
});
|
|
2114
|
+
PersistedUsage = z4.object({
|
|
2115
|
+
used: z4.number().optional(),
|
|
2116
|
+
size: z4.number().optional(),
|
|
2117
|
+
costAmount: z4.number().optional(),
|
|
2118
|
+
costCurrency: z4.string().optional()
|
|
2119
|
+
});
|
|
2120
|
+
SessionRecord = z4.object({
|
|
2121
|
+
version: z4.literal(1),
|
|
2122
|
+
sessionId: z4.string(),
|
|
2123
|
+
// Optional for back-compat with records written before this field
|
|
2124
|
+
// existed; mergeForPersistence generates one on next write so any
|
|
2125
|
+
// touched session converges to having a lineageId. A record that
|
|
2126
|
+
// never gets written again (truly cold and untouched) just won't
|
|
2127
|
+
// participate in lineage-based dedup, which is correct — it was
|
|
2128
|
+
// never exported, so no incoming bundle can claim its lineage.
|
|
2129
|
+
lineageId: z4.string().optional(),
|
|
2130
|
+
upstreamSessionId: z4.string(),
|
|
2131
|
+
// When non-empty, marks a session that was created by import and is
|
|
2132
|
+
// waiting for its first attach to bootstrap a fresh upstream agent
|
|
2133
|
+
// and replay the imported history as a takeover transcript. The
|
|
2134
|
+
// origin's local id at export time, kept for debuggability and as a
|
|
2135
|
+
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
2136
|
+
importedFromSessionId: z4.string().optional(),
|
|
2137
|
+
agentId: z4.string(),
|
|
2138
|
+
cwd: z4.string(),
|
|
2139
|
+
title: z4.string().optional(),
|
|
2140
|
+
agentArgs: z4.array(z4.string()).optional(),
|
|
2141
|
+
// Snapshot of "what is currently true about this session" carried in
|
|
2142
|
+
// meta.json so a late-attaching or cold-resurrected client can be
|
|
2143
|
+
// told via the attach response _meta without depending on history
|
|
2144
|
+
// replay of a snapshot-shaped notification.
|
|
2145
|
+
currentModel: z4.string().optional(),
|
|
2146
|
+
currentMode: z4.string().optional(),
|
|
2147
|
+
currentUsage: PersistedUsage.optional(),
|
|
2148
|
+
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
2149
|
+
createdAt: z4.string(),
|
|
2150
|
+
updatedAt: z4.string()
|
|
2151
|
+
});
|
|
2152
|
+
SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
2153
|
+
SessionStore = class {
|
|
2154
|
+
async write(record) {
|
|
2155
|
+
assertSafeId(record.sessionId);
|
|
2156
|
+
await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
2157
|
+
const full = { version: 1, ...record };
|
|
2158
|
+
await fs5.writeFile(
|
|
2159
|
+
paths.sessionFile(record.sessionId),
|
|
2160
|
+
JSON.stringify(full, null, 2) + "\n",
|
|
2161
|
+
{ encoding: "utf8", mode: 384 }
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
async read(sessionId) {
|
|
2165
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
2166
|
+
return void 0;
|
|
2167
|
+
}
|
|
2168
|
+
let raw;
|
|
2169
|
+
try {
|
|
2170
|
+
raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
const e = err;
|
|
2173
|
+
if (e.code === "ENOENT") {
|
|
2174
|
+
return void 0;
|
|
2175
|
+
}
|
|
2176
|
+
throw err;
|
|
2177
|
+
}
|
|
2178
|
+
try {
|
|
2179
|
+
return SessionRecord.parse(JSON.parse(raw));
|
|
2180
|
+
} catch {
|
|
2181
|
+
return void 0;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
async delete(sessionId) {
|
|
2185
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
try {
|
|
2189
|
+
await fs5.unlink(paths.sessionFile(sessionId));
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
const e = err;
|
|
2192
|
+
if (e.code !== "ENOENT") {
|
|
2193
|
+
throw err;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
try {
|
|
2197
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
const e = err;
|
|
2200
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
2201
|
+
throw err;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
// Find a persisted session by lineageId. Used by SessionManager.import
|
|
2206
|
+
// to detect bundles that have already been imported (lineageId match)
|
|
2207
|
+
// so we can either error out or, with replace:true, overwrite.
|
|
2208
|
+
// Returns undefined if no record has that lineageId. Records that
|
|
2209
|
+
// pre-date the lineageId field simply don't match — which is
|
|
2210
|
+
// correct: they were never exported, so no incoming bundle can
|
|
2211
|
+
// legitimately claim their lineage.
|
|
2212
|
+
async findByLineageId(lineageId) {
|
|
2213
|
+
if (lineageId.length === 0) {
|
|
2214
|
+
return void 0;
|
|
2215
|
+
}
|
|
2216
|
+
const all = await this.list().catch(() => []);
|
|
2217
|
+
for (const record of all) {
|
|
2218
|
+
if (record.lineageId === lineageId) {
|
|
2219
|
+
return record;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return void 0;
|
|
2223
|
+
}
|
|
2224
|
+
async list() {
|
|
2225
|
+
let entries;
|
|
2226
|
+
try {
|
|
2227
|
+
entries = await fs5.readdir(paths.sessionsDir());
|
|
2228
|
+
} catch (err) {
|
|
2229
|
+
const e = err;
|
|
2230
|
+
if (e.code === "ENOENT") {
|
|
2231
|
+
return [];
|
|
2232
|
+
}
|
|
2233
|
+
throw err;
|
|
2234
|
+
}
|
|
2235
|
+
const records = [];
|
|
2236
|
+
for (const entry of entries) {
|
|
2237
|
+
const record = await this.read(entry);
|
|
2238
|
+
if (record) {
|
|
2239
|
+
records.push(record);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
return records;
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
|
|
1812
2248
|
// src/tui/history.ts
|
|
1813
2249
|
import { promises as fs7 } from "fs";
|
|
1814
|
-
import * as
|
|
2250
|
+
import * as path5 from "path";
|
|
1815
2251
|
async function loadHistory(file) {
|
|
1816
2252
|
let text;
|
|
1817
2253
|
try {
|
|
@@ -1855,7 +2291,7 @@ function appendEntry(history, entry) {
|
|
|
1855
2291
|
return out;
|
|
1856
2292
|
}
|
|
1857
2293
|
async function saveHistory(file, history) {
|
|
1858
|
-
await fs7.mkdir(
|
|
2294
|
+
await fs7.mkdir(path5.dirname(file), { recursive: true });
|
|
1859
2295
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1860
2296
|
await fs7.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1861
2297
|
}
|
|
@@ -1867,6 +2303,113 @@ var init_history = __esm({
|
|
|
1867
2303
|
}
|
|
1868
2304
|
});
|
|
1869
2305
|
|
|
2306
|
+
// src/core/hydra-version.ts
|
|
2307
|
+
import { fileURLToPath } from "url";
|
|
2308
|
+
import * as path6 from "path";
|
|
2309
|
+
import * as fs8 from "fs";
|
|
2310
|
+
function resolveVersion() {
|
|
2311
|
+
try {
|
|
2312
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
2313
|
+
for (let i = 0; i < 8; i += 1) {
|
|
2314
|
+
const candidate = path6.join(dir, "package.json");
|
|
2315
|
+
if (fs8.existsSync(candidate)) {
|
|
2316
|
+
const pkg = JSON.parse(fs8.readFileSync(candidate, "utf8"));
|
|
2317
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
2318
|
+
return pkg.version;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const parent = path6.dirname(dir);
|
|
2322
|
+
if (parent === dir) {
|
|
2323
|
+
break;
|
|
2324
|
+
}
|
|
2325
|
+
dir = parent;
|
|
2326
|
+
}
|
|
2327
|
+
} catch {
|
|
2328
|
+
}
|
|
2329
|
+
return "0.0.0";
|
|
2330
|
+
}
|
|
2331
|
+
var HYDRA_VERSION;
|
|
2332
|
+
var init_hydra_version = __esm({
|
|
2333
|
+
"src/core/hydra-version.ts"() {
|
|
2334
|
+
"use strict";
|
|
2335
|
+
HYDRA_VERSION = resolveVersion();
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
// src/core/bundle.ts
|
|
2340
|
+
import { z as z5 } from "zod";
|
|
2341
|
+
function encodeBundle(params) {
|
|
2342
|
+
const bundle = {
|
|
2343
|
+
version: 1,
|
|
2344
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2345
|
+
exportedFrom: {
|
|
2346
|
+
hydraVersion: params.hydraVersion,
|
|
2347
|
+
machine: params.machine
|
|
2348
|
+
},
|
|
2349
|
+
session: {
|
|
2350
|
+
sessionId: params.record.sessionId,
|
|
2351
|
+
lineageId: params.record.lineageId,
|
|
2352
|
+
agentId: params.record.agentId,
|
|
2353
|
+
cwd: params.record.cwd,
|
|
2354
|
+
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
2355
|
+
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
2356
|
+
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
2357
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
2358
|
+
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
2359
|
+
createdAt: params.record.createdAt,
|
|
2360
|
+
updatedAt: params.record.updatedAt
|
|
2361
|
+
},
|
|
2362
|
+
history: params.history
|
|
2363
|
+
};
|
|
2364
|
+
if (params.promptHistory !== void 0) {
|
|
2365
|
+
bundle.promptHistory = params.promptHistory;
|
|
2366
|
+
}
|
|
2367
|
+
return bundle;
|
|
2368
|
+
}
|
|
2369
|
+
function decodeBundle(raw) {
|
|
2370
|
+
return Bundle.parse(raw);
|
|
2371
|
+
}
|
|
2372
|
+
var HistoryEntrySchema, BundleSession, Bundle;
|
|
2373
|
+
var init_bundle = __esm({
|
|
2374
|
+
"src/core/bundle.ts"() {
|
|
2375
|
+
"use strict";
|
|
2376
|
+
init_session_store();
|
|
2377
|
+
HistoryEntrySchema = z5.object({
|
|
2378
|
+
method: z5.string(),
|
|
2379
|
+
params: z5.unknown(),
|
|
2380
|
+
recordedAt: z5.number()
|
|
2381
|
+
});
|
|
2382
|
+
BundleSession = z5.object({
|
|
2383
|
+
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
2384
|
+
// the local namespace; lineageId is what survives across hops).
|
|
2385
|
+
sessionId: z5.string(),
|
|
2386
|
+
// Required on bundles — the export path backfills if the source
|
|
2387
|
+
// record was written before lineageId existed.
|
|
2388
|
+
lineageId: z5.string(),
|
|
2389
|
+
agentId: z5.string(),
|
|
2390
|
+
cwd: z5.string(),
|
|
2391
|
+
title: z5.string().optional(),
|
|
2392
|
+
currentModel: z5.string().optional(),
|
|
2393
|
+
currentMode: z5.string().optional(),
|
|
2394
|
+
currentUsage: PersistedUsage.optional(),
|
|
2395
|
+
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
2396
|
+
createdAt: z5.string(),
|
|
2397
|
+
updatedAt: z5.string()
|
|
2398
|
+
});
|
|
2399
|
+
Bundle = z5.object({
|
|
2400
|
+
version: z5.literal(1),
|
|
2401
|
+
exportedAt: z5.string(),
|
|
2402
|
+
exportedFrom: z5.object({
|
|
2403
|
+
hydraVersion: z5.string(),
|
|
2404
|
+
machine: z5.string()
|
|
2405
|
+
}),
|
|
2406
|
+
session: BundleSession,
|
|
2407
|
+
history: z5.array(HistoryEntrySchema),
|
|
2408
|
+
promptHistory: z5.array(z5.string()).optional()
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
|
|
1870
2413
|
// src/acp/ws-stream.ts
|
|
1871
2414
|
function wsToMessageStream(ws) {
|
|
1872
2415
|
const messageHandlers = [];
|
|
@@ -1945,7 +2488,7 @@ var init_ws_stream = __esm({
|
|
|
1945
2488
|
});
|
|
1946
2489
|
|
|
1947
2490
|
// src/core/daemon-bootstrap.ts
|
|
1948
|
-
import { spawn as
|
|
2491
|
+
import { spawn as spawn5 } from "child_process";
|
|
1949
2492
|
import { setTimeout as sleep } from "timers/promises";
|
|
1950
2493
|
async function ensureDaemonReachable(config) {
|
|
1951
2494
|
if (await pingHealth(config)) {
|
|
@@ -1972,7 +2515,7 @@ function spawnDaemonDetached() {
|
|
|
1972
2515
|
if (!cliPath) {
|
|
1973
2516
|
throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
|
|
1974
2517
|
}
|
|
1975
|
-
const child =
|
|
2518
|
+
const child = spawn5(
|
|
1976
2519
|
process.execPath,
|
|
1977
2520
|
[cliPath, "daemon", "start", "--foreground"],
|
|
1978
2521
|
{
|
|
@@ -2020,8 +2563,8 @@ function formatAgentWithModel(agentId, model) {
|
|
|
2020
2563
|
}
|
|
2021
2564
|
return `${agent}${AGENT_MODEL_SEP}${short}`;
|
|
2022
2565
|
}
|
|
2023
|
-
function formatAgentCell(agentId,
|
|
2024
|
-
const base =
|
|
2566
|
+
function formatAgentCell(agentId, usage) {
|
|
2567
|
+
const base = agentId ?? "?";
|
|
2025
2568
|
if (!usage || typeof usage.costAmount !== "number") {
|
|
2026
2569
|
return base;
|
|
2027
2570
|
}
|
|
@@ -2058,10 +2601,10 @@ function toRow(s, now = Date.now()) {
|
|
|
2058
2601
|
session: stripHydraSessionPrefix(s.sessionId),
|
|
2059
2602
|
upstream: s.upstreamSessionId ?? "-",
|
|
2060
2603
|
state: formatState(s.status, s.attachedClients),
|
|
2061
|
-
agent: formatAgentCell(s.agentId, s.
|
|
2604
|
+
agent: formatAgentCell(s.agentId, s.currentUsage),
|
|
2062
2605
|
age: formatRelativeAge(s.updatedAt, now),
|
|
2063
2606
|
title: s.title ?? "-",
|
|
2064
|
-
cwd: s.cwd
|
|
2607
|
+
cwd: shortenHomePath(s.cwd)
|
|
2065
2608
|
};
|
|
2066
2609
|
}
|
|
2067
2610
|
function formatState(status, clients) {
|
|
@@ -2077,6 +2620,7 @@ function computeWidths(rows) {
|
|
|
2077
2620
|
state: maxLen(HEADER.state, rows.map((r) => r.state)),
|
|
2078
2621
|
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
2079
2622
|
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
2623
|
+
cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
|
|
2080
2624
|
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
2081
2625
|
};
|
|
2082
2626
|
}
|
|
@@ -2125,7 +2669,7 @@ function maxLen(headerCell, values) {
|
|
|
2125
2669
|
}
|
|
2126
2670
|
return max;
|
|
2127
2671
|
}
|
|
2128
|
-
function formatRow(r, w, maxWidth) {
|
|
2672
|
+
function formatRow(r, w, maxWidth, cwdMaxWidth = DEFAULT_CWD_MAX_WIDTH) {
|
|
2129
2673
|
const fixed = [
|
|
2130
2674
|
r.session.padEnd(w.session),
|
|
2131
2675
|
r.upstream.padEnd(w.upstream),
|
|
@@ -2134,20 +2678,18 @@ function formatRow(r, w, maxWidth) {
|
|
|
2134
2678
|
r.age.padStart(w.age)
|
|
2135
2679
|
].join(SEP);
|
|
2136
2680
|
if (maxWidth === void 0) {
|
|
2137
|
-
return [fixed, r.
|
|
2681
|
+
return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
|
|
2138
2682
|
}
|
|
2139
|
-
const titleCap = Math.min(w.title, TITLE_MAX_WIDTH);
|
|
2140
2683
|
const budget = maxWidth - fixed.length - SEP.length;
|
|
2141
2684
|
if (budget <= 0) {
|
|
2142
2685
|
return fixed.slice(0, maxWidth);
|
|
2143
2686
|
}
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
return [fixed, titleCell, cwdCell].join(SEP);
|
|
2687
|
+
const cwdCap = Math.min(w.cwd, cwdMaxWidth);
|
|
2688
|
+
const cwdAlloc = Math.min(cwdCap, Math.max(0, budget - SEP.length - 1));
|
|
2689
|
+
const cwdCell = truncateMiddle(r.cwd, cwdAlloc).padEnd(cwdAlloc);
|
|
2690
|
+
const titleBudget = Math.max(0, budget - cwdAlloc - SEP.length);
|
|
2691
|
+
const titleCell = truncateRight(r.title, titleBudget);
|
|
2692
|
+
return [fixed, cwdCell, titleCell].join(SEP);
|
|
2151
2693
|
}
|
|
2152
2694
|
function truncateRight(s, max) {
|
|
2153
2695
|
if (max <= 0) {
|
|
@@ -2175,11 +2717,12 @@ function truncateMiddle(s, max) {
|
|
|
2175
2717
|
const tail = max - 1 - head;
|
|
2176
2718
|
return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
|
|
2177
2719
|
}
|
|
2178
|
-
var HEADER, SEP,
|
|
2720
|
+
var HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
|
|
2179
2721
|
var init_session_row = __esm({
|
|
2180
2722
|
"src/cli/session-row.ts"() {
|
|
2181
2723
|
"use strict";
|
|
2182
2724
|
init_agent_display();
|
|
2725
|
+
init_paths();
|
|
2183
2726
|
init_session();
|
|
2184
2727
|
HEADER = {
|
|
2185
2728
|
session: "SESSION",
|
|
@@ -2191,14 +2734,13 @@ var init_session_row = __esm({
|
|
|
2191
2734
|
cwd: "CWD"
|
|
2192
2735
|
};
|
|
2193
2736
|
SEP = " ";
|
|
2194
|
-
|
|
2195
|
-
TITLE_MAX_WIDTH = 40;
|
|
2737
|
+
DEFAULT_CWD_MAX_WIDTH = 24;
|
|
2196
2738
|
}
|
|
2197
2739
|
});
|
|
2198
2740
|
|
|
2199
2741
|
// src/cli/commands/sessions.ts
|
|
2200
|
-
import * as
|
|
2201
|
-
import * as
|
|
2742
|
+
import * as fs13 from "fs/promises";
|
|
2743
|
+
import * as path8 from "path";
|
|
2202
2744
|
async function runSessionsList(opts = {}) {
|
|
2203
2745
|
const config = await loadConfig();
|
|
2204
2746
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -2237,9 +2779,10 @@ async function runSessionsList(opts = {}) {
|
|
|
2237
2779
|
const rows = visible.map((s) => toRow(s, now));
|
|
2238
2780
|
const widths = computeWidths(rows);
|
|
2239
2781
|
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2240
|
-
|
|
2782
|
+
const cwdMax = config.tui.cwdColumnMaxWidth;
|
|
2783
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
|
|
2241
2784
|
for (const r of rows) {
|
|
2242
|
-
process.stdout.write(formatRow(r, widths, maxWidth) + "\n");
|
|
2785
|
+
process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
|
|
2243
2786
|
}
|
|
2244
2787
|
if (truncated > 0) {
|
|
2245
2788
|
process.stdout.write(
|
|
@@ -2268,9 +2811,9 @@ async function runSessionsKill(id) {
|
|
|
2268
2811
|
process.stdout.write(`Killed ${id}
|
|
2269
2812
|
`);
|
|
2270
2813
|
}
|
|
2271
|
-
async function
|
|
2814
|
+
async function runSessionsRemove(id) {
|
|
2272
2815
|
if (!id) {
|
|
2273
|
-
process.stderr.write("Usage: hydra-acp sessions
|
|
2816
|
+
process.stderr.write("Usage: hydra-acp sessions remove <session-id>\n");
|
|
2274
2817
|
process.exit(2);
|
|
2275
2818
|
}
|
|
2276
2819
|
const config = await loadConfig();
|
|
@@ -2317,23 +2860,40 @@ async function runSessionsExport(id, outPath) {
|
|
|
2317
2860
|
return;
|
|
2318
2861
|
}
|
|
2319
2862
|
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
2320
|
-
await
|
|
2321
|
-
await
|
|
2863
|
+
await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
|
|
2864
|
+
await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2322
2865
|
process.stdout.write(`Wrote ${resolved}
|
|
2323
2866
|
`);
|
|
2324
2867
|
}
|
|
2325
2868
|
async function runSessionsImport(file, opts = {}) {
|
|
2326
2869
|
if (!file) {
|
|
2327
2870
|
process.stderr.write(
|
|
2328
|
-
"Usage: hydra-acp sessions import <file>|- [--replace]\n"
|
|
2871
|
+
"Usage: hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]\n"
|
|
2329
2872
|
);
|
|
2330
2873
|
process.exit(2);
|
|
2331
2874
|
}
|
|
2875
|
+
let cwdOverride;
|
|
2876
|
+
if (opts.cwd !== void 0) {
|
|
2877
|
+
const resolved = path8.resolve(opts.cwd);
|
|
2878
|
+
try {
|
|
2879
|
+
const stat4 = await fs13.stat(resolved);
|
|
2880
|
+
if (!stat4.isDirectory()) {
|
|
2881
|
+
process.stderr.write(`--cwd ${resolved} is not a directory
|
|
2882
|
+
`);
|
|
2883
|
+
process.exit(1);
|
|
2884
|
+
}
|
|
2885
|
+
} catch {
|
|
2886
|
+
process.stderr.write(`--cwd ${resolved} does not exist
|
|
2887
|
+
`);
|
|
2888
|
+
process.exit(1);
|
|
2889
|
+
}
|
|
2890
|
+
cwdOverride = resolved;
|
|
2891
|
+
}
|
|
2332
2892
|
let body;
|
|
2333
2893
|
if (file === "-") {
|
|
2334
2894
|
body = await readStdin();
|
|
2335
2895
|
} else {
|
|
2336
|
-
body = await
|
|
2896
|
+
body = await fs13.readFile(file, "utf8");
|
|
2337
2897
|
}
|
|
2338
2898
|
let bundle;
|
|
2339
2899
|
try {
|
|
@@ -2343,6 +2903,11 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2343
2903
|
`);
|
|
2344
2904
|
process.exit(1);
|
|
2345
2905
|
}
|
|
2906
|
+
if (opts.info === true) {
|
|
2907
|
+
const inspectConfig = await loadConfigReadOnly();
|
|
2908
|
+
printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2346
2911
|
const config = await loadConfig();
|
|
2347
2912
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2348
2913
|
const response = await fetch(`${baseUrl}/v1/sessions/import`, {
|
|
@@ -2351,7 +2916,11 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2351
2916
|
"Content-Type": "application/json",
|
|
2352
2917
|
Authorization: `Bearer ${config.daemon.authToken}`
|
|
2353
2918
|
},
|
|
2354
|
-
body: JSON.stringify({
|
|
2919
|
+
body: JSON.stringify({
|
|
2920
|
+
bundle,
|
|
2921
|
+
replace: opts.replace === true,
|
|
2922
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
2923
|
+
})
|
|
2355
2924
|
});
|
|
2356
2925
|
if (response.status === 409) {
|
|
2357
2926
|
const detail = await response.json().catch(() => ({}));
|
|
@@ -2374,6 +2943,42 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2374
2943
|
`
|
|
2375
2944
|
);
|
|
2376
2945
|
}
|
|
2946
|
+
function bundleToSummary(parsed) {
|
|
2947
|
+
return {
|
|
2948
|
+
sessionId: parsed.session.sessionId,
|
|
2949
|
+
upstreamSessionId: "-",
|
|
2950
|
+
cwd: parsed.session.cwd,
|
|
2951
|
+
agentId: parsed.session.agentId,
|
|
2952
|
+
currentUsage: parsed.session.currentUsage,
|
|
2953
|
+
title: parsed.session.title,
|
|
2954
|
+
attachedClients: 0,
|
|
2955
|
+
updatedAt: parsed.session.updatedAt,
|
|
2956
|
+
status: "cold"
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
function printBundleInfo(raw, cwdColumnMaxWidth) {
|
|
2960
|
+
let parsed;
|
|
2961
|
+
try {
|
|
2962
|
+
parsed = decodeBundle(raw);
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
process.stderr.write(`Not a valid bundle: ${err.message}
|
|
2965
|
+
`);
|
|
2966
|
+
process.exit(1);
|
|
2967
|
+
}
|
|
2968
|
+
const summary = bundleToSummary(parsed);
|
|
2969
|
+
const row = toRow(summary);
|
|
2970
|
+
const widths = computeWidths([row]);
|
|
2971
|
+
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2972
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
2973
|
+
process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
2974
|
+
process.stdout.write(
|
|
2975
|
+
`
|
|
2976
|
+
lineage: ${parsed.session.lineageId}
|
|
2977
|
+
exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
|
|
2978
|
+
history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
|
|
2979
|
+
` : "\n")
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2377
2982
|
async function readStdin() {
|
|
2378
2983
|
const chunks = [];
|
|
2379
2984
|
for await (const chunk of process.stdin) {
|
|
@@ -2400,6 +3005,7 @@ var init_sessions = __esm({
|
|
|
2400
3005
|
"src/cli/commands/sessions.ts"() {
|
|
2401
3006
|
"use strict";
|
|
2402
3007
|
init_config();
|
|
3008
|
+
init_bundle();
|
|
2403
3009
|
init_session_row();
|
|
2404
3010
|
}
|
|
2405
3011
|
});
|
|
@@ -2734,12 +3340,15 @@ async function pickSession(term, opts) {
|
|
|
2734
3340
|
return b.updatedAt.localeCompare(a.updatedAt);
|
|
2735
3341
|
});
|
|
2736
3342
|
};
|
|
2737
|
-
let
|
|
3343
|
+
let allSessions = sortSessions(opts.sessions);
|
|
3344
|
+
let visible = allSessions;
|
|
2738
3345
|
let rows = visible.map((s) => toRow(s, Date.now()));
|
|
2739
3346
|
let widths = computeWidths(rows);
|
|
2740
3347
|
let total = 1 + visible.length;
|
|
2741
3348
|
let selectedIdx = 0;
|
|
2742
3349
|
let scrollOffset = 0;
|
|
3350
|
+
let searchActive = false;
|
|
3351
|
+
let searchTerm = "";
|
|
2743
3352
|
let mode = "normal";
|
|
2744
3353
|
let pendingAction = null;
|
|
2745
3354
|
let transientStatus = null;
|
|
@@ -2750,6 +3359,7 @@ async function pickSession(term, opts) {
|
|
|
2750
3359
|
let headerLine = "";
|
|
2751
3360
|
let sessionLines = [];
|
|
2752
3361
|
let startRow = 1;
|
|
3362
|
+
const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
|
|
2753
3363
|
const computeLayout = () => {
|
|
2754
3364
|
termHeight = readTermHeight(term);
|
|
2755
3365
|
termWidth = readTermWidth(term);
|
|
@@ -2757,8 +3367,8 @@ async function pickSession(term, opts) {
|
|
|
2757
3367
|
viewportSize = Math.min(visible.length, maxViewportRows);
|
|
2758
3368
|
const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
|
|
2759
3369
|
newSessionLabel = formatNewSessionLabel(opts.cwd, rowMaxWidth);
|
|
2760
|
-
headerLine = formatRow(HEADER, widths, rowMaxWidth);
|
|
2761
|
-
sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth));
|
|
3370
|
+
headerLine = formatRow(HEADER, widths, rowMaxWidth, cwdMaxWidth);
|
|
3371
|
+
sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth, cwdMaxWidth));
|
|
2762
3372
|
};
|
|
2763
3373
|
const rebuildRows = () => {
|
|
2764
3374
|
rows = visible.map((s) => toRow(s, Date.now()));
|
|
@@ -2766,6 +3376,24 @@ async function pickSession(term, opts) {
|
|
|
2766
3376
|
total = 1 + visible.length;
|
|
2767
3377
|
computeLayout();
|
|
2768
3378
|
};
|
|
3379
|
+
const applyFilter = () => {
|
|
3380
|
+
if (searchActive && searchTerm.length > 0) {
|
|
3381
|
+
visible = allSessions.filter((s) => matchesSearch(s, searchTerm));
|
|
3382
|
+
} else {
|
|
3383
|
+
visible = allSessions;
|
|
3384
|
+
}
|
|
3385
|
+
rebuildRows();
|
|
3386
|
+
if (searchActive) {
|
|
3387
|
+
scrollOffset = 0;
|
|
3388
|
+
selectedIdx = visible.length > 0 ? 1 : 0;
|
|
3389
|
+
} else if (selectedIdx > total - 1) {
|
|
3390
|
+
selectedIdx = Math.max(0, total - 1);
|
|
3391
|
+
}
|
|
3392
|
+
if (scrollOffset + viewportSize > visible.length) {
|
|
3393
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
3394
|
+
}
|
|
3395
|
+
adjustScroll();
|
|
3396
|
+
};
|
|
2769
3397
|
const adjustScroll = () => {
|
|
2770
3398
|
if (selectedIdx === 0) {
|
|
2771
3399
|
return;
|
|
@@ -2828,6 +3456,13 @@ async function pickSession(term, opts) {
|
|
|
2828
3456
|
term.dim.noFormat(` ${transientStatus}`);
|
|
2829
3457
|
return;
|
|
2830
3458
|
}
|
|
3459
|
+
if (searchActive) {
|
|
3460
|
+
term.brightYellow.noFormat(` /${searchTerm}`);
|
|
3461
|
+
term.bgBrightYellow(" ");
|
|
3462
|
+
const hint = visible.length === 0 ? " no matches" : ` ${visible.length} match${visible.length === 1 ? "" : "es"}`;
|
|
3463
|
+
term.dim.noFormat(`${hint} \xB7 ^c clears`);
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
2831
3466
|
term.dim.noFormat(formatIndicator());
|
|
2832
3467
|
};
|
|
2833
3468
|
const indicatorRow = () => startRow + 3 + viewportSize;
|
|
@@ -2894,8 +3529,8 @@ async function pickSession(term, opts) {
|
|
|
2894
3529
|
const refresh = async (preferredId) => {
|
|
2895
3530
|
try {
|
|
2896
3531
|
const next = await listSessions(opts.config);
|
|
2897
|
-
|
|
2898
|
-
|
|
3532
|
+
allSessions = sortSessions(next);
|
|
3533
|
+
applyFilter();
|
|
2899
3534
|
if (preferredId !== void 0) {
|
|
2900
3535
|
const idx = visible.findIndex((s) => s.sessionId === preferredId);
|
|
2901
3536
|
if (idx >= 0) {
|
|
@@ -2992,7 +3627,37 @@ async function pickSession(term, opts) {
|
|
|
2992
3627
|
return;
|
|
2993
3628
|
}
|
|
2994
3629
|
clearTransient();
|
|
3630
|
+
if (searchActive) {
|
|
3631
|
+
if (data?.isCharacter) {
|
|
3632
|
+
searchTerm += name;
|
|
3633
|
+
applyFilter();
|
|
3634
|
+
renderFromScratch();
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
if (name === "BACKSPACE") {
|
|
3638
|
+
if (searchTerm.length > 0) {
|
|
3639
|
+
searchTerm = searchTerm.slice(0, -1);
|
|
3640
|
+
applyFilter();
|
|
3641
|
+
renderFromScratch();
|
|
3642
|
+
}
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
if (name === "ESCAPE" || name === "CTRL_C") {
|
|
3646
|
+
searchActive = false;
|
|
3647
|
+
searchTerm = "";
|
|
3648
|
+
applyFilter();
|
|
3649
|
+
renderFromScratch();
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
2995
3653
|
if (data?.isCharacter) {
|
|
3654
|
+
if (name === "/") {
|
|
3655
|
+
searchActive = true;
|
|
3656
|
+
searchTerm = "";
|
|
3657
|
+
applyFilter();
|
|
3658
|
+
renderFromScratch();
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
2996
3661
|
if (name === "r" || name === "R") {
|
|
2997
3662
|
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
2998
3663
|
void refresh(currentId);
|
|
@@ -3097,13 +3762,34 @@ function readTermWidth(term) {
|
|
|
3097
3762
|
function formatNewSessionLabel(cwd, maxWidth) {
|
|
3098
3763
|
const prefix = "+ New session in ";
|
|
3099
3764
|
const budget = Math.max(1, maxWidth - prefix.length);
|
|
3100
|
-
return prefix + truncateMiddle(cwd, budget);
|
|
3765
|
+
return prefix + truncateMiddle(shortenHomePath(cwd), budget);
|
|
3766
|
+
}
|
|
3767
|
+
function matchesSearch(s, term) {
|
|
3768
|
+
if (term.length === 0) {
|
|
3769
|
+
return true;
|
|
3770
|
+
}
|
|
3771
|
+
const t = term.toLowerCase();
|
|
3772
|
+
const haystacks = [
|
|
3773
|
+
stripHydraSessionPrefix(s.sessionId),
|
|
3774
|
+
s.upstreamSessionId ?? "",
|
|
3775
|
+
s.agentId ?? "",
|
|
3776
|
+
s.title ?? "",
|
|
3777
|
+
s.cwd,
|
|
3778
|
+
shortenHomePath(s.cwd)
|
|
3779
|
+
];
|
|
3780
|
+
for (const h of haystacks) {
|
|
3781
|
+
if (h.toLowerCase().includes(t)) {
|
|
3782
|
+
return true;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
return false;
|
|
3101
3786
|
}
|
|
3102
3787
|
var ROW_PREFIX_WIDTH;
|
|
3103
3788
|
var init_picker = __esm({
|
|
3104
3789
|
"src/tui/picker.ts"() {
|
|
3105
3790
|
"use strict";
|
|
3106
3791
|
init_session_row();
|
|
3792
|
+
init_paths();
|
|
3107
3793
|
init_session();
|
|
3108
3794
|
init_discovery();
|
|
3109
3795
|
ROW_PREFIX_WIDTH = 2;
|
|
@@ -3111,14 +3797,14 @@ var init_picker = __esm({
|
|
|
3111
3797
|
});
|
|
3112
3798
|
|
|
3113
3799
|
// src/tui/screen.ts
|
|
3114
|
-
import os3 from "os";
|
|
3115
3800
|
import stringWidth from "string-width";
|
|
3116
3801
|
import wrapAnsi from "wrap-ansi";
|
|
3117
|
-
function formattedLineSig(zone, width, line) {
|
|
3802
|
+
function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null) {
|
|
3803
|
+
const active = activeCol === null ? "" : `a${activeCol}`;
|
|
3118
3804
|
if (!line) {
|
|
3119
|
-
return `${zone}|${width}|empty`;
|
|
3805
|
+
return `${zone}|${width}|empty|${highlight2 ?? ""}|${active}`;
|
|
3120
3806
|
}
|
|
3121
|
-
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}`;
|
|
3807
|
+
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}|${highlight2 ?? ""}|${active}`;
|
|
3122
3808
|
}
|
|
3123
3809
|
function computePromptVisualRows(buffer, room) {
|
|
3124
3810
|
const rows = [];
|
|
@@ -3130,9 +3816,24 @@ function computePromptVisualRows(buffer, room) {
|
|
|
3130
3816
|
}
|
|
3131
3817
|
let pos = 0;
|
|
3132
3818
|
while (pos < line.length) {
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3819
|
+
if (line.length - pos <= room) {
|
|
3820
|
+
rows.push({ bufferIdx: i, startCol: pos, endCol: line.length });
|
|
3821
|
+
pos = line.length;
|
|
3822
|
+
break;
|
|
3823
|
+
}
|
|
3824
|
+
let breakAt = -1;
|
|
3825
|
+
for (let j = pos + room - 1; j >= pos; j--) {
|
|
3826
|
+
const c = line[j];
|
|
3827
|
+
if (c === " " || c === " ") {
|
|
3828
|
+
breakAt = j + 1;
|
|
3829
|
+
break;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
if (breakAt === -1) {
|
|
3833
|
+
breakAt = pos + room;
|
|
3834
|
+
}
|
|
3835
|
+
rows.push({ bufferIdx: i, startCol: pos, endCol: breakAt });
|
|
3836
|
+
pos = breakAt;
|
|
3136
3837
|
}
|
|
3137
3838
|
}
|
|
3138
3839
|
if (rows.length === 0) {
|
|
@@ -3180,6 +3881,34 @@ function computePromptLayout(visualRows, state, maxRows) {
|
|
|
3180
3881
|
}
|
|
3181
3882
|
return { cursorVisualRow, cursorVisualCol, windowStart, rendered };
|
|
3182
3883
|
}
|
|
3884
|
+
function writeBodyWithHighlight(termObj, text, style, term, activeCol = null, _activeLength = 0) {
|
|
3885
|
+
if (text.length === 0) {
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
if (term.length === 0) {
|
|
3889
|
+
writeStyled(termObj, text, style);
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3892
|
+
const haystack = text.toLowerCase();
|
|
3893
|
+
let i = 0;
|
|
3894
|
+
while (i < text.length) {
|
|
3895
|
+
const next = haystack.indexOf(term, i);
|
|
3896
|
+
if (next === -1) {
|
|
3897
|
+
writeStyled(termObj, text.slice(i), style);
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
if (next > i) {
|
|
3901
|
+
writeStyled(termObj, text.slice(i, next), style);
|
|
3902
|
+
}
|
|
3903
|
+
const isActive = activeCol !== null && next === activeCol;
|
|
3904
|
+
writeStyled(
|
|
3905
|
+
termObj,
|
|
3906
|
+
text.slice(next, next + term.length),
|
|
3907
|
+
isActive ? "search-highlight-active" : "search-highlight"
|
|
3908
|
+
);
|
|
3909
|
+
i = next + term.length;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3183
3912
|
function writeStyled(term, text, style) {
|
|
3184
3913
|
if (text.length === 0) {
|
|
3185
3914
|
return;
|
|
@@ -3242,6 +3971,12 @@ function writeStyled(term, text, style) {
|
|
|
3242
3971
|
case "heading-3":
|
|
3243
3972
|
term.bold.noFormat(text);
|
|
3244
3973
|
return;
|
|
3974
|
+
case "search-highlight":
|
|
3975
|
+
term.bgBrightYellow.black.noFormat(text);
|
|
3976
|
+
return;
|
|
3977
|
+
case "search-highlight-active":
|
|
3978
|
+
term.bgRed.brightWhite.noFormat(text);
|
|
3979
|
+
return;
|
|
3245
3980
|
default:
|
|
3246
3981
|
term.noFormat(text);
|
|
3247
3982
|
}
|
|
@@ -3255,17 +3990,80 @@ function wrapAnsiBody(text, width) {
|
|
|
3255
3990
|
}
|
|
3256
3991
|
return wrapAnsi(text, width, { hard: true, trim: false }).split("\n");
|
|
3257
3992
|
}
|
|
3258
|
-
function
|
|
3993
|
+
function matchTkMarkupAt(text, i) {
|
|
3994
|
+
if (text.charCodeAt(i) !== 94) {
|
|
3995
|
+
return null;
|
|
3996
|
+
}
|
|
3997
|
+
const c = text[i + 1];
|
|
3998
|
+
if (c === void 0) {
|
|
3999
|
+
return null;
|
|
4000
|
+
}
|
|
4001
|
+
if (c === "^") {
|
|
4002
|
+
return { text: "^^", width: 1 };
|
|
4003
|
+
}
|
|
4004
|
+
if (c === "[") {
|
|
4005
|
+
const end = text.indexOf("]", i + 2);
|
|
4006
|
+
if (end !== -1) {
|
|
4007
|
+
return { text: text.slice(i, end + 1), width: 0 };
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
if (TK_MARKUP_STYLE_CHAR.test(c)) {
|
|
4011
|
+
return { text: text.slice(i, i + 2), width: 0 };
|
|
4012
|
+
}
|
|
4013
|
+
return null;
|
|
4014
|
+
}
|
|
4015
|
+
function hasTkMarkup(text) {
|
|
4016
|
+
if (!text.includes("^")) {
|
|
4017
|
+
return false;
|
|
4018
|
+
}
|
|
4019
|
+
for (let i = 0; i < text.length; i++) {
|
|
4020
|
+
if (matchTkMarkupAt(text, i)) {
|
|
4021
|
+
return true;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
return false;
|
|
4025
|
+
}
|
|
4026
|
+
function* segmentForWidth(text) {
|
|
4027
|
+
let i = 0;
|
|
4028
|
+
while (i < text.length) {
|
|
4029
|
+
const m = matchTkMarkupAt(text, i);
|
|
4030
|
+
if (m) {
|
|
4031
|
+
yield { text: m.text, width: m.width };
|
|
4032
|
+
i += m.text.length;
|
|
4033
|
+
continue;
|
|
4034
|
+
}
|
|
4035
|
+
let runEnd = text.length;
|
|
4036
|
+
let probe = text.indexOf("^", i);
|
|
4037
|
+
while (probe !== -1 && probe < text.length) {
|
|
4038
|
+
if (matchTkMarkupAt(text, probe)) {
|
|
4039
|
+
runEnd = probe;
|
|
4040
|
+
break;
|
|
4041
|
+
}
|
|
4042
|
+
probe = text.indexOf("^", probe + 1);
|
|
4043
|
+
}
|
|
4044
|
+
if (runEnd === i) {
|
|
4045
|
+
yield { text: "^", width: 1 };
|
|
4046
|
+
i += 1;
|
|
4047
|
+
continue;
|
|
4048
|
+
}
|
|
4049
|
+
for (const { segment } of SEGMENTER.segment(text.slice(i, runEnd))) {
|
|
4050
|
+
yield { text: segment, width: stringWidth(segment) };
|
|
4051
|
+
}
|
|
4052
|
+
i = runEnd;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
function wrap(text, width, opts = {}) {
|
|
3259
4056
|
if (width <= 0) {
|
|
3260
4057
|
return [text];
|
|
3261
4058
|
}
|
|
3262
4059
|
if (text.length === 0) {
|
|
3263
4060
|
return [""];
|
|
3264
4061
|
}
|
|
3265
|
-
|
|
4062
|
+
const stripMarkup = opts.stripMarkup === true && hasTkMarkup(text);
|
|
4063
|
+
if (!stripMarkup && !NON_ASCII.test(text)) {
|
|
3266
4064
|
return wrapAscii(text, width);
|
|
3267
4065
|
}
|
|
3268
|
-
return wrapVisible(text, width);
|
|
4066
|
+
return wrapVisible(text, width, stripMarkup);
|
|
3269
4067
|
}
|
|
3270
4068
|
function wrapAscii(text, width) {
|
|
3271
4069
|
const out = [];
|
|
@@ -3290,32 +4088,33 @@ function wrapAscii(text, width) {
|
|
|
3290
4088
|
out.push(remaining);
|
|
3291
4089
|
return out;
|
|
3292
4090
|
}
|
|
3293
|
-
function wrapVisible(text, width) {
|
|
4091
|
+
function wrapVisible(text, width, stripMarkup) {
|
|
3294
4092
|
const out = [];
|
|
3295
|
-
const
|
|
3296
|
-
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3297
|
-
graphemes.push({ seg: segment, w: stringWidth(segment) });
|
|
3298
|
-
}
|
|
4093
|
+
const segments = stripMarkup ? [...segmentForWidth(text)] : graphemeSegments(text);
|
|
3299
4094
|
let i = 0;
|
|
3300
|
-
while (i <
|
|
4095
|
+
while (i < segments.length) {
|
|
3301
4096
|
let chunk = "";
|
|
3302
4097
|
let chunkW = 0;
|
|
3303
4098
|
let lastSpaceI = -1;
|
|
3304
4099
|
let chunkAtLastSpace = "";
|
|
3305
|
-
while (i <
|
|
3306
|
-
const
|
|
3307
|
-
if (chunkW +
|
|
4100
|
+
while (i < segments.length) {
|
|
4101
|
+
const s = segments[i];
|
|
4102
|
+
if (chunkW + s.width > width) {
|
|
4103
|
+
if (s.text === " " && s.width === 1) {
|
|
4104
|
+
lastSpaceI = i;
|
|
4105
|
+
chunkAtLastSpace = chunk;
|
|
4106
|
+
}
|
|
3308
4107
|
break;
|
|
3309
4108
|
}
|
|
3310
|
-
if (
|
|
4109
|
+
if (s.text === " " && s.width === 1) {
|
|
3311
4110
|
lastSpaceI = i;
|
|
3312
4111
|
chunkAtLastSpace = chunk;
|
|
3313
4112
|
}
|
|
3314
|
-
chunk +=
|
|
3315
|
-
chunkW +=
|
|
4113
|
+
chunk += s.text;
|
|
4114
|
+
chunkW += s.width;
|
|
3316
4115
|
i += 1;
|
|
3317
4116
|
}
|
|
3318
|
-
if (i >=
|
|
4117
|
+
if (i >= segments.length) {
|
|
3319
4118
|
out.push(chunk);
|
|
3320
4119
|
break;
|
|
3321
4120
|
}
|
|
@@ -3323,7 +4122,7 @@ function wrapVisible(text, width) {
|
|
|
3323
4122
|
out.push(chunkAtLastSpace);
|
|
3324
4123
|
i = lastSpaceI + 1;
|
|
3325
4124
|
} else if (chunk.length === 0) {
|
|
3326
|
-
out.push(
|
|
4125
|
+
out.push(segments[i].text);
|
|
3327
4126
|
i += 1;
|
|
3328
4127
|
} else {
|
|
3329
4128
|
out.push(chunk);
|
|
@@ -3331,34 +4130,43 @@ function wrapVisible(text, width) {
|
|
|
3331
4130
|
}
|
|
3332
4131
|
return out;
|
|
3333
4132
|
}
|
|
3334
|
-
function
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
}
|
|
3339
|
-
if (p === home) {
|
|
3340
|
-
return "~";
|
|
3341
|
-
}
|
|
3342
|
-
if (p.startsWith(home + "/")) {
|
|
3343
|
-
return "~" + p.slice(home.length);
|
|
4133
|
+
function graphemeSegments(text) {
|
|
4134
|
+
const out = [];
|
|
4135
|
+
for (const { segment } of SEGMENTER.segment(text)) {
|
|
4136
|
+
out.push({ text: segment, width: stringWidth(segment) });
|
|
3344
4137
|
}
|
|
3345
|
-
return
|
|
4138
|
+
return out;
|
|
3346
4139
|
}
|
|
3347
|
-
function truncate(text, max) {
|
|
4140
|
+
function truncate(text, max, opts = {}) {
|
|
3348
4141
|
if (max <= 0) {
|
|
3349
4142
|
return "";
|
|
3350
4143
|
}
|
|
3351
|
-
|
|
4144
|
+
const stripMarkup = opts.stripMarkup === true && hasTkMarkup(text);
|
|
4145
|
+
if (!stripMarkup && text.length <= max && !NON_ASCII.test(text)) {
|
|
3352
4146
|
return text;
|
|
3353
4147
|
}
|
|
3354
|
-
|
|
4148
|
+
if (!stripMarkup) {
|
|
4149
|
+
const visible2 = stringWidth(text);
|
|
4150
|
+
if (visible2 <= max) {
|
|
4151
|
+
return text;
|
|
4152
|
+
}
|
|
4153
|
+
if (max <= 1) {
|
|
4154
|
+
return takeByWidth(text, max);
|
|
4155
|
+
}
|
|
4156
|
+
return takeByWidth(text, max - 1) + "\u2026";
|
|
4157
|
+
}
|
|
4158
|
+
const segments = [...segmentForWidth(text)];
|
|
4159
|
+
let visible = 0;
|
|
4160
|
+
for (const s of segments) {
|
|
4161
|
+
visible += s.width;
|
|
4162
|
+
}
|
|
3355
4163
|
if (visible <= max) {
|
|
3356
4164
|
return text;
|
|
3357
4165
|
}
|
|
3358
4166
|
if (max <= 1) {
|
|
3359
|
-
return
|
|
4167
|
+
return takeFromSegments(segments, max);
|
|
3360
4168
|
}
|
|
3361
|
-
return
|
|
4169
|
+
return takeFromSegments(segments, max - 1) + "\u2026";
|
|
3362
4170
|
}
|
|
3363
4171
|
function takeByWidth(text, budget) {
|
|
3364
4172
|
if (budget <= 0) {
|
|
@@ -3376,6 +4184,21 @@ function takeByWidth(text, budget) {
|
|
|
3376
4184
|
}
|
|
3377
4185
|
return out;
|
|
3378
4186
|
}
|
|
4187
|
+
function takeFromSegments(segments, budget) {
|
|
4188
|
+
if (budget <= 0) {
|
|
4189
|
+
return "";
|
|
4190
|
+
}
|
|
4191
|
+
let out = "";
|
|
4192
|
+
let used = 0;
|
|
4193
|
+
for (const s of segments) {
|
|
4194
|
+
if (used + s.width > budget) {
|
|
4195
|
+
break;
|
|
4196
|
+
}
|
|
4197
|
+
out += s.text;
|
|
4198
|
+
used += s.width;
|
|
4199
|
+
}
|
|
4200
|
+
return out;
|
|
4201
|
+
}
|
|
3379
4202
|
function firstLine2(text) {
|
|
3380
4203
|
const idx = text.indexOf("\n");
|
|
3381
4204
|
return idx === -1 ? text : `${text.slice(0, idx)} \u21B5`;
|
|
@@ -3472,6 +4295,10 @@ function mapKeyName(name) {
|
|
|
3472
4295
|
return "ctrl-o";
|
|
3473
4296
|
case "CTRL_P":
|
|
3474
4297
|
return "ctrl-p";
|
|
4298
|
+
case "CTRL_R":
|
|
4299
|
+
return "ctrl-r";
|
|
4300
|
+
case "CTRL_S":
|
|
4301
|
+
return "ctrl-s";
|
|
3475
4302
|
case "CTRL_U":
|
|
3476
4303
|
return "ctrl-u";
|
|
3477
4304
|
case "CTRL_W":
|
|
@@ -3484,11 +4311,12 @@ function mapKeyName(name) {
|
|
|
3484
4311
|
return null;
|
|
3485
4312
|
}
|
|
3486
4313
|
}
|
|
3487
|
-
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, shortId;
|
|
4314
|
+
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
3488
4315
|
var init_screen = __esm({
|
|
3489
4316
|
"src/tui/screen.ts"() {
|
|
3490
4317
|
"use strict";
|
|
3491
4318
|
init_agent_display();
|
|
4319
|
+
init_paths();
|
|
3492
4320
|
init_session();
|
|
3493
4321
|
HEADER_ROWS = 2;
|
|
3494
4322
|
BANNER_ROWS = 1;
|
|
@@ -3532,6 +4360,12 @@ var init_screen = __esm({
|
|
|
3532
4360
|
lineIds = /* @__PURE__ */ new WeakMap();
|
|
3533
4361
|
wrapCache = /* @__PURE__ */ new Map();
|
|
3534
4362
|
wrapCacheWidth = 0;
|
|
4363
|
+
// For each wrapped chunk (produced by wrapOne), record the source
|
|
4364
|
+
// line's id and the col offset where this chunk starts in the source
|
|
4365
|
+
// body. Used by the active-match highlight in scrollback search to
|
|
4366
|
+
// map currentMatch (sourceLineId, sourceCol) onto the wrapped chunk
|
|
4367
|
+
// that owns it without scanning the wrap cache.
|
|
4368
|
+
wrapOrigin = /* @__PURE__ */ new WeakMap();
|
|
3535
4369
|
// Per-row signature of what was painted to each terminal row on the
|
|
3536
4370
|
// previous repaint. drawX methods funnel through paintRow(), which
|
|
3537
4371
|
// skips the moveTo+eraseLineAfter+write sequence when the new
|
|
@@ -3549,10 +4383,30 @@ var init_screen = __esm({
|
|
|
3549
4383
|
// above the bottom. Mouse wheel and PgUp/PgDn adjust this; new content
|
|
3550
4384
|
// pushes the view down naturally when at 0.
|
|
3551
4385
|
scrollOffset = 0;
|
|
4386
|
+
// Scrollback search state. While active the prompt area is taken over
|
|
4387
|
+
// by a single-row search input (drawSearchPrompt) and matches in the
|
|
4388
|
+
// visible scrollback are rendered with a background-highlight style.
|
|
4389
|
+
// baselineScroll captures the scrollOffset at the moment the user
|
|
4390
|
+
// engaged search so cancel can restore the view.
|
|
4391
|
+
scrollbackSearch = null;
|
|
4392
|
+
// Lowercased search term used by drawScrollback to drive per-row
|
|
4393
|
+
// highlight rendering. Mirrors scrollbackSearch?.term but cached as a
|
|
4394
|
+
// separate field so the per-row signature can include it cheaply.
|
|
4395
|
+
scrollbackHighlight = null;
|
|
4396
|
+
// Right-side banner slot. Three sources, in priority order:
|
|
4397
|
+
// 1. Active scrollback search term (auto, from this.scrollbackSearch)
|
|
4398
|
+
// 2. External search indicator pushed by the app while prompt-
|
|
4399
|
+
// history reverse-search is active (gives that mode visible
|
|
4400
|
+
// feedback for its otherwise-hidden query)
|
|
4401
|
+
// 3. Transient notification set via notify(), auto-cleared after
|
|
4402
|
+
// durationMs
|
|
4403
|
+
bannerNotification = null;
|
|
4404
|
+
bannerNotificationTimer = null;
|
|
4405
|
+
bannerSearchIndicator = null;
|
|
3552
4406
|
banner = {
|
|
3553
4407
|
status: "ready",
|
|
3554
4408
|
planMode: false,
|
|
3555
|
-
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D
|
|
4409
|
+
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
3556
4410
|
queued: 0
|
|
3557
4411
|
};
|
|
3558
4412
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -3615,6 +4469,10 @@ var init_screen = __esm({
|
|
|
3615
4469
|
return;
|
|
3616
4470
|
}
|
|
3617
4471
|
this.started = false;
|
|
4472
|
+
if (this.bannerNotificationTimer) {
|
|
4473
|
+
clearTimeout(this.bannerNotificationTimer);
|
|
4474
|
+
this.bannerNotificationTimer = null;
|
|
4475
|
+
}
|
|
3618
4476
|
this.uninstallBracketedPaste();
|
|
3619
4477
|
this.term.off("key", this.keyHandler);
|
|
3620
4478
|
if (this.mouseEnabled) {
|
|
@@ -3695,7 +4553,7 @@ var init_screen = __esm({
|
|
|
3695
4553
|
this.streamingActive = false;
|
|
3696
4554
|
this.lines.push(...lines);
|
|
3697
4555
|
this.trackLines(lines);
|
|
3698
|
-
this.
|
|
4556
|
+
this.adjustScrollForRowChange(this.wrappedRowsOfMany(lines));
|
|
3699
4557
|
this.trimScrollback();
|
|
3700
4558
|
this.scheduleRepaint();
|
|
3701
4559
|
}
|
|
@@ -3703,19 +4561,40 @@ var init_screen = __esm({
|
|
|
3703
4561
|
this.streamingActive = false;
|
|
3704
4562
|
this.lines.push(line);
|
|
3705
4563
|
this.trackLine(line);
|
|
3706
|
-
this.
|
|
4564
|
+
this.adjustScrollForRowChange(this.wrappedRowsOf(line));
|
|
3707
4565
|
this.trimScrollback();
|
|
3708
4566
|
this.scheduleRepaint();
|
|
3709
4567
|
}
|
|
3710
4568
|
// When scrolled away from the bottom, shift scrollOffset to keep the
|
|
3711
4569
|
// user's visible window anchored on the same content as the lines
|
|
3712
|
-
// array grows.
|
|
3713
|
-
//
|
|
3714
|
-
|
|
4570
|
+
// array grows. `delta` is measured in WRAPPED ROWS — the same unit
|
|
4571
|
+
// scrollOffset uses — so a single logical line that wraps to N rows
|
|
4572
|
+
// contributes N, not 1. Counting logical lines here was the original
|
|
4573
|
+
// bug: any wrapped append would slide the view up by N−1 rows.
|
|
4574
|
+
adjustScrollForRowChange(delta) {
|
|
3715
4575
|
if (this.scrollOffset > 0 && delta !== 0) {
|
|
3716
4576
|
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
3717
4577
|
}
|
|
3718
4578
|
}
|
|
4579
|
+
// Wrapped-row count for a single line at the current terminal width.
|
|
4580
|
+
// Reuses the wrap cache, and synchronises the cache's width with the
|
|
4581
|
+
// current width so a resize that hasn't yet been picked up by
|
|
4582
|
+
// drawScrollback can't return stale counts during an insert.
|
|
4583
|
+
wrappedRowsOf(line) {
|
|
4584
|
+
const w = this.term.width;
|
|
4585
|
+
if (this.wrapCacheWidth !== w) {
|
|
4586
|
+
this.wrapCache.clear();
|
|
4587
|
+
this.wrapCacheWidth = w;
|
|
4588
|
+
}
|
|
4589
|
+
return this.wrapOne(line, w).length;
|
|
4590
|
+
}
|
|
4591
|
+
wrappedRowsOfMany(lines) {
|
|
4592
|
+
let n = 0;
|
|
4593
|
+
for (const line of lines) {
|
|
4594
|
+
n += this.wrappedRowsOf(line);
|
|
4595
|
+
}
|
|
4596
|
+
return n;
|
|
4597
|
+
}
|
|
3719
4598
|
trackLine(line) {
|
|
3720
4599
|
this.lineIds.set(line, this.nextLineId++);
|
|
3721
4600
|
}
|
|
@@ -3765,12 +4644,14 @@ var init_screen = __esm({
|
|
|
3765
4644
|
}
|
|
3766
4645
|
const existing = this.keyedBlocks.get(key);
|
|
3767
4646
|
let touchesEnd = false;
|
|
3768
|
-
let
|
|
4647
|
+
let rowDelta = 0;
|
|
3769
4648
|
if (existing) {
|
|
3770
4649
|
const oldEnd = existing.start + existing.count;
|
|
3771
4650
|
touchesEnd = oldEnd >= this.lines.length;
|
|
4651
|
+
const oldRows = this.wrappedRowsOfMany(
|
|
4652
|
+
this.lines.slice(existing.start, oldEnd)
|
|
4653
|
+
);
|
|
3772
4654
|
const delta = newLines.length - existing.count;
|
|
3773
|
-
scrollDelta = delta;
|
|
3774
4655
|
const removed = this.lines.splice(
|
|
3775
4656
|
existing.start,
|
|
3776
4657
|
existing.count,
|
|
@@ -3788,20 +4669,21 @@ var init_screen = __esm({
|
|
|
3788
4669
|
}
|
|
3789
4670
|
}
|
|
3790
4671
|
}
|
|
4672
|
+
rowDelta = this.wrappedRowsOfMany(newLines) - oldRows;
|
|
3791
4673
|
} else {
|
|
3792
4674
|
touchesEnd = true;
|
|
3793
|
-
scrollDelta = newLines.length;
|
|
3794
4675
|
this.keyedBlocks.set(key, {
|
|
3795
4676
|
start: this.lines.length,
|
|
3796
4677
|
count: newLines.length
|
|
3797
4678
|
});
|
|
3798
4679
|
this.lines.push(...newLines);
|
|
3799
4680
|
this.trackLines(newLines);
|
|
4681
|
+
rowDelta = this.wrappedRowsOfMany(newLines);
|
|
3800
4682
|
}
|
|
3801
4683
|
if (touchesEnd) {
|
|
3802
4684
|
this.streamingActive = false;
|
|
3803
4685
|
}
|
|
3804
|
-
this.
|
|
4686
|
+
this.adjustScrollForRowChange(rowDelta);
|
|
3805
4687
|
this.trimScrollback();
|
|
3806
4688
|
this.scheduleRepaint();
|
|
3807
4689
|
}
|
|
@@ -3815,12 +4697,14 @@ var init_screen = __esm({
|
|
|
3815
4697
|
}
|
|
3816
4698
|
const fragments = text.split("\n");
|
|
3817
4699
|
const [first, ...rest] = fragments;
|
|
3818
|
-
let
|
|
4700
|
+
let rowDelta = 0;
|
|
3819
4701
|
if (this.streamingActive && this.lines.length > 0) {
|
|
3820
4702
|
const last = this.lines[this.lines.length - 1];
|
|
3821
4703
|
if (last) {
|
|
4704
|
+
const before = this.wrappedRowsOf(last);
|
|
3822
4705
|
this.forgetLine(last);
|
|
3823
4706
|
last.body += first ?? "";
|
|
4707
|
+
rowDelta += this.wrappedRowsOf(last) - before;
|
|
3824
4708
|
}
|
|
3825
4709
|
} else {
|
|
3826
4710
|
if (this.lines.length > 0) {
|
|
@@ -3830,7 +4714,7 @@ var init_screen = __esm({
|
|
|
3830
4714
|
const sep = { body: "" };
|
|
3831
4715
|
this.lines.push(sep);
|
|
3832
4716
|
this.trackLine(sep);
|
|
3833
|
-
|
|
4717
|
+
rowDelta += this.wrappedRowsOf(sep);
|
|
3834
4718
|
}
|
|
3835
4719
|
}
|
|
3836
4720
|
const initial = {
|
|
@@ -3843,7 +4727,7 @@ var init_screen = __esm({
|
|
|
3843
4727
|
}
|
|
3844
4728
|
this.lines.push(initial);
|
|
3845
4729
|
this.trackLine(initial);
|
|
3846
|
-
|
|
4730
|
+
rowDelta += this.wrappedRowsOf(initial);
|
|
3847
4731
|
}
|
|
3848
4732
|
const continuationPrefix = " ".repeat(prefix.length);
|
|
3849
4733
|
for (const piece of rest) {
|
|
@@ -3854,10 +4738,10 @@ var init_screen = __esm({
|
|
|
3854
4738
|
};
|
|
3855
4739
|
this.lines.push(cont);
|
|
3856
4740
|
this.trackLine(cont);
|
|
3857
|
-
|
|
4741
|
+
rowDelta += this.wrappedRowsOf(cont);
|
|
3858
4742
|
}
|
|
3859
4743
|
this.streamingActive = true;
|
|
3860
|
-
this.
|
|
4744
|
+
this.adjustScrollForRowChange(rowDelta);
|
|
3861
4745
|
this.trimScrollback();
|
|
3862
4746
|
this.scheduleRepaint();
|
|
3863
4747
|
}
|
|
@@ -3889,6 +4773,58 @@ var init_screen = __esm({
|
|
|
3889
4773
|
this.drawBanner();
|
|
3890
4774
|
this.placeCursor();
|
|
3891
4775
|
}
|
|
4776
|
+
// Transient right-side banner message. Cleared automatically after
|
|
4777
|
+
// durationMs (default 4s). Each call resets the timer, so rapid
|
|
4778
|
+
// successive notifications coalesce on the latest text. Active
|
|
4779
|
+
// scrollback / prompt-history search indicators take priority over
|
|
4780
|
+
// notifications, so a notification queued during search is held
|
|
4781
|
+
// behind it and visible once search exits — unless its timer fires
|
|
4782
|
+
// first, in which case it's dropped.
|
|
4783
|
+
notify(text, durationMs = 4e3) {
|
|
4784
|
+
if (this.bannerNotificationTimer) {
|
|
4785
|
+
clearTimeout(this.bannerNotificationTimer);
|
|
4786
|
+
}
|
|
4787
|
+
this.bannerNotification = text;
|
|
4788
|
+
this.bannerNotificationTimer = setTimeout(() => {
|
|
4789
|
+
this.bannerNotification = null;
|
|
4790
|
+
this.bannerNotificationTimer = null;
|
|
4791
|
+
this.drawBanner();
|
|
4792
|
+
this.placeCursor();
|
|
4793
|
+
}, durationMs);
|
|
4794
|
+
this.drawBanner();
|
|
4795
|
+
this.placeCursor();
|
|
4796
|
+
}
|
|
4797
|
+
// Pushed by the app each onKey tick to reflect prompt-history
|
|
4798
|
+
// reverse-search state in the banner — the only place that mode's
|
|
4799
|
+
// query is visible. Pass null when not searching.
|
|
4800
|
+
setBannerSearchIndicator(text) {
|
|
4801
|
+
if (this.bannerSearchIndicator === text) {
|
|
4802
|
+
return;
|
|
4803
|
+
}
|
|
4804
|
+
this.bannerSearchIndicator = text;
|
|
4805
|
+
this.drawBanner();
|
|
4806
|
+
this.placeCursor();
|
|
4807
|
+
}
|
|
4808
|
+
// Computes what (if anything) the right-side banner slot should show
|
|
4809
|
+
// this paint. Priority: scrollback search term > prompt-history
|
|
4810
|
+
// indicator > notification. Scrollback gets a "N/M" counter suffix
|
|
4811
|
+
// since the user can't see which match they're on from the highlight
|
|
4812
|
+
// alone; prompt-history's match is visible in the buffer, so no
|
|
4813
|
+
// counter needed there.
|
|
4814
|
+
bannerRightContent() {
|
|
4815
|
+
if (this.scrollbackSearch !== null) {
|
|
4816
|
+
const sb = this.scrollbackSearch;
|
|
4817
|
+
const counter = sb.matches.length > 0 ? ` ${sb.matchIndex + 1}/${sb.matches.length}` : sb.term.length === 0 ? "" : " 0/0";
|
|
4818
|
+
return { text: `\u{1F50D} ${sb.term}${counter}`, kind: "search" };
|
|
4819
|
+
}
|
|
4820
|
+
if (this.bannerSearchIndicator !== null) {
|
|
4821
|
+
return { text: `\u{1F50D} ${this.bannerSearchIndicator}`, kind: "search" };
|
|
4822
|
+
}
|
|
4823
|
+
if (this.bannerNotification !== null) {
|
|
4824
|
+
return { text: this.bannerNotification, kind: "notify" };
|
|
4825
|
+
}
|
|
4826
|
+
return null;
|
|
4827
|
+
}
|
|
3892
4828
|
clearScrollback() {
|
|
3893
4829
|
this.lines = [];
|
|
3894
4830
|
this.keyedBlocks.clear();
|
|
@@ -3916,6 +4852,9 @@ var init_screen = __esm({
|
|
|
3916
4852
|
return;
|
|
3917
4853
|
}
|
|
3918
4854
|
const touchesEnd = existing.start + existing.count >= this.lines.length;
|
|
4855
|
+
const removedRows = this.wrappedRowsOfMany(
|
|
4856
|
+
this.lines.slice(existing.start, existing.start + existing.count)
|
|
4857
|
+
);
|
|
3919
4858
|
const removed = this.lines.splice(existing.start, existing.count);
|
|
3920
4859
|
for (const line of removed) {
|
|
3921
4860
|
this.forgetLine(line);
|
|
@@ -3929,7 +4868,7 @@ var init_screen = __esm({
|
|
|
3929
4868
|
if (touchesEnd) {
|
|
3930
4869
|
this.streamingActive = false;
|
|
3931
4870
|
}
|
|
3932
|
-
this.
|
|
4871
|
+
this.adjustScrollForRowChange(-removedRows);
|
|
3933
4872
|
this.scheduleRepaint();
|
|
3934
4873
|
}
|
|
3935
4874
|
redraw() {
|
|
@@ -4014,7 +4953,7 @@ var init_screen = __esm({
|
|
|
4014
4953
|
this.lines.push(sep);
|
|
4015
4954
|
this.trackLine(sep);
|
|
4016
4955
|
this.streamingActive = false;
|
|
4017
|
-
this.
|
|
4956
|
+
this.adjustScrollForRowChange(this.wrappedRowsOf(sep));
|
|
4018
4957
|
this.trimScrollback();
|
|
4019
4958
|
this.scheduleRepaint();
|
|
4020
4959
|
}
|
|
@@ -4070,6 +5009,9 @@ var init_screen = __esm({
|
|
|
4070
5009
|
if (delta === 0) {
|
|
4071
5010
|
return;
|
|
4072
5011
|
}
|
|
5012
|
+
if (this.scrollbackSearch !== null) {
|
|
5013
|
+
this.acceptScrollbackSearch();
|
|
5014
|
+
}
|
|
4073
5015
|
const max = this.maxScrollOffset();
|
|
4074
5016
|
const next = Math.min(max, Math.max(0, this.scrollOffset + delta));
|
|
4075
5017
|
if (next === this.scrollOffset) {
|
|
@@ -4079,6 +5021,9 @@ var init_screen = __esm({
|
|
|
4079
5021
|
this.repaint();
|
|
4080
5022
|
}
|
|
4081
5023
|
scrollToBottom() {
|
|
5024
|
+
if (this.scrollbackSearch !== null) {
|
|
5025
|
+
this.acceptScrollbackSearch();
|
|
5026
|
+
}
|
|
4082
5027
|
if (this.scrollOffset === 0) {
|
|
4083
5028
|
return;
|
|
4084
5029
|
}
|
|
@@ -4086,6 +5031,9 @@ var init_screen = __esm({
|
|
|
4086
5031
|
this.repaint();
|
|
4087
5032
|
}
|
|
4088
5033
|
scrollToTop() {
|
|
5034
|
+
if (this.scrollbackSearch !== null) {
|
|
5035
|
+
this.acceptScrollbackSearch();
|
|
5036
|
+
}
|
|
4089
5037
|
const max = this.maxScrollOffset();
|
|
4090
5038
|
if (this.scrollOffset === max) {
|
|
4091
5039
|
return;
|
|
@@ -4093,6 +5041,221 @@ var init_screen = __esm({
|
|
|
4093
5041
|
this.scrollOffset = max;
|
|
4094
5042
|
this.repaint();
|
|
4095
5043
|
}
|
|
5044
|
+
// True iff the user is scrolled above the live tail — gates the
|
|
5045
|
+
// app-level decision of whether ^r engages scrollback search vs.
|
|
5046
|
+
// prompt-history search.
|
|
5047
|
+
isScrolledBack() {
|
|
5048
|
+
return this.scrollOffset > 0;
|
|
5049
|
+
}
|
|
5050
|
+
// True iff a scrollback search is currently active. Used by the app
|
|
5051
|
+
// to decide whether to keep routing keys into search vs. the prompt
|
|
5052
|
+
// dispatcher.
|
|
5053
|
+
isScrollbackSearchActive() {
|
|
5054
|
+
return this.scrollbackSearch !== null;
|
|
5055
|
+
}
|
|
5056
|
+
// Engage scrollback reverse-search. Captures the current scroll
|
|
5057
|
+
// position so cancel can restore it, and seeds an empty search term
|
|
5058
|
+
// (the prompt row renders the search input immediately so the user
|
|
5059
|
+
// sees the entry). Idempotent: no-op when already active.
|
|
5060
|
+
enterScrollbackSearch() {
|
|
5061
|
+
if (this.scrollbackSearch !== null) {
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
this.scrollbackSearch = {
|
|
5065
|
+
term: "",
|
|
5066
|
+
matchIndex: 0,
|
|
5067
|
+
matches: [],
|
|
5068
|
+
baselineScroll: this.scrollOffset
|
|
5069
|
+
};
|
|
5070
|
+
this.scrollbackHighlight = null;
|
|
5071
|
+
this.repaint();
|
|
5072
|
+
}
|
|
5073
|
+
// Update the search term and recompute matches. Walks `lines` from
|
|
5074
|
+
// the tail (newest) toward the head (oldest), pushing every case-
|
|
5075
|
+
// insensitive substring hit. Snaps the viewport to the newest match
|
|
5076
|
+
// when found. Called per keystroke; sub-millisecond on typical
|
|
5077
|
+
// scrollback sizes.
|
|
5078
|
+
updateScrollbackSearchTerm(term) {
|
|
5079
|
+
if (this.scrollbackSearch === null) {
|
|
5080
|
+
return;
|
|
5081
|
+
}
|
|
5082
|
+
const lowered = term.toLowerCase();
|
|
5083
|
+
const matches = [];
|
|
5084
|
+
if (lowered.length > 0) {
|
|
5085
|
+
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
5086
|
+
const line = this.lines[i];
|
|
5087
|
+
if (!line || line.body.length === 0) {
|
|
5088
|
+
continue;
|
|
5089
|
+
}
|
|
5090
|
+
if (line.ansi) {
|
|
5091
|
+
continue;
|
|
5092
|
+
}
|
|
5093
|
+
const hay = line.body.toLowerCase();
|
|
5094
|
+
const lineCols = [];
|
|
5095
|
+
let pos = 0;
|
|
5096
|
+
while (pos < hay.length) {
|
|
5097
|
+
const found = hay.indexOf(lowered, pos);
|
|
5098
|
+
if (found === -1) {
|
|
5099
|
+
break;
|
|
5100
|
+
}
|
|
5101
|
+
lineCols.push(found);
|
|
5102
|
+
pos = found + lowered.length;
|
|
5103
|
+
}
|
|
5104
|
+
for (let j = lineCols.length - 1; j >= 0; j--) {
|
|
5105
|
+
matches.push({ lineIdx: i, col: lineCols[j] });
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
this.scrollbackSearch.term = term;
|
|
5110
|
+
this.scrollbackSearch.matches = matches;
|
|
5111
|
+
this.scrollbackSearch.matchIndex = 0;
|
|
5112
|
+
this.scrollbackHighlight = lowered.length > 0 ? lowered : null;
|
|
5113
|
+
if (matches.length > 0) {
|
|
5114
|
+
this.scrollToMatch(matches[0]);
|
|
5115
|
+
}
|
|
5116
|
+
this.repaint();
|
|
5117
|
+
}
|
|
5118
|
+
// Advance to the next-older match (called for repeated ^r). Stops at
|
|
5119
|
+
// the oldest match (does not wrap). No-op when there are no matches
|
|
5120
|
+
// or search is inactive.
|
|
5121
|
+
advanceScrollbackSearch() {
|
|
5122
|
+
if (this.scrollbackSearch === null || this.scrollbackSearch.matches.length === 0) {
|
|
5123
|
+
return;
|
|
5124
|
+
}
|
|
5125
|
+
const nextIdx = Math.min(
|
|
5126
|
+
this.scrollbackSearch.matches.length - 1,
|
|
5127
|
+
this.scrollbackSearch.matchIndex + 1
|
|
5128
|
+
);
|
|
5129
|
+
if (nextIdx === this.scrollbackSearch.matchIndex) {
|
|
5130
|
+
return;
|
|
5131
|
+
}
|
|
5132
|
+
this.scrollbackSearch.matchIndex = nextIdx;
|
|
5133
|
+
this.scrollToMatch(this.scrollbackSearch.matches[nextIdx]);
|
|
5134
|
+
this.repaint();
|
|
5135
|
+
}
|
|
5136
|
+
// Retreat to the previous (newer) match — ^s forward-search. Stops
|
|
5137
|
+
// at the newest match (no wrap).
|
|
5138
|
+
retreatScrollbackSearch() {
|
|
5139
|
+
if (this.scrollbackSearch === null || this.scrollbackSearch.matches.length === 0) {
|
|
5140
|
+
return;
|
|
5141
|
+
}
|
|
5142
|
+
if (this.scrollbackSearch.matchIndex === 0) {
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
this.scrollbackSearch.matchIndex -= 1;
|
|
5146
|
+
this.scrollToMatch(this.scrollbackSearch.matches[this.scrollbackSearch.matchIndex]);
|
|
5147
|
+
this.repaint();
|
|
5148
|
+
}
|
|
5149
|
+
// Exit search keeping the viewport at the current match. Highlight is
|
|
5150
|
+
// cleared so subsequent scrollback content reads normally.
|
|
5151
|
+
acceptScrollbackSearch() {
|
|
5152
|
+
if (this.scrollbackSearch === null) {
|
|
5153
|
+
return;
|
|
5154
|
+
}
|
|
5155
|
+
this.scrollbackSearch = null;
|
|
5156
|
+
this.scrollbackHighlight = null;
|
|
5157
|
+
this.repaint();
|
|
5158
|
+
}
|
|
5159
|
+
// Exit search and restore the viewport to where the user was when
|
|
5160
|
+
// they engaged search.
|
|
5161
|
+
cancelScrollbackSearch() {
|
|
5162
|
+
if (this.scrollbackSearch === null) {
|
|
5163
|
+
return;
|
|
5164
|
+
}
|
|
5165
|
+
const baseline = this.scrollbackSearch.baselineScroll;
|
|
5166
|
+
this.scrollbackSearch = null;
|
|
5167
|
+
this.scrollbackHighlight = null;
|
|
5168
|
+
this.scrollOffset = baseline;
|
|
5169
|
+
this.repaint();
|
|
5170
|
+
}
|
|
5171
|
+
scrollbackSearchTerm() {
|
|
5172
|
+
return this.scrollbackSearch?.term ?? "";
|
|
5173
|
+
}
|
|
5174
|
+
// Source-line identity + col + term length for whichever match is
|
|
5175
|
+
// currently selected (advanced via ^r / retreated via ^s). Used by
|
|
5176
|
+
// drawScrollback to give the current match a distinct highlight
|
|
5177
|
+
// style without disturbing the bulk-highlight on the other matches.
|
|
5178
|
+
currentMatchInfo() {
|
|
5179
|
+
if (this.scrollbackSearch === null || this.scrollbackSearch.matches.length === 0) {
|
|
5180
|
+
return null;
|
|
5181
|
+
}
|
|
5182
|
+
const match = this.scrollbackSearch.matches[this.scrollbackSearch.matchIndex];
|
|
5183
|
+
if (!match) {
|
|
5184
|
+
return null;
|
|
5185
|
+
}
|
|
5186
|
+
const sourceLine = this.lines[match.lineIdx];
|
|
5187
|
+
if (!sourceLine) {
|
|
5188
|
+
return null;
|
|
5189
|
+
}
|
|
5190
|
+
const lineId = this.lineIds.get(sourceLine);
|
|
5191
|
+
if (lineId === void 0) {
|
|
5192
|
+
return null;
|
|
5193
|
+
}
|
|
5194
|
+
return {
|
|
5195
|
+
lineId,
|
|
5196
|
+
col: match.col,
|
|
5197
|
+
length: this.scrollbackSearch.term.length
|
|
5198
|
+
};
|
|
5199
|
+
}
|
|
5200
|
+
// If `line` is the wrapped chunk that contains the active match,
|
|
5201
|
+
// returns the col within the chunk's body where the match starts;
|
|
5202
|
+
// otherwise null. The chunk's source identity comes from
|
|
5203
|
+
// this.wrapOrigin which wrapOne populates for every wrapped chunk.
|
|
5204
|
+
activeMatchCol(line, info) {
|
|
5205
|
+
if (!line || info === null) {
|
|
5206
|
+
return null;
|
|
5207
|
+
}
|
|
5208
|
+
const origin = this.wrapOrigin.get(line);
|
|
5209
|
+
if (!origin || origin.sourceLineId !== info.lineId) {
|
|
5210
|
+
return null;
|
|
5211
|
+
}
|
|
5212
|
+
const colInChunk = info.col - origin.sourceColOffset;
|
|
5213
|
+
if (colInChunk < 0 || colInChunk >= line.body.length) {
|
|
5214
|
+
return null;
|
|
5215
|
+
}
|
|
5216
|
+
return colInChunk;
|
|
5217
|
+
}
|
|
5218
|
+
// Position scrollOffset so the wrapped row containing the given
|
|
5219
|
+
// (lineIdx, col) lands on a visible row of the scrollback viewport.
|
|
5220
|
+
// Walks wrapTail to count wrapped rows between the target line and
|
|
5221
|
+
// the tail.
|
|
5222
|
+
scrollToMatch(match) {
|
|
5223
|
+
const w = this.term.width;
|
|
5224
|
+
const visibleRows = this.scrollbackVisibleRows();
|
|
5225
|
+
if (visibleRows <= 0) {
|
|
5226
|
+
return;
|
|
5227
|
+
}
|
|
5228
|
+
let rowsBelowMatchLine = 0;
|
|
5229
|
+
for (let i = this.lines.length - 1; i > match.lineIdx; i--) {
|
|
5230
|
+
const line = this.lines[i];
|
|
5231
|
+
if (!line) {
|
|
5232
|
+
continue;
|
|
5233
|
+
}
|
|
5234
|
+
rowsBelowMatchLine += this.wrapOne(line, w).length;
|
|
5235
|
+
}
|
|
5236
|
+
const matchLine = this.lines[match.lineIdx];
|
|
5237
|
+
let rowsWithinMatchLine = 0;
|
|
5238
|
+
if (matchLine) {
|
|
5239
|
+
const wrapped = this.wrapOne(matchLine, w);
|
|
5240
|
+
let consumed = 0;
|
|
5241
|
+
for (let r = 0; r < wrapped.length; r++) {
|
|
5242
|
+
const piece = wrapped[r];
|
|
5243
|
+
if (!piece) {
|
|
5244
|
+
continue;
|
|
5245
|
+
}
|
|
5246
|
+
const bodyLen = piece.body.length;
|
|
5247
|
+
if (match.col < consumed + bodyLen) {
|
|
5248
|
+
rowsWithinMatchLine = wrapped.length - 1 - r;
|
|
5249
|
+
break;
|
|
5250
|
+
}
|
|
5251
|
+
consumed += bodyLen;
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
const target = rowsBelowMatchLine + rowsWithinMatchLine;
|
|
5255
|
+
const desired = Math.max(0, target - Math.floor(visibleRows / 2));
|
|
5256
|
+
const max = this.maxScrollOffset();
|
|
5257
|
+
this.scrollOffset = Math.min(max, desired);
|
|
5258
|
+
}
|
|
4096
5259
|
scrollPageSize() {
|
|
4097
5260
|
return Math.max(1, this.scrollbackVisibleRows() - 2);
|
|
4098
5261
|
}
|
|
@@ -4215,8 +5378,8 @@ var init_screen = __esm({
|
|
|
4215
5378
|
}
|
|
4216
5379
|
if (usage) {
|
|
4217
5380
|
const col = Math.max(1, w - usage.length + 1);
|
|
4218
|
-
this.term.moveTo(col, 1);
|
|
4219
|
-
this.term.dim(usage);
|
|
5381
|
+
this.term.moveTo(col, 1).eraseLineAfter();
|
|
5382
|
+
this.term.dim.noFormat(usage);
|
|
4220
5383
|
}
|
|
4221
5384
|
});
|
|
4222
5385
|
}
|
|
@@ -4247,14 +5410,23 @@ var init_screen = __esm({
|
|
|
4247
5410
|
const start = Math.max(0, end - visibleRows);
|
|
4248
5411
|
const slice = wrapped.slice(start, end);
|
|
4249
5412
|
const padTop = Math.max(0, visibleRows - slice.length);
|
|
5413
|
+
const matchInfo = this.currentMatchInfo();
|
|
5414
|
+
const activeLength = matchInfo?.length ?? 0;
|
|
4250
5415
|
for (let i = 0; i < visibleRows; i++) {
|
|
4251
5416
|
const row = top + i;
|
|
4252
5417
|
const sliceIdx = i - padTop;
|
|
4253
5418
|
const line = sliceIdx >= 0 ? slice[sliceIdx] : void 0;
|
|
4254
|
-
const
|
|
5419
|
+
const activeCol = this.activeMatchCol(line, matchInfo);
|
|
5420
|
+
const sig = formattedLineSig(
|
|
5421
|
+
"sb",
|
|
5422
|
+
w,
|
|
5423
|
+
line,
|
|
5424
|
+
this.scrollbackHighlight,
|
|
5425
|
+
activeCol
|
|
5426
|
+
);
|
|
4255
5427
|
this.paintRow(row, sig, () => {
|
|
4256
5428
|
if (line) {
|
|
4257
|
-
this.writeFormattedLine(line, w);
|
|
5429
|
+
this.writeFormattedLine(line, w, activeCol, activeLength);
|
|
4258
5430
|
}
|
|
4259
5431
|
});
|
|
4260
5432
|
}
|
|
@@ -4454,7 +5626,9 @@ var init_screen = __esm({
|
|
|
4454
5626
|
const row = this.term.height;
|
|
4455
5627
|
const w = this.term.width;
|
|
4456
5628
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
4457
|
-
const
|
|
5629
|
+
const right = this.bannerRightContent();
|
|
5630
|
+
const rightSig = right ? `${right.kind}|${right.text}` : "";
|
|
5631
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.planMode ? "1" : "0"}|${this.banner.hint}|` + rightSig;
|
|
4458
5632
|
this.paintRow(row, sig, () => {
|
|
4459
5633
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
4460
5634
|
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
@@ -4481,6 +5655,16 @@ var init_screen = __esm({
|
|
|
4481
5655
|
this.term.dim(planLabel);
|
|
4482
5656
|
}
|
|
4483
5657
|
this.term(" \xB7 ").dim(this.banner.hint);
|
|
5658
|
+
if (right) {
|
|
5659
|
+
const visibleWidth = stringWidth(right.text);
|
|
5660
|
+
const col = Math.max(1, w - visibleWidth + 1);
|
|
5661
|
+
this.term.moveTo(col, row).eraseLineAfter();
|
|
5662
|
+
if (right.kind === "search") {
|
|
5663
|
+
this.term.brightCyan.noFormat(right.text);
|
|
5664
|
+
} else {
|
|
5665
|
+
this.term.brightYellow.noFormat(right.text);
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
4484
5668
|
});
|
|
4485
5669
|
}
|
|
4486
5670
|
placeCursor() {
|
|
@@ -4496,6 +5680,11 @@ var init_screen = __esm({
|
|
|
4496
5680
|
this.term.moveTo(2, top2);
|
|
4497
5681
|
return;
|
|
4498
5682
|
}
|
|
5683
|
+
if (this.scrollbackSearch) {
|
|
5684
|
+
this.term.hideCursor(true);
|
|
5685
|
+
return;
|
|
5686
|
+
}
|
|
5687
|
+
this.term.hideCursor(false);
|
|
4499
5688
|
const w = this.term.width;
|
|
4500
5689
|
const room = Math.max(1, w - 2);
|
|
4501
5690
|
const state = this.dispatcher.state();
|
|
@@ -4582,8 +5771,10 @@ var init_screen = __esm({
|
|
|
4582
5771
|
}
|
|
4583
5772
|
const prefix = line.prefix ?? "";
|
|
4584
5773
|
const room = Math.max(1, width - prefix.length);
|
|
4585
|
-
const
|
|
5774
|
+
const stripMarkup = line.bodyStyle === "agent";
|
|
5775
|
+
const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room, { stripMarkup });
|
|
4586
5776
|
const wrapped = [];
|
|
5777
|
+
let scanPos = 0;
|
|
4587
5778
|
for (let i = 0; i < chunks.length; i++) {
|
|
4588
5779
|
const chunk = chunks[i] ?? "";
|
|
4589
5780
|
const wrappedLine = {
|
|
@@ -4602,6 +5793,15 @@ var init_screen = __esm({
|
|
|
4602
5793
|
if (line.ansi) {
|
|
4603
5794
|
wrappedLine.ansi = true;
|
|
4604
5795
|
}
|
|
5796
|
+
if (id !== void 0 && chunk.length > 0) {
|
|
5797
|
+
const found = line.body.indexOf(chunk, scanPos);
|
|
5798
|
+
const colOffset = found === -1 ? scanPos : found;
|
|
5799
|
+
this.wrapOrigin.set(wrappedLine, {
|
|
5800
|
+
sourceLineId: id,
|
|
5801
|
+
sourceColOffset: colOffset
|
|
5802
|
+
});
|
|
5803
|
+
scanPos = colOffset + chunk.length;
|
|
5804
|
+
}
|
|
4605
5805
|
wrapped.push(wrappedLine);
|
|
4606
5806
|
}
|
|
4607
5807
|
if (id !== void 0) {
|
|
@@ -4609,13 +5809,25 @@ var init_screen = __esm({
|
|
|
4609
5809
|
}
|
|
4610
5810
|
return wrapped;
|
|
4611
5811
|
}
|
|
4612
|
-
writeFormattedLine(line, width) {
|
|
5812
|
+
writeFormattedLine(line, width, activeMatchCol = null, activeMatchLength = 0) {
|
|
4613
5813
|
if (line.prefix) {
|
|
4614
5814
|
writeStyled(this.term, line.prefix, line.prefixStyle ?? line.bodyStyle);
|
|
4615
5815
|
}
|
|
4616
5816
|
const remaining = Math.max(0, width - (line.prefix?.length ?? 0));
|
|
4617
|
-
const
|
|
4618
|
-
|
|
5817
|
+
const stripMarkup = line.bodyStyle === "agent";
|
|
5818
|
+
const bodyText = line.ansi ? line.body : truncate(line.body, remaining, { stripMarkup });
|
|
5819
|
+
if (this.scrollbackHighlight !== null && !line.ansi) {
|
|
5820
|
+
writeBodyWithHighlight(
|
|
5821
|
+
this.term,
|
|
5822
|
+
bodyText,
|
|
5823
|
+
line.bodyStyle,
|
|
5824
|
+
this.scrollbackHighlight,
|
|
5825
|
+
activeMatchCol,
|
|
5826
|
+
activeMatchLength
|
|
5827
|
+
);
|
|
5828
|
+
} else {
|
|
5829
|
+
writeStyled(this.term, bodyText, line.bodyStyle);
|
|
5830
|
+
}
|
|
4619
5831
|
if (line.fillRow) {
|
|
4620
5832
|
const visible = line.ansi ? stringWidth(bodyText) : bodyText.length;
|
|
4621
5833
|
const pad = remaining - visible;
|
|
@@ -4630,6 +5842,7 @@ var init_screen = __esm({
|
|
|
4630
5842
|
};
|
|
4631
5843
|
NON_ASCII = /[^\x20-\x7e]/;
|
|
4632
5844
|
SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
5845
|
+
TK_MARKUP_STYLE_CHAR = /[a-zA-Z+\-:_!#/]/;
|
|
4633
5846
|
shortId = stripHydraSessionPrefix;
|
|
4634
5847
|
}
|
|
4635
5848
|
});
|
|
@@ -4653,6 +5866,14 @@ var init_input = __esm({
|
|
|
4653
5866
|
queueIndex = -1;
|
|
4654
5867
|
savedDraft = null;
|
|
4655
5868
|
history = [];
|
|
5869
|
+
// Active reverse-incremental search over `history`. Set when ^r is
|
|
5870
|
+
// pressed; cleared when the user accepts (Enter / typing / arrows)
|
|
5871
|
+
// or cancels (ESC). `query` is the lowercased substring matched
|
|
5872
|
+
// against history entries; `matchIndices` are history indices in
|
|
5873
|
+
// newest→oldest order; `cursor` is the current index into that list.
|
|
5874
|
+
// `savedDraft` snapshots the buffer/cursor at the moment search
|
|
5875
|
+
// began so ESC can restore it.
|
|
5876
|
+
historySearch = null;
|
|
4656
5877
|
// Waiting queue snapshot (excludes the in-flight head). Newest item lives
|
|
4657
5878
|
// at the end so Up walks the array right-to-left.
|
|
4658
5879
|
queue = [];
|
|
@@ -4672,7 +5893,8 @@ var init_input = __esm({
|
|
|
4672
5893
|
col: this.col,
|
|
4673
5894
|
planMode: this.planMode,
|
|
4674
5895
|
historyIndex: this.historyIndex,
|
|
4675
|
-
queueIndex: this.queueIndex
|
|
5896
|
+
queueIndex: this.queueIndex,
|
|
5897
|
+
historySearchQuery: this.historySearch?.query ?? null
|
|
4676
5898
|
};
|
|
4677
5899
|
}
|
|
4678
5900
|
setTurnRunning(running) {
|
|
@@ -4682,6 +5904,7 @@ var init_input = __esm({
|
|
|
4682
5904
|
this.history = [...history];
|
|
4683
5905
|
this.historyIndex = -1;
|
|
4684
5906
|
this.savedDraft = null;
|
|
5907
|
+
this.historySearch = null;
|
|
4685
5908
|
}
|
|
4686
5909
|
// Snapshot of the waiting queue (head excluded). Called by the app after
|
|
4687
5910
|
// every queue mutation so Up/Down can walk a fresh view. queueIndex is
|
|
@@ -4710,8 +5933,44 @@ var init_input = __esm({
|
|
|
4710
5933
|
this.historyIndex = -1;
|
|
4711
5934
|
this.queueIndex = -1;
|
|
4712
5935
|
this.savedDraft = null;
|
|
5936
|
+
this.historySearch = null;
|
|
4713
5937
|
}
|
|
4714
5938
|
feed(event) {
|
|
5939
|
+
if (this.historySearch !== null) {
|
|
5940
|
+
if (event.type === "char") {
|
|
5941
|
+
return this.mutateHistorySearchQuery(
|
|
5942
|
+
this.historySearch.query + event.ch.toLowerCase()
|
|
5943
|
+
);
|
|
5944
|
+
}
|
|
5945
|
+
if (event.type === "paste") {
|
|
5946
|
+
return this.mutateHistorySearchQuery(
|
|
5947
|
+
this.historySearch.query + event.text.replace(/\n/g, " ").toLowerCase()
|
|
5948
|
+
);
|
|
5949
|
+
}
|
|
5950
|
+
if (event.type === "key") {
|
|
5951
|
+
if (event.name === "ctrl-r") {
|
|
5952
|
+
return this.advanceHistorySearch();
|
|
5953
|
+
}
|
|
5954
|
+
if (event.name === "ctrl-s") {
|
|
5955
|
+
this.retreatHistorySearch();
|
|
5956
|
+
return [];
|
|
5957
|
+
}
|
|
5958
|
+
if (event.name === "escape") {
|
|
5959
|
+
this.cancelHistorySearch();
|
|
5960
|
+
return [];
|
|
5961
|
+
}
|
|
5962
|
+
if (event.name === "backspace") {
|
|
5963
|
+
if (this.historySearch.query.length === 0) {
|
|
5964
|
+
this.cancelHistorySearch();
|
|
5965
|
+
return [];
|
|
5966
|
+
}
|
|
5967
|
+
return this.mutateHistorySearchQuery(
|
|
5968
|
+
this.historySearch.query.slice(0, -1)
|
|
5969
|
+
);
|
|
5970
|
+
}
|
|
5971
|
+
this.historySearch = null;
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
4715
5974
|
if (event.type === "char") {
|
|
4716
5975
|
this.insertChar(event.ch);
|
|
4717
5976
|
return [];
|
|
@@ -4789,6 +6048,10 @@ var init_input = __esm({
|
|
|
4789
6048
|
return [{ type: "redraw" }];
|
|
4790
6049
|
case "ctrl-p":
|
|
4791
6050
|
return [{ type: "switch-session" }];
|
|
6051
|
+
case "ctrl-r":
|
|
6052
|
+
return this.startHistorySearch();
|
|
6053
|
+
case "ctrl-s":
|
|
6054
|
+
return [];
|
|
4792
6055
|
case "ctrl-u":
|
|
4793
6056
|
this.killLine();
|
|
4794
6057
|
return [];
|
|
@@ -4824,6 +6087,7 @@ var init_input = __esm({
|
|
|
4824
6087
|
this.historyIndex = -1;
|
|
4825
6088
|
this.queueIndex = -1;
|
|
4826
6089
|
this.savedDraft = null;
|
|
6090
|
+
this.historySearch = null;
|
|
4827
6091
|
}
|
|
4828
6092
|
insertChar(ch) {
|
|
4829
6093
|
if (ch.length === 0) {
|
|
@@ -5051,6 +6315,143 @@ var init_input = __esm({
|
|
|
5051
6315
|
this.clearBuffer();
|
|
5052
6316
|
}
|
|
5053
6317
|
}
|
|
6318
|
+
// Engage reverse-incremental search over prompt history. Uses the
|
|
6319
|
+
// current buffer text as the search query. With an empty buffer we
|
|
6320
|
+
// enter search mode in an "empty query, no match shown" state — the
|
|
6321
|
+
// banner indicator lights up, and as the user types we extend the
|
|
6322
|
+
// query and load top matches. We deliberately do NOT auto-load the
|
|
6323
|
+
// most recent entry on an empty ^R (that's a surprise — Up-arrow
|
|
6324
|
+
// already walks history if that's what they wanted). With a
|
|
6325
|
+
// non-empty query that has no history match, escalate straight to
|
|
6326
|
+
// scrollback search so the typed term searches session output.
|
|
6327
|
+
startHistorySearch() {
|
|
6328
|
+
const query = this.bufferText().toLowerCase();
|
|
6329
|
+
if (query.length === 0) {
|
|
6330
|
+
this.historySearch = {
|
|
6331
|
+
query: "",
|
|
6332
|
+
matchIndices: [],
|
|
6333
|
+
cursor: 0,
|
|
6334
|
+
savedDraft: {
|
|
6335
|
+
buffer: [...this.buffer],
|
|
6336
|
+
row: this.row,
|
|
6337
|
+
col: this.col
|
|
6338
|
+
}
|
|
6339
|
+
};
|
|
6340
|
+
return [];
|
|
6341
|
+
}
|
|
6342
|
+
const matchIndices = this.findHistoryMatches(query);
|
|
6343
|
+
if (matchIndices.length === 0) {
|
|
6344
|
+
return [{ type: "escalate-search", query }];
|
|
6345
|
+
}
|
|
6346
|
+
this.historySearch = {
|
|
6347
|
+
query,
|
|
6348
|
+
matchIndices,
|
|
6349
|
+
cursor: 0,
|
|
6350
|
+
savedDraft: {
|
|
6351
|
+
buffer: [...this.buffer],
|
|
6352
|
+
row: this.row,
|
|
6353
|
+
col: this.col
|
|
6354
|
+
}
|
|
6355
|
+
};
|
|
6356
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
6357
|
+
return [];
|
|
6358
|
+
}
|
|
6359
|
+
// ^R advance. At the oldest match with a non-empty query, falls
|
|
6360
|
+
// through to scrollback search (same escalate path as a never-
|
|
6361
|
+
// matched startHistorySearch). With an empty query at the oldest
|
|
6362
|
+
// match (i.e. the user walked all history with no filter), advance
|
|
6363
|
+
// is a no-op so the buffer stays on the oldest entry.
|
|
6364
|
+
advanceHistorySearch() {
|
|
6365
|
+
if (this.historySearch === null) {
|
|
6366
|
+
return [];
|
|
6367
|
+
}
|
|
6368
|
+
const search = this.historySearch;
|
|
6369
|
+
const atOldest = search.cursor >= search.matchIndices.length - 1;
|
|
6370
|
+
if (atOldest) {
|
|
6371
|
+
if (search.query.length === 0) {
|
|
6372
|
+
return [];
|
|
6373
|
+
}
|
|
6374
|
+
const query = search.query;
|
|
6375
|
+
const draft = search.savedDraft;
|
|
6376
|
+
this.historySearch = null;
|
|
6377
|
+
this.buffer = [...draft.buffer];
|
|
6378
|
+
this.row = draft.row;
|
|
6379
|
+
this.col = draft.col;
|
|
6380
|
+
return [{ type: "escalate-search", query }];
|
|
6381
|
+
}
|
|
6382
|
+
search.cursor += 1;
|
|
6383
|
+
const idx = search.matchIndices[search.cursor];
|
|
6384
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
6385
|
+
return [];
|
|
6386
|
+
}
|
|
6387
|
+
// ^S retreat — walk toward newer matches. No-op at the newest match
|
|
6388
|
+
// (no wrap, mirroring ^R no-wrap at the oldest).
|
|
6389
|
+
retreatHistorySearch() {
|
|
6390
|
+
if (this.historySearch === null) {
|
|
6391
|
+
return;
|
|
6392
|
+
}
|
|
6393
|
+
if (this.historySearch.cursor === 0) {
|
|
6394
|
+
return;
|
|
6395
|
+
}
|
|
6396
|
+
this.historySearch.cursor -= 1;
|
|
6397
|
+
const idx = this.historySearch.matchIndices[this.historySearch.cursor];
|
|
6398
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
6399
|
+
}
|
|
6400
|
+
// Backspace / typing within search mode mutates the query and
|
|
6401
|
+
// re-searches. When the new query is empty, restore the saved
|
|
6402
|
+
// draft buffer (typically empty) and stay in search mode — the
|
|
6403
|
+
// user can keep typing. When the new query has matches, load the
|
|
6404
|
+
// top one. When the new query has no matches, escalate to scrollback
|
|
6405
|
+
// search so the typed term applies there instead.
|
|
6406
|
+
mutateHistorySearchQuery(newQuery) {
|
|
6407
|
+
if (this.historySearch === null) {
|
|
6408
|
+
return [];
|
|
6409
|
+
}
|
|
6410
|
+
if (newQuery.length === 0) {
|
|
6411
|
+
this.historySearch.query = "";
|
|
6412
|
+
this.historySearch.matchIndices = [];
|
|
6413
|
+
this.historySearch.cursor = 0;
|
|
6414
|
+
const draft = this.historySearch.savedDraft;
|
|
6415
|
+
this.buffer = [...draft.buffer];
|
|
6416
|
+
this.row = draft.row;
|
|
6417
|
+
this.col = draft.col;
|
|
6418
|
+
return [];
|
|
6419
|
+
}
|
|
6420
|
+
const matchIndices = this.findHistoryMatches(newQuery);
|
|
6421
|
+
if (matchIndices.length === 0) {
|
|
6422
|
+
const draft = this.historySearch.savedDraft;
|
|
6423
|
+
this.historySearch = null;
|
|
6424
|
+
this.buffer = [...draft.buffer];
|
|
6425
|
+
this.row = draft.row;
|
|
6426
|
+
this.col = draft.col;
|
|
6427
|
+
return [{ type: "escalate-search", query: newQuery }];
|
|
6428
|
+
}
|
|
6429
|
+
this.historySearch.query = newQuery;
|
|
6430
|
+
this.historySearch.matchIndices = matchIndices;
|
|
6431
|
+
this.historySearch.cursor = 0;
|
|
6432
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
6433
|
+
return [];
|
|
6434
|
+
}
|
|
6435
|
+
findHistoryMatches(query) {
|
|
6436
|
+
const out = [];
|
|
6437
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
6438
|
+
const entry = this.history[i] ?? "";
|
|
6439
|
+
if (query.length === 0 || entry.toLowerCase().includes(query)) {
|
|
6440
|
+
out.push(i);
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
return out;
|
|
6444
|
+
}
|
|
6445
|
+
cancelHistorySearch() {
|
|
6446
|
+
if (this.historySearch === null) {
|
|
6447
|
+
return;
|
|
6448
|
+
}
|
|
6449
|
+
const draft = this.historySearch.savedDraft;
|
|
6450
|
+
this.historySearch = null;
|
|
6451
|
+
this.buffer = [...draft.buffer];
|
|
6452
|
+
this.row = draft.row;
|
|
6453
|
+
this.col = draft.col;
|
|
6454
|
+
}
|
|
5054
6455
|
loadEntry(text) {
|
|
5055
6456
|
this.buffer = text.split("\n");
|
|
5056
6457
|
if (this.buffer.length === 0) {
|
|
@@ -5790,6 +7191,7 @@ import { nanoid as nanoid3 } from "nanoid";
|
|
|
5790
7191
|
import termkit from "terminal-kit";
|
|
5791
7192
|
async function runTuiApp(opts) {
|
|
5792
7193
|
const config = await ensureConfig();
|
|
7194
|
+
logMaxBytes = config.tui.logMaxBytes;
|
|
5793
7195
|
await ensureDaemonReachable(config);
|
|
5794
7196
|
const term = termkit.terminal;
|
|
5795
7197
|
const exitHint = {};
|
|
@@ -5810,7 +7212,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5810
7212
|
process.exit(0);
|
|
5811
7213
|
}
|
|
5812
7214
|
const launchLabel = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
5813
|
-
term.
|
|
7215
|
+
term.brightYellow(launchLabel)("\n");
|
|
5814
7216
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
5815
7217
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
5816
7218
|
const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
|
|
@@ -5897,13 +7299,25 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5897
7299
|
} else if (event?.kind === "turn-complete") {
|
|
5898
7300
|
adjustPendingTurns(-1);
|
|
5899
7301
|
}
|
|
7302
|
+
if (rawTag === "permission_resolved") {
|
|
7303
|
+
handlePermissionResolved(update);
|
|
7304
|
+
return;
|
|
7305
|
+
}
|
|
5900
7306
|
appendRender(event);
|
|
5901
7307
|
maybeDismissPermissionByToolUpdate(update);
|
|
5902
7308
|
});
|
|
5903
|
-
|
|
5904
|
-
const
|
|
5905
|
-
|
|
5906
|
-
|
|
7309
|
+
const handlePermissionResolved = (update) => {
|
|
7310
|
+
const u = update ?? {};
|
|
7311
|
+
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
|
|
7312
|
+
let outcome;
|
|
7313
|
+
if (u.outcome && typeof u.outcome === "object") {
|
|
7314
|
+
outcome = u.outcome;
|
|
7315
|
+
} else if (typeof u.chosenOptionId === "string") {
|
|
7316
|
+
outcome = { kind: "selected", optionId: u.chosenOptionId };
|
|
7317
|
+
}
|
|
7318
|
+
const result = outcome ? { outcome } : void 0;
|
|
7319
|
+
dismissPermissionExternally(toolCallId, result);
|
|
7320
|
+
};
|
|
5907
7321
|
let pendingPermission = null;
|
|
5908
7322
|
const dismissPermissionExternally = (toolCallId, result) => {
|
|
5909
7323
|
if (!pendingPermission) {
|
|
@@ -5997,12 +7411,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5997
7411
|
let agentInfoName;
|
|
5998
7412
|
try {
|
|
5999
7413
|
const initResult = await conn.request("initialize", {
|
|
6000
|
-
protocolVersion:
|
|
7414
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
6001
7415
|
clientCapabilities: {
|
|
6002
7416
|
fs: { readTextFile: false, writeTextFile: false },
|
|
6003
7417
|
terminal: false
|
|
6004
7418
|
},
|
|
6005
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
7419
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
6006
7420
|
});
|
|
6007
7421
|
agentInfoName = initResult?.agentInfo?.name;
|
|
6008
7422
|
} catch {
|
|
@@ -6051,7 +7465,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6051
7465
|
const attached = await conn.request("session/attach", {
|
|
6052
7466
|
sessionId: ctx.sessionId,
|
|
6053
7467
|
historyPolicy: "full",
|
|
6054
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
7468
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
6055
7469
|
});
|
|
6056
7470
|
resolvedSessionId = attached.sessionId;
|
|
6057
7471
|
exitHint.sessionId = resolvedSessionId;
|
|
@@ -6096,6 +7510,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6096
7510
|
if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
|
|
6097
7511
|
continue;
|
|
6098
7512
|
}
|
|
7513
|
+
if (tryHandleScrollbackSearchKey(ev)) {
|
|
7514
|
+
continue;
|
|
7515
|
+
}
|
|
6099
7516
|
if (tryHandleCompletionKey(ev)) {
|
|
6100
7517
|
continue;
|
|
6101
7518
|
}
|
|
@@ -6105,6 +7522,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6105
7522
|
}
|
|
6106
7523
|
}
|
|
6107
7524
|
refreshCompletions();
|
|
7525
|
+
screen.setBannerSearchIndicator(
|
|
7526
|
+
dispatcher.state().historySearchQuery
|
|
7527
|
+
);
|
|
6108
7528
|
screen.refreshPrompt();
|
|
6109
7529
|
}
|
|
6110
7530
|
});
|
|
@@ -6167,6 +7587,55 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6167
7587
|
dispatcher.replaceFirstLine(next);
|
|
6168
7588
|
return true;
|
|
6169
7589
|
};
|
|
7590
|
+
const tryHandleScrollbackSearchKey = (ev) => {
|
|
7591
|
+
if (!screen.isScrollbackSearchActive()) {
|
|
7592
|
+
if (ev.type === "key" && ev.name === "ctrl-r" && screen.isScrolledBack()) {
|
|
7593
|
+
screen.enterScrollbackSearch();
|
|
7594
|
+
screen.updateScrollbackSearchTerm("");
|
|
7595
|
+
return true;
|
|
7596
|
+
}
|
|
7597
|
+
return false;
|
|
7598
|
+
}
|
|
7599
|
+
if (ev.type === "char") {
|
|
7600
|
+
const term2 = screen.scrollbackSearchTerm() + ev.ch;
|
|
7601
|
+
screen.updateScrollbackSearchTerm(term2);
|
|
7602
|
+
return true;
|
|
7603
|
+
}
|
|
7604
|
+
if (ev.type === "paste") {
|
|
7605
|
+
const term2 = screen.scrollbackSearchTerm() + ev.text.replace(/\n/g, " ");
|
|
7606
|
+
screen.updateScrollbackSearchTerm(term2);
|
|
7607
|
+
return true;
|
|
7608
|
+
}
|
|
7609
|
+
if (ev.type === "key") {
|
|
7610
|
+
switch (ev.name) {
|
|
7611
|
+
case "ctrl-r":
|
|
7612
|
+
screen.advanceScrollbackSearch();
|
|
7613
|
+
return true;
|
|
7614
|
+
case "ctrl-s":
|
|
7615
|
+
screen.retreatScrollbackSearch();
|
|
7616
|
+
return true;
|
|
7617
|
+
case "backspace": {
|
|
7618
|
+
const term2 = screen.scrollbackSearchTerm();
|
|
7619
|
+
if (term2.length === 0) {
|
|
7620
|
+
screen.cancelScrollbackSearch();
|
|
7621
|
+
} else {
|
|
7622
|
+
screen.updateScrollbackSearchTerm(term2.slice(0, -1));
|
|
7623
|
+
}
|
|
7624
|
+
return true;
|
|
7625
|
+
}
|
|
7626
|
+
case "enter":
|
|
7627
|
+
screen.acceptScrollbackSearch();
|
|
7628
|
+
return true;
|
|
7629
|
+
case "escape":
|
|
7630
|
+
case "ctrl-c":
|
|
7631
|
+
screen.cancelScrollbackSearch();
|
|
7632
|
+
return true;
|
|
7633
|
+
default:
|
|
7634
|
+
return true;
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
return true;
|
|
7638
|
+
};
|
|
6170
7639
|
const tryHandlePermissionKey = (ev) => {
|
|
6171
7640
|
if (!pendingPermission) {
|
|
6172
7641
|
return false;
|
|
@@ -6437,6 +7906,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6437
7906
|
toolsExpanded = !toolsExpanded;
|
|
6438
7907
|
renderToolsBlock();
|
|
6439
7908
|
return;
|
|
7909
|
+
case "escalate-search":
|
|
7910
|
+
screen.enterScrollbackSearch();
|
|
7911
|
+
screen.updateScrollbackSearchTerm(effect.query);
|
|
7912
|
+
return;
|
|
6440
7913
|
}
|
|
6441
7914
|
};
|
|
6442
7915
|
const promptQueue = [];
|
|
@@ -6482,6 +7955,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6482
7955
|
toolCallOrder.length = 0;
|
|
6483
7956
|
toolsBlockStartedAt = null;
|
|
6484
7957
|
toolsBlockEndedAt = null;
|
|
7958
|
+
toolsBlockStopReason = null;
|
|
6485
7959
|
toolsExpanded = false;
|
|
6486
7960
|
screen.clearScrollback();
|
|
6487
7961
|
return true;
|
|
@@ -6685,6 +8159,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6685
8159
|
let toolsExpanded = false;
|
|
6686
8160
|
let toolsBlockStartedAt = null;
|
|
6687
8161
|
let toolsBlockEndedAt = null;
|
|
8162
|
+
let toolsBlockStopReason = null;
|
|
6688
8163
|
const TOOLS_COLLAPSED_LIMIT = 5;
|
|
6689
8164
|
let agentBuffer = "";
|
|
6690
8165
|
let agentKey = null;
|
|
@@ -6726,12 +8201,17 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6726
8201
|
const inProgress = toolsBlockEndedAt === null;
|
|
6727
8202
|
const end = toolsBlockEndedAt ?? Date.now();
|
|
6728
8203
|
const elapsed = end - toolsBlockStartedAt;
|
|
8204
|
+
const stoppedReason = !inProgress && toolsBlockStopReason !== null && toolsBlockStopReason !== "end_turn" ? toolsBlockStopReason : null;
|
|
6729
8205
|
let summary;
|
|
6730
8206
|
if (total === 0) {
|
|
6731
|
-
|
|
8207
|
+
if (stoppedReason !== null) {
|
|
8208
|
+
summary = `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}`;
|
|
8209
|
+
} else {
|
|
8210
|
+
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
|
|
8211
|
+
}
|
|
6732
8212
|
} else {
|
|
6733
8213
|
const noun = total === 1 ? "tool" : "tools";
|
|
6734
|
-
const timing = inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
8214
|
+
const timing = stoppedReason !== null ? `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}` : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
6735
8215
|
const parts = [`${total} ${noun}`, timing];
|
|
6736
8216
|
if (inProgress) {
|
|
6737
8217
|
if (hidden > 0) {
|
|
@@ -6743,12 +8223,14 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6743
8223
|
summary = parts.join(" \xB7 ");
|
|
6744
8224
|
}
|
|
6745
8225
|
const pureThinking = total === 0 && inProgress;
|
|
8226
|
+
const frozenStyle = stoppedReason !== null ? "tool-status-fail" : "tool";
|
|
8227
|
+
const frozenBodyStyle = stoppedReason !== null ? "tool-status-fail" : "dim";
|
|
6746
8228
|
const lines = [
|
|
6747
8229
|
{
|
|
6748
8230
|
prefix: "\u2692 ",
|
|
6749
|
-
prefixStyle: pureThinking ? "tool-status-running" :
|
|
8231
|
+
prefixStyle: pureThinking ? "tool-status-running" : frozenStyle,
|
|
6750
8232
|
body: summary,
|
|
6751
|
-
bodyStyle: pureThinking ? "tool-status-running" :
|
|
8233
|
+
bodyStyle: pureThinking ? "tool-status-running" : frozenBodyStyle
|
|
6752
8234
|
}
|
|
6753
8235
|
];
|
|
6754
8236
|
for (const id of visibleIds) {
|
|
@@ -6762,6 +8244,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6762
8244
|
const startToolsBlock = () => {
|
|
6763
8245
|
toolsBlockStartedAt = Date.now();
|
|
6764
8246
|
toolsBlockEndedAt = null;
|
|
8247
|
+
toolsBlockStopReason = null;
|
|
6765
8248
|
renderToolsBlock();
|
|
6766
8249
|
};
|
|
6767
8250
|
const recordToolCall = (id, title, status) => {
|
|
@@ -6786,6 +8269,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6786
8269
|
if (toolsBlockStartedAt === null) {
|
|
6787
8270
|
toolsBlockStartedAt = Date.now();
|
|
6788
8271
|
toolsBlockEndedAt = null;
|
|
8272
|
+
toolsBlockStopReason = null;
|
|
6789
8273
|
}
|
|
6790
8274
|
toolCallOrder.push(id);
|
|
6791
8275
|
}
|
|
@@ -6887,6 +8371,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6887
8371
|
screen.clearKey("plan");
|
|
6888
8372
|
if (toolsBlockStartedAt !== null) {
|
|
6889
8373
|
toolsBlockEndedAt = Date.now();
|
|
8374
|
+
toolsBlockStopReason = event.stopReason ?? null;
|
|
6890
8375
|
renderToolsBlock();
|
|
6891
8376
|
screen.clearKey("tools");
|
|
6892
8377
|
}
|
|
@@ -6894,6 +8379,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6894
8379
|
toolCallOrder.length = 0;
|
|
6895
8380
|
toolsBlockStartedAt = null;
|
|
6896
8381
|
toolsBlockEndedAt = null;
|
|
8382
|
+
toolsBlockStopReason = null;
|
|
6897
8383
|
toolsExpanded = false;
|
|
6898
8384
|
screen.ensureSeparator();
|
|
6899
8385
|
}
|
|
@@ -6937,12 +8423,14 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6937
8423
|
closeAgentText();
|
|
6938
8424
|
if (toolsBlockStartedAt !== null) {
|
|
6939
8425
|
toolsBlockEndedAt = Date.now();
|
|
8426
|
+
toolsBlockStopReason = null;
|
|
6940
8427
|
renderToolsBlock();
|
|
6941
8428
|
screen.clearKey("tools");
|
|
6942
8429
|
toolStates.clear();
|
|
6943
8430
|
toolCallOrder.length = 0;
|
|
6944
8431
|
toolsBlockStartedAt = null;
|
|
6945
8432
|
toolsBlockEndedAt = null;
|
|
8433
|
+
toolsBlockStopReason = null;
|
|
6946
8434
|
toolsExpanded = false;
|
|
6947
8435
|
}
|
|
6948
8436
|
screen.clearKey("plan");
|
|
@@ -6960,12 +8448,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6960
8448
|
id: `tui-reinit-${nanoid3()}`,
|
|
6961
8449
|
method: "initialize",
|
|
6962
8450
|
params: {
|
|
6963
|
-
protocolVersion:
|
|
8451
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
6964
8452
|
clientCapabilities: {
|
|
6965
8453
|
fs: { readTextFile: false, writeTextFile: false },
|
|
6966
8454
|
terminal: false
|
|
6967
8455
|
},
|
|
6968
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
8456
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
6969
8457
|
}
|
|
6970
8458
|
};
|
|
6971
8459
|
try {
|
|
@@ -6979,7 +8467,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6979
8467
|
params: {
|
|
6980
8468
|
sessionId: resolvedSessionId,
|
|
6981
8469
|
historyPolicy: "none",
|
|
6982
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
8470
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
|
|
6983
8471
|
...upstreamSessionId !== void 0 ? {
|
|
6984
8472
|
_meta: {
|
|
6985
8473
|
[HYDRA_META_KEY]: {
|
|
@@ -7103,15 +8591,15 @@ function writeDebugLine(payload) {
|
|
|
7103
8591
|
}
|
|
7104
8592
|
function rotateIfBig(target) {
|
|
7105
8593
|
try {
|
|
7106
|
-
const
|
|
7107
|
-
if (
|
|
8594
|
+
const stat4 = statSync(target);
|
|
8595
|
+
if (stat4.size < logMaxBytes) {
|
|
7108
8596
|
return;
|
|
7109
8597
|
}
|
|
7110
8598
|
renameSync(target, `${target}.0`);
|
|
7111
8599
|
} catch {
|
|
7112
8600
|
}
|
|
7113
8601
|
}
|
|
7114
|
-
var PLAN_PREFIX_TEXT,
|
|
8602
|
+
var PLAN_PREFIX_TEXT, logMaxBytes;
|
|
7115
8603
|
var init_app = __esm({
|
|
7116
8604
|
"src/tui/app.ts"() {
|
|
7117
8605
|
"use strict";
|
|
@@ -7122,6 +8610,7 @@ var init_app = __esm({
|
|
|
7122
8610
|
init_daemon_bootstrap();
|
|
7123
8611
|
init_session();
|
|
7124
8612
|
init_paths();
|
|
8613
|
+
init_hydra_version();
|
|
7125
8614
|
init_history();
|
|
7126
8615
|
init_discovery();
|
|
7127
8616
|
init_picker();
|
|
@@ -7131,7 +8620,7 @@ var init_app = __esm({
|
|
|
7131
8620
|
init_render_update();
|
|
7132
8621
|
init_format();
|
|
7133
8622
|
PLAN_PREFIX_TEXT = "Plan mode is on. Outline what you would do without making any changes. Do not edit files, run shell commands, or otherwise execute side effects; produce a plan only.";
|
|
7134
|
-
|
|
8623
|
+
logMaxBytes = 5 * 1024 * 1024;
|
|
7135
8624
|
}
|
|
7136
8625
|
});
|
|
7137
8626
|
|
|
@@ -7148,9 +8637,9 @@ var init_tui = __esm({
|
|
|
7148
8637
|
});
|
|
7149
8638
|
|
|
7150
8639
|
// src/cli.ts
|
|
7151
|
-
import { readFileSync } from "fs";
|
|
7152
|
-
import { fileURLToPath } from "url";
|
|
7153
|
-
import { dirname as
|
|
8640
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
8641
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8642
|
+
import { dirname as dirname6, resolve as resolve4 } from "path";
|
|
7154
8643
|
|
|
7155
8644
|
// src/cli/parse-args.ts
|
|
7156
8645
|
function parseArgs(argv) {
|
|
@@ -7243,13 +8732,13 @@ New token: ${newToken}
|
|
|
7243
8732
|
// src/cli/commands/daemon.ts
|
|
7244
8733
|
init_paths();
|
|
7245
8734
|
init_config();
|
|
7246
|
-
import * as
|
|
8735
|
+
import * as fsp6 from "fs/promises";
|
|
7247
8736
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
7248
8737
|
|
|
7249
8738
|
// src/daemon/server.ts
|
|
7250
8739
|
init_config();
|
|
7251
|
-
import * as
|
|
7252
|
-
import * as
|
|
8740
|
+
import * as fs11 from "fs";
|
|
8741
|
+
import * as fsp4 from "fs/promises";
|
|
7253
8742
|
import Fastify from "fastify";
|
|
7254
8743
|
import websocketPlugin from "@fastify/websocket";
|
|
7255
8744
|
import pino from "pino";
|
|
@@ -7433,31 +8922,148 @@ function run(cmd, args) {
|
|
|
7433
8922
|
const child = spawn(cmd, args, {
|
|
7434
8923
|
stdio: ["ignore", "ignore", "inherit"]
|
|
7435
8924
|
});
|
|
7436
|
-
child.on("error", reject);
|
|
8925
|
+
child.on("error", reject);
|
|
8926
|
+
child.on("exit", (code, signal) => {
|
|
8927
|
+
if (code === 0) {
|
|
8928
|
+
resolve5();
|
|
8929
|
+
return;
|
|
8930
|
+
}
|
|
8931
|
+
reject(
|
|
8932
|
+
new Error(
|
|
8933
|
+
`${cmd} ${args.join(" ")} exited with ${code !== null ? `code ${code}` : `signal ${signal}`}`
|
|
8934
|
+
)
|
|
8935
|
+
);
|
|
8936
|
+
});
|
|
8937
|
+
});
|
|
8938
|
+
}
|
|
8939
|
+
async function hasCommand(name) {
|
|
8940
|
+
return new Promise((resolve5) => {
|
|
8941
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
8942
|
+
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
8943
|
+
child.on("error", () => resolve5(false));
|
|
8944
|
+
child.on("exit", (code) => resolve5(code === 0));
|
|
8945
|
+
});
|
|
8946
|
+
}
|
|
8947
|
+
async function fileExists(p) {
|
|
8948
|
+
try {
|
|
8949
|
+
await fsp.access(p);
|
|
8950
|
+
return true;
|
|
8951
|
+
} catch {
|
|
8952
|
+
return false;
|
|
8953
|
+
}
|
|
8954
|
+
}
|
|
8955
|
+
|
|
8956
|
+
// src/core/npm-install.ts
|
|
8957
|
+
init_paths();
|
|
8958
|
+
import * as fsp2 from "fs/promises";
|
|
8959
|
+
import * as path3 from "path";
|
|
8960
|
+
import { spawn as spawn2 } from "child_process";
|
|
8961
|
+
var logSink2 = (msg) => {
|
|
8962
|
+
process.stderr.write(msg + "\n");
|
|
8963
|
+
};
|
|
8964
|
+
function setNpmInstallLogger(log) {
|
|
8965
|
+
logSink2 = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
8966
|
+
}
|
|
8967
|
+
async function ensureNpmPackage(args) {
|
|
8968
|
+
const platformKey = currentPlatformKey();
|
|
8969
|
+
if (!platformKey) {
|
|
8970
|
+
throw new Error(
|
|
8971
|
+
`Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
|
|
8972
|
+
);
|
|
8973
|
+
}
|
|
8974
|
+
const installDir = paths.agentNpmInstallDir(
|
|
8975
|
+
args.agentId,
|
|
8976
|
+
platformKey,
|
|
8977
|
+
args.version
|
|
8978
|
+
);
|
|
8979
|
+
const binPath = path3.join(installDir, "node_modules", ".bin", args.bin);
|
|
8980
|
+
if (await fileExists2(binPath)) {
|
|
8981
|
+
return binPath;
|
|
8982
|
+
}
|
|
8983
|
+
await installInto({
|
|
8984
|
+
agentId: args.agentId,
|
|
8985
|
+
packageSpec: args.packageSpec,
|
|
8986
|
+
installDir
|
|
8987
|
+
});
|
|
8988
|
+
if (!await fileExists2(binPath)) {
|
|
8989
|
+
throw new Error(
|
|
8990
|
+
`Agent ${args.agentId}: npm install of ${args.packageSpec} did not produce bin ${args.bin} (looked in ${installDir}/node_modules/.bin/)`
|
|
8991
|
+
);
|
|
8992
|
+
}
|
|
8993
|
+
return binPath;
|
|
8994
|
+
}
|
|
8995
|
+
async function installInto(args) {
|
|
8996
|
+
await fsp2.mkdir(path3.dirname(args.installDir), { recursive: true });
|
|
8997
|
+
const tempDir = await fsp2.mkdtemp(`${args.installDir}.partial-`);
|
|
8998
|
+
try {
|
|
8999
|
+
logSink2(
|
|
9000
|
+
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
9001
|
+
);
|
|
9002
|
+
await runNpmInstall({
|
|
9003
|
+
packageSpec: args.packageSpec,
|
|
9004
|
+
cwd: tempDir
|
|
9005
|
+
});
|
|
9006
|
+
try {
|
|
9007
|
+
await fsp2.rename(tempDir, args.installDir);
|
|
9008
|
+
} catch (err) {
|
|
9009
|
+
const e = err;
|
|
9010
|
+
if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists2(args.installDir)) {
|
|
9011
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
9012
|
+
() => void 0
|
|
9013
|
+
);
|
|
9014
|
+
return;
|
|
9015
|
+
}
|
|
9016
|
+
throw err;
|
|
9017
|
+
}
|
|
9018
|
+
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
9019
|
+
} catch (err) {
|
|
9020
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
9021
|
+
() => void 0
|
|
9022
|
+
);
|
|
9023
|
+
throw err;
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
function runNpmInstall(args) {
|
|
9027
|
+
return new Promise((resolve5, reject) => {
|
|
9028
|
+
const child = spawn2(
|
|
9029
|
+
"npm",
|
|
9030
|
+
["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
|
|
9031
|
+
{
|
|
9032
|
+
cwd: args.cwd,
|
|
9033
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
9034
|
+
}
|
|
9035
|
+
);
|
|
9036
|
+
let stderrTail = "";
|
|
9037
|
+
child.stdout?.on("data", (chunk) => {
|
|
9038
|
+
void chunk;
|
|
9039
|
+
});
|
|
9040
|
+
child.stderr?.setEncoding("utf8");
|
|
9041
|
+
child.stderr?.on("data", (chunk) => {
|
|
9042
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
9043
|
+
});
|
|
9044
|
+
child.on("error", (err) => {
|
|
9045
|
+
const msg = err.code === "ENOENT" ? `npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)` : err.message;
|
|
9046
|
+
reject(new Error(msg));
|
|
9047
|
+
});
|
|
7437
9048
|
child.on("exit", (code, signal) => {
|
|
7438
9049
|
if (code === 0) {
|
|
7439
9050
|
resolve5();
|
|
7440
9051
|
return;
|
|
7441
9052
|
}
|
|
9053
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
9054
|
+
const tail = stderrTail.trim();
|
|
7442
9055
|
reject(
|
|
7443
9056
|
new Error(
|
|
7444
|
-
|
|
9057
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
9058
|
+
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
7445
9059
|
)
|
|
7446
9060
|
);
|
|
7447
9061
|
});
|
|
7448
9062
|
});
|
|
7449
9063
|
}
|
|
7450
|
-
async function
|
|
7451
|
-
return new Promise((resolve5) => {
|
|
7452
|
-
const finder = process.platform === "win32" ? "where" : "which";
|
|
7453
|
-
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
7454
|
-
child.on("error", () => resolve5(false));
|
|
7455
|
-
child.on("exit", (code) => resolve5(code === 0));
|
|
7456
|
-
});
|
|
7457
|
-
}
|
|
7458
|
-
async function fileExists(p) {
|
|
9064
|
+
async function fileExists2(p) {
|
|
7459
9065
|
try {
|
|
7460
|
-
await
|
|
9066
|
+
await fsp2.access(p);
|
|
7461
9067
|
return true;
|
|
7462
9068
|
} catch {
|
|
7463
9069
|
return false;
|
|
@@ -7467,6 +9073,10 @@ async function fileExists(p) {
|
|
|
7467
9073
|
// src/core/registry.ts
|
|
7468
9074
|
var NpxDistribution = z2.object({
|
|
7469
9075
|
package: z2.string(),
|
|
9076
|
+
// The bin to invoke after install. Defaults to the package basename
|
|
9077
|
+
// (e.g. "claude-code" for "@anthropic-ai/claude-code"). Required when
|
|
9078
|
+
// the package exposes a bin name that differs from its basename.
|
|
9079
|
+
bin: z2.string().optional(),
|
|
7470
9080
|
args: z2.array(z2.string()).optional(),
|
|
7471
9081
|
env: z2.record(z2.string()).optional()
|
|
7472
9082
|
});
|
|
@@ -7630,9 +9240,23 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
7630
9240
|
if (agent.distribution.npx) {
|
|
7631
9241
|
const npx = agent.distribution.npx;
|
|
7632
9242
|
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
9243
|
+
if (process.env.HYDRA_ACP_SKIP_NPM_PREFETCH) {
|
|
9244
|
+
return {
|
|
9245
|
+
command: "npx",
|
|
9246
|
+
args: ["-y", npx.package, ...tail],
|
|
9247
|
+
env: npx.env ?? {}
|
|
9248
|
+
};
|
|
9249
|
+
}
|
|
9250
|
+
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
9251
|
+
const binPath = await ensureNpmPackage({
|
|
9252
|
+
agentId: agent.id,
|
|
9253
|
+
version: agent.version ?? "current",
|
|
9254
|
+
packageSpec: npx.package,
|
|
9255
|
+
bin
|
|
9256
|
+
});
|
|
7633
9257
|
return {
|
|
7634
|
-
command:
|
|
7635
|
-
args:
|
|
9258
|
+
command: binPath,
|
|
9259
|
+
args: tail,
|
|
7636
9260
|
env: npx.env ?? {}
|
|
7637
9261
|
};
|
|
7638
9262
|
}
|
|
@@ -7667,12 +9291,8 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
7667
9291
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
7668
9292
|
}
|
|
7669
9293
|
|
|
7670
|
-
// src/core/session-manager.ts
|
|
7671
|
-
import * as fs8 from "fs/promises";
|
|
7672
|
-
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
7673
|
-
|
|
7674
9294
|
// src/core/agent-instance.ts
|
|
7675
|
-
import { spawn as
|
|
9295
|
+
import { spawn as spawn3 } from "child_process";
|
|
7676
9296
|
|
|
7677
9297
|
// src/acp/framing.ts
|
|
7678
9298
|
init_types();
|
|
@@ -7753,17 +9373,22 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
7753
9373
|
|
|
7754
9374
|
// src/core/agent-instance.ts
|
|
7755
9375
|
init_connection();
|
|
9376
|
+
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
7756
9377
|
var AgentInstance = class _AgentInstance {
|
|
7757
9378
|
agentId;
|
|
7758
9379
|
cwd;
|
|
7759
9380
|
connection;
|
|
7760
9381
|
child;
|
|
7761
9382
|
exited = false;
|
|
9383
|
+
killed = false;
|
|
9384
|
+
stderrTail = "";
|
|
9385
|
+
stderrTailBytes;
|
|
7762
9386
|
exitHandlers = [];
|
|
7763
9387
|
constructor(opts, child) {
|
|
7764
9388
|
this.agentId = opts.agentId;
|
|
7765
9389
|
this.cwd = opts.cwd;
|
|
7766
9390
|
this.child = child;
|
|
9391
|
+
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
7767
9392
|
if (!child.stdout || !child.stdin) {
|
|
7768
9393
|
throw new Error("agent subprocess missing stdio");
|
|
7769
9394
|
}
|
|
@@ -7771,22 +9396,36 @@ var AgentInstance = class _AgentInstance {
|
|
|
7771
9396
|
this.connection = new JsonRpcConnection(stream);
|
|
7772
9397
|
child.stderr?.setEncoding("utf8");
|
|
7773
9398
|
child.stderr?.on("data", (chunk) => {
|
|
9399
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
|
|
7774
9400
|
process.stderr.write(`[${opts.agentId}] ${chunk}`);
|
|
7775
9401
|
});
|
|
9402
|
+
child.on("error", (err) => {
|
|
9403
|
+
const msg = this.formatFailure(err.message);
|
|
9404
|
+
this.connection.fail(new Error(msg));
|
|
9405
|
+
});
|
|
7776
9406
|
child.on("exit", (code, signal) => {
|
|
7777
9407
|
this.exited = true;
|
|
9408
|
+
if (!this.killed) {
|
|
9409
|
+
const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
|
|
9410
|
+
this.connection.fail(new Error(this.formatFailure(reason)));
|
|
9411
|
+
}
|
|
7778
9412
|
for (const handler of this.exitHandlers) {
|
|
7779
9413
|
handler(code, signal);
|
|
7780
9414
|
}
|
|
7781
9415
|
});
|
|
7782
9416
|
}
|
|
9417
|
+
formatFailure(reason) {
|
|
9418
|
+
const tail = this.stderrTail.trim();
|
|
9419
|
+
return tail ? `${reason}
|
|
9420
|
+
stderr: ${tail}` : reason;
|
|
9421
|
+
}
|
|
7783
9422
|
static spawn(opts) {
|
|
7784
9423
|
const env = {
|
|
7785
9424
|
...process.env,
|
|
7786
9425
|
...opts.plan.env,
|
|
7787
9426
|
...opts.extraEnv ?? {}
|
|
7788
9427
|
};
|
|
7789
|
-
const child =
|
|
9428
|
+
const child = spawn3(opts.plan.command, opts.plan.args, {
|
|
7790
9429
|
cwd: opts.cwd,
|
|
7791
9430
|
env,
|
|
7792
9431
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7803,196 +9442,33 @@ var AgentInstance = class _AgentInstance {
|
|
|
7803
9442
|
if (this.exited) {
|
|
7804
9443
|
return;
|
|
7805
9444
|
}
|
|
9445
|
+
this.killed = true;
|
|
7806
9446
|
await this.connection.close().catch(() => void 0);
|
|
7807
9447
|
this.child.kill(signal);
|
|
7808
9448
|
}
|
|
7809
9449
|
};
|
|
7810
9450
|
|
|
7811
9451
|
// src/core/session-manager.ts
|
|
9452
|
+
import * as fs9 from "fs/promises";
|
|
9453
|
+
import * as os2 from "os";
|
|
9454
|
+
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
7812
9455
|
init_session();
|
|
7813
|
-
|
|
7814
|
-
// src/core/session-store.ts
|
|
7815
|
-
init_paths();
|
|
7816
|
-
import * as fs5 from "fs/promises";
|
|
7817
|
-
import * as path3 from "path";
|
|
7818
|
-
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
7819
|
-
import { z as z4 } from "zod";
|
|
7820
|
-
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7821
|
-
var generateRawId = customAlphabet2(HYDRA_ID_ALPHABET2, 16);
|
|
7822
|
-
var HYDRA_LINEAGE_PREFIX = "hydra_lineage_";
|
|
7823
|
-
function generateLineageId() {
|
|
7824
|
-
return `${HYDRA_LINEAGE_PREFIX}${generateRawId()}`;
|
|
7825
|
-
}
|
|
7826
|
-
var PersistedAgentCommand = z4.object({
|
|
7827
|
-
name: z4.string(),
|
|
7828
|
-
description: z4.string().optional()
|
|
7829
|
-
});
|
|
7830
|
-
var PersistedUsage = z4.object({
|
|
7831
|
-
used: z4.number().optional(),
|
|
7832
|
-
size: z4.number().optional(),
|
|
7833
|
-
costAmount: z4.number().optional(),
|
|
7834
|
-
costCurrency: z4.string().optional()
|
|
7835
|
-
});
|
|
7836
|
-
var SessionRecord = z4.object({
|
|
7837
|
-
version: z4.literal(1),
|
|
7838
|
-
sessionId: z4.string(),
|
|
7839
|
-
// Optional for back-compat with records written before this field
|
|
7840
|
-
// existed; mergeForPersistence generates one on next write so any
|
|
7841
|
-
// touched session converges to having a lineageId. A record that
|
|
7842
|
-
// never gets written again (truly cold and untouched) just won't
|
|
7843
|
-
// participate in lineage-based dedup, which is correct — it was
|
|
7844
|
-
// never exported, so no incoming bundle can claim its lineage.
|
|
7845
|
-
lineageId: z4.string().optional(),
|
|
7846
|
-
upstreamSessionId: z4.string(),
|
|
7847
|
-
// When non-empty, marks a session that was created by import and is
|
|
7848
|
-
// waiting for its first attach to bootstrap a fresh upstream agent
|
|
7849
|
-
// and replay the imported history as a takeover transcript. The
|
|
7850
|
-
// origin's local id at export time, kept for debuggability and as a
|
|
7851
|
-
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
7852
|
-
importedFromSessionId: z4.string().optional(),
|
|
7853
|
-
agentId: z4.string(),
|
|
7854
|
-
cwd: z4.string(),
|
|
7855
|
-
title: z4.string().optional(),
|
|
7856
|
-
agentArgs: z4.array(z4.string()).optional(),
|
|
7857
|
-
// Snapshot of "what is currently true about this session" carried in
|
|
7858
|
-
// meta.json so a late-attaching or cold-resurrected client can be
|
|
7859
|
-
// told via the attach response _meta without depending on history
|
|
7860
|
-
// replay of a snapshot-shaped notification.
|
|
7861
|
-
currentModel: z4.string().optional(),
|
|
7862
|
-
currentMode: z4.string().optional(),
|
|
7863
|
-
currentUsage: PersistedUsage.optional(),
|
|
7864
|
-
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
7865
|
-
createdAt: z4.string(),
|
|
7866
|
-
updatedAt: z4.string()
|
|
7867
|
-
});
|
|
7868
|
-
var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
7869
|
-
function assertSafeId(id) {
|
|
7870
|
-
if (!SESSION_ID_PATTERN.test(id)) {
|
|
7871
|
-
throw new Error(`unsafe session id: ${id}`);
|
|
7872
|
-
}
|
|
7873
|
-
}
|
|
7874
|
-
var SessionStore = class {
|
|
7875
|
-
async write(record) {
|
|
7876
|
-
assertSafeId(record.sessionId);
|
|
7877
|
-
await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
7878
|
-
const full = { version: 1, ...record };
|
|
7879
|
-
await fs5.writeFile(
|
|
7880
|
-
paths.sessionFile(record.sessionId),
|
|
7881
|
-
JSON.stringify(full, null, 2) + "\n",
|
|
7882
|
-
{ encoding: "utf8", mode: 384 }
|
|
7883
|
-
);
|
|
7884
|
-
}
|
|
7885
|
-
async read(sessionId) {
|
|
7886
|
-
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
7887
|
-
return void 0;
|
|
7888
|
-
}
|
|
7889
|
-
let raw;
|
|
7890
|
-
try {
|
|
7891
|
-
raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
|
|
7892
|
-
} catch (err) {
|
|
7893
|
-
const e = err;
|
|
7894
|
-
if (e.code === "ENOENT") {
|
|
7895
|
-
return void 0;
|
|
7896
|
-
}
|
|
7897
|
-
throw err;
|
|
7898
|
-
}
|
|
7899
|
-
try {
|
|
7900
|
-
return SessionRecord.parse(JSON.parse(raw));
|
|
7901
|
-
} catch {
|
|
7902
|
-
return void 0;
|
|
7903
|
-
}
|
|
7904
|
-
}
|
|
7905
|
-
async delete(sessionId) {
|
|
7906
|
-
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
7907
|
-
return;
|
|
7908
|
-
}
|
|
7909
|
-
try {
|
|
7910
|
-
await fs5.unlink(paths.sessionFile(sessionId));
|
|
7911
|
-
} catch (err) {
|
|
7912
|
-
const e = err;
|
|
7913
|
-
if (e.code !== "ENOENT") {
|
|
7914
|
-
throw err;
|
|
7915
|
-
}
|
|
7916
|
-
}
|
|
7917
|
-
try {
|
|
7918
|
-
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
7919
|
-
} catch (err) {
|
|
7920
|
-
const e = err;
|
|
7921
|
-
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
7922
|
-
throw err;
|
|
7923
|
-
}
|
|
7924
|
-
}
|
|
7925
|
-
}
|
|
7926
|
-
// Find a persisted session by lineageId. Used by SessionManager.import
|
|
7927
|
-
// to detect bundles that have already been imported (lineageId match)
|
|
7928
|
-
// so we can either error out or, with replace:true, overwrite.
|
|
7929
|
-
// Returns undefined if no record has that lineageId. Records that
|
|
7930
|
-
// pre-date the lineageId field simply don't match — which is
|
|
7931
|
-
// correct: they were never exported, so no incoming bundle can
|
|
7932
|
-
// legitimately claim their lineage.
|
|
7933
|
-
async findByLineageId(lineageId) {
|
|
7934
|
-
if (lineageId.length === 0) {
|
|
7935
|
-
return void 0;
|
|
7936
|
-
}
|
|
7937
|
-
const all = await this.list().catch(() => []);
|
|
7938
|
-
for (const record of all) {
|
|
7939
|
-
if (record.lineageId === lineageId) {
|
|
7940
|
-
return record;
|
|
7941
|
-
}
|
|
7942
|
-
}
|
|
7943
|
-
return void 0;
|
|
7944
|
-
}
|
|
7945
|
-
async list() {
|
|
7946
|
-
let entries;
|
|
7947
|
-
try {
|
|
7948
|
-
entries = await fs5.readdir(paths.sessionsDir());
|
|
7949
|
-
} catch (err) {
|
|
7950
|
-
const e = err;
|
|
7951
|
-
if (e.code === "ENOENT") {
|
|
7952
|
-
return [];
|
|
7953
|
-
}
|
|
7954
|
-
throw err;
|
|
7955
|
-
}
|
|
7956
|
-
const records = [];
|
|
7957
|
-
for (const entry of entries) {
|
|
7958
|
-
const record = await this.read(entry);
|
|
7959
|
-
if (record) {
|
|
7960
|
-
records.push(record);
|
|
7961
|
-
}
|
|
7962
|
-
}
|
|
7963
|
-
return records;
|
|
7964
|
-
}
|
|
7965
|
-
};
|
|
7966
|
-
function recordFromMemorySession(args) {
|
|
7967
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7968
|
-
return {
|
|
7969
|
-
sessionId: args.sessionId,
|
|
7970
|
-
lineageId: args.lineageId,
|
|
7971
|
-
upstreamSessionId: args.upstreamSessionId,
|
|
7972
|
-
importedFromSessionId: args.importedFromSessionId,
|
|
7973
|
-
agentId: args.agentId,
|
|
7974
|
-
cwd: args.cwd,
|
|
7975
|
-
title: args.title,
|
|
7976
|
-
agentArgs: args.agentArgs,
|
|
7977
|
-
currentModel: args.currentModel,
|
|
7978
|
-
currentMode: args.currentMode,
|
|
7979
|
-
currentUsage: args.currentUsage,
|
|
7980
|
-
agentCommands: args.agentCommands,
|
|
7981
|
-
createdAt: args.createdAt ?? now,
|
|
7982
|
-
updatedAt: args.updatedAt ?? now
|
|
7983
|
-
};
|
|
7984
|
-
}
|
|
9456
|
+
init_session_store();
|
|
7985
9457
|
|
|
7986
9458
|
// src/core/history-store.ts
|
|
7987
9459
|
init_paths();
|
|
7988
9460
|
import * as fs6 from "fs/promises";
|
|
7989
9461
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
7990
|
-
var
|
|
9462
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
7991
9463
|
var HistoryStore = class {
|
|
7992
9464
|
// Serialize writes per session id so appends and rewrites don't
|
|
7993
9465
|
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
7994
9466
|
// failed append doesn't poison every subsequent write.
|
|
7995
9467
|
writeQueues = /* @__PURE__ */ new Map();
|
|
9468
|
+
maxEntries;
|
|
9469
|
+
constructor(options = {}) {
|
|
9470
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
9471
|
+
}
|
|
7996
9472
|
async append(sessionId, entry) {
|
|
7997
9473
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
7998
9474
|
return;
|
|
@@ -8094,8 +9570,8 @@ var HistoryStore = class {
|
|
|
8094
9570
|
recordedAt: obj.recordedAt
|
|
8095
9571
|
});
|
|
8096
9572
|
}
|
|
8097
|
-
if (out.length >
|
|
8098
|
-
return out.slice(-
|
|
9573
|
+
if (out.length > this.maxEntries) {
|
|
9574
|
+
return out.slice(-this.maxEntries);
|
|
8099
9575
|
}
|
|
8100
9576
|
return out;
|
|
8101
9577
|
}
|
|
@@ -8140,6 +9616,7 @@ var HistoryStore = class {
|
|
|
8140
9616
|
init_paths();
|
|
8141
9617
|
init_history();
|
|
8142
9618
|
init_types();
|
|
9619
|
+
init_hydra_version();
|
|
8143
9620
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
8144
9621
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
8145
9622
|
var SessionManager = class {
|
|
@@ -8147,7 +9624,8 @@ var SessionManager = class {
|
|
|
8147
9624
|
this.registry = registry;
|
|
8148
9625
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
8149
9626
|
this.store = store ?? new SessionStore();
|
|
8150
|
-
this.
|
|
9627
|
+
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
9628
|
+
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
8151
9629
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
8152
9630
|
this.defaultModels = options.defaultModels ?? {};
|
|
8153
9631
|
}
|
|
@@ -8159,6 +9637,7 @@ var SessionManager = class {
|
|
|
8159
9637
|
histories;
|
|
8160
9638
|
idleTimeoutMs;
|
|
8161
9639
|
defaultModels;
|
|
9640
|
+
sessionHistoryMaxEntries;
|
|
8162
9641
|
// Serialize meta.json read-modify-write operations per session id so
|
|
8163
9642
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
8164
9643
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -8182,6 +9661,7 @@ var SessionManager = class {
|
|
|
8182
9661
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
8183
9662
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
8184
9663
|
historyStore: this.histories,
|
|
9664
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
8185
9665
|
currentModel: fresh.initialModel
|
|
8186
9666
|
});
|
|
8187
9667
|
await this.attachManagerHooks(session);
|
|
@@ -8233,11 +9713,16 @@ var SessionManager = class {
|
|
|
8233
9713
|
cwd: params.cwd,
|
|
8234
9714
|
plan
|
|
8235
9715
|
});
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
9716
|
+
try {
|
|
9717
|
+
await agent.connection.request("initialize", {
|
|
9718
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
9719
|
+
clientCapabilities: {},
|
|
9720
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
9721
|
+
});
|
|
9722
|
+
} catch (err) {
|
|
9723
|
+
await agent.kill().catch(() => void 0);
|
|
9724
|
+
throw err;
|
|
9725
|
+
}
|
|
8241
9726
|
let loadResult;
|
|
8242
9727
|
try {
|
|
8243
9728
|
loadResult = await agent.connection.request(
|
|
@@ -8249,10 +9734,12 @@ var SessionManager = class {
|
|
|
8249
9734
|
}
|
|
8250
9735
|
);
|
|
8251
9736
|
} catch (err) {
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
9737
|
+
process.stderr.write(
|
|
9738
|
+
`session/load failed for upstream ${params.upstreamSessionId} on ${params.agentId} (${err.message}); recovering via import-reseed
|
|
9739
|
+
`
|
|
8255
9740
|
);
|
|
9741
|
+
await agent.kill().catch(() => void 0);
|
|
9742
|
+
return this.doResurrectFromImport(params);
|
|
8256
9743
|
}
|
|
8257
9744
|
const session = new Session({
|
|
8258
9745
|
sessionId: params.hydraSessionId,
|
|
@@ -8266,6 +9753,7 @@ var SessionManager = class {
|
|
|
8266
9753
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
8267
9754
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
8268
9755
|
historyStore: this.histories,
|
|
9756
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
8269
9757
|
// Prefer what we previously stored from a current_model_update; if
|
|
8270
9758
|
// we never captured one (e.g. old opencode sessions on disk before
|
|
8271
9759
|
// this fix), fall back to the model the agent ships in its
|
|
@@ -8292,15 +9780,16 @@ var SessionManager = class {
|
|
|
8292
9780
|
// so subsequent resurrects of this session use the normal session/load
|
|
8293
9781
|
// path.
|
|
8294
9782
|
async doResurrectFromImport(params) {
|
|
9783
|
+
const cwd = await this.resolveImportCwd(params.cwd);
|
|
8295
9784
|
const fresh = await this.bootstrapAgent({
|
|
8296
9785
|
agentId: params.agentId,
|
|
8297
|
-
cwd
|
|
9786
|
+
cwd,
|
|
8298
9787
|
agentArgs: params.agentArgs,
|
|
8299
9788
|
mcpServers: []
|
|
8300
9789
|
});
|
|
8301
9790
|
const session = new Session({
|
|
8302
9791
|
sessionId: params.hydraSessionId,
|
|
8303
|
-
cwd
|
|
9792
|
+
cwd,
|
|
8304
9793
|
agentId: params.agentId,
|
|
8305
9794
|
agent: fresh.agent,
|
|
8306
9795
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
@@ -8310,6 +9799,7 @@ var SessionManager = class {
|
|
|
8310
9799
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
8311
9800
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
8312
9801
|
historyStore: this.histories,
|
|
9802
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
8313
9803
|
// Prefer the stored value (set by a previous current_model_update);
|
|
8314
9804
|
// fall back to whatever the agent ships in its session/new response.
|
|
8315
9805
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
@@ -8323,6 +9813,16 @@ var SessionManager = class {
|
|
|
8323
9813
|
void session.seedFromImport().catch(() => void 0);
|
|
8324
9814
|
return session;
|
|
8325
9815
|
}
|
|
9816
|
+
async resolveImportCwd(cwd) {
|
|
9817
|
+
try {
|
|
9818
|
+
const stat4 = await fs9.stat(cwd);
|
|
9819
|
+
if (stat4.isDirectory()) {
|
|
9820
|
+
return cwd;
|
|
9821
|
+
}
|
|
9822
|
+
} catch {
|
|
9823
|
+
}
|
|
9824
|
+
return os2.homedir();
|
|
9825
|
+
}
|
|
8326
9826
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
8327
9827
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
8328
9828
|
// go through the same env / capabilities / error-handling.
|
|
@@ -8343,9 +9843,9 @@ var SessionManager = class {
|
|
|
8343
9843
|
});
|
|
8344
9844
|
try {
|
|
8345
9845
|
await agent.connection.request("initialize", {
|
|
8346
|
-
protocolVersion:
|
|
9846
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
8347
9847
|
clientCapabilities: {},
|
|
8348
|
-
clientInfo: { name: "hydra", version:
|
|
9848
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
8349
9849
|
});
|
|
8350
9850
|
const newResult = await agent.connection.request(
|
|
8351
9851
|
"session/new",
|
|
@@ -8625,7 +10125,8 @@ var SessionManager = class {
|
|
|
8625
10125
|
await this.writeImportedRecord({
|
|
8626
10126
|
sessionId: existing.sessionId,
|
|
8627
10127
|
bundle,
|
|
8628
|
-
preservedCreatedAt: existing.createdAt
|
|
10128
|
+
preservedCreatedAt: existing.createdAt,
|
|
10129
|
+
cwd: opts.cwd
|
|
8629
10130
|
});
|
|
8630
10131
|
return {
|
|
8631
10132
|
sessionId: existing.sessionId,
|
|
@@ -8634,7 +10135,11 @@ var SessionManager = class {
|
|
|
8634
10135
|
};
|
|
8635
10136
|
}
|
|
8636
10137
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
8637
|
-
await this.writeImportedRecord({
|
|
10138
|
+
await this.writeImportedRecord({
|
|
10139
|
+
sessionId: newId,
|
|
10140
|
+
bundle,
|
|
10141
|
+
cwd: opts.cwd
|
|
10142
|
+
});
|
|
8638
10143
|
return {
|
|
8639
10144
|
sessionId: newId,
|
|
8640
10145
|
importedFromSessionId: bundle.session.sessionId,
|
|
@@ -8664,7 +10169,7 @@ var SessionManager = class {
|
|
|
8664
10169
|
upstreamSessionId: "",
|
|
8665
10170
|
importedFromSessionId: args.bundle.session.sessionId,
|
|
8666
10171
|
agentId: args.bundle.session.agentId,
|
|
8667
|
-
cwd: args.bundle.session.cwd,
|
|
10172
|
+
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
8668
10173
|
title: args.bundle.session.title,
|
|
8669
10174
|
currentModel: args.bundle.session.currentModel,
|
|
8670
10175
|
currentMode: args.bundle.session.currentMode,
|
|
@@ -8857,7 +10362,7 @@ function asString(value) {
|
|
|
8857
10362
|
}
|
|
8858
10363
|
async function loadPromptHistorySafely(sessionId) {
|
|
8859
10364
|
try {
|
|
8860
|
-
const raw = await
|
|
10365
|
+
const raw = await fs9.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
8861
10366
|
const out = [];
|
|
8862
10367
|
for (const line of raw.split("\n")) {
|
|
8863
10368
|
if (line.length === 0) {
|
|
@@ -8878,7 +10383,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
8878
10383
|
}
|
|
8879
10384
|
async function historyMtimeIso(sessionId) {
|
|
8880
10385
|
try {
|
|
8881
|
-
const st = await
|
|
10386
|
+
const st = await fs9.stat(paths.historyFile(sessionId));
|
|
8882
10387
|
return new Date(st.mtimeMs).toISOString();
|
|
8883
10388
|
} catch {
|
|
8884
10389
|
return void 0;
|
|
@@ -8887,10 +10392,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
8887
10392
|
|
|
8888
10393
|
// src/core/extensions.ts
|
|
8889
10394
|
init_paths();
|
|
8890
|
-
import { spawn as
|
|
8891
|
-
import * as
|
|
8892
|
-
import * as
|
|
8893
|
-
import * as
|
|
10395
|
+
import { spawn as spawn4 } from "child_process";
|
|
10396
|
+
import * as fs10 from "fs";
|
|
10397
|
+
import * as fsp3 from "fs/promises";
|
|
10398
|
+
import * as path7 from "path";
|
|
8894
10399
|
var RESTART_BASE_MS = 1e3;
|
|
8895
10400
|
var RESTART_CAP_MS = 6e4;
|
|
8896
10401
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -8911,7 +10416,7 @@ var ExtensionManager = class {
|
|
|
8911
10416
|
if (!this.context) {
|
|
8912
10417
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
8913
10418
|
}
|
|
8914
|
-
await
|
|
10419
|
+
await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
|
|
8915
10420
|
await this.reapOrphans();
|
|
8916
10421
|
for (const entry of this.entries.values()) {
|
|
8917
10422
|
if (!entry.config.enabled) {
|
|
@@ -9120,7 +10625,7 @@ var ExtensionManager = class {
|
|
|
9120
10625
|
async reapOrphans() {
|
|
9121
10626
|
let entries;
|
|
9122
10627
|
try {
|
|
9123
|
-
entries = await
|
|
10628
|
+
entries = await fsp3.readdir(paths.extensionsDir());
|
|
9124
10629
|
} catch (err) {
|
|
9125
10630
|
const e = err;
|
|
9126
10631
|
if (e.code === "ENOENT") {
|
|
@@ -9132,10 +10637,10 @@ var ExtensionManager = class {
|
|
|
9132
10637
|
if (!entry.endsWith(".pid")) {
|
|
9133
10638
|
continue;
|
|
9134
10639
|
}
|
|
9135
|
-
const pidPath =
|
|
10640
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
9136
10641
|
let pid;
|
|
9137
10642
|
try {
|
|
9138
|
-
const raw = await
|
|
10643
|
+
const raw = await fsp3.readFile(pidPath, "utf8");
|
|
9139
10644
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
9140
10645
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
9141
10646
|
pid = parsed;
|
|
@@ -9158,7 +10663,7 @@ var ExtensionManager = class {
|
|
|
9158
10663
|
}
|
|
9159
10664
|
}
|
|
9160
10665
|
}
|
|
9161
|
-
await
|
|
10666
|
+
await fsp3.unlink(pidPath).catch(() => void 0);
|
|
9162
10667
|
}
|
|
9163
10668
|
}
|
|
9164
10669
|
spawn(entry, attempt) {
|
|
@@ -9171,7 +10676,7 @@ var ExtensionManager = class {
|
|
|
9171
10676
|
}
|
|
9172
10677
|
const ext = entry.config;
|
|
9173
10678
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
9174
|
-
const logStream =
|
|
10679
|
+
const logStream = fs10.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
9175
10680
|
flags: "a"
|
|
9176
10681
|
});
|
|
9177
10682
|
logStream.write(
|
|
@@ -9199,7 +10704,7 @@ var ExtensionManager = class {
|
|
|
9199
10704
|
const args = [...baseArgs, ...ext.args];
|
|
9200
10705
|
let child;
|
|
9201
10706
|
try {
|
|
9202
|
-
child =
|
|
10707
|
+
child = spawn4(cmd, args, {
|
|
9203
10708
|
env,
|
|
9204
10709
|
stdio: ["ignore", "pipe", "pipe"],
|
|
9205
10710
|
detached: false
|
|
@@ -9221,7 +10726,7 @@ var ExtensionManager = class {
|
|
|
9221
10726
|
}
|
|
9222
10727
|
if (typeof child.pid === "number") {
|
|
9223
10728
|
try {
|
|
9224
|
-
|
|
10729
|
+
fs10.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
9225
10730
|
`, {
|
|
9226
10731
|
encoding: "utf8",
|
|
9227
10732
|
mode: 384
|
|
@@ -9246,7 +10751,7 @@ var ExtensionManager = class {
|
|
|
9246
10751
|
});
|
|
9247
10752
|
child.on("exit", (code, signal) => {
|
|
9248
10753
|
try {
|
|
9249
|
-
|
|
10754
|
+
fs10.unlinkSync(paths.extensionPidFile(ext.name));
|
|
9250
10755
|
} catch {
|
|
9251
10756
|
}
|
|
9252
10757
|
logStream.write(
|
|
@@ -9304,6 +10809,7 @@ function withCode2(err, code) {
|
|
|
9304
10809
|
|
|
9305
10810
|
// src/daemon/server.ts
|
|
9306
10811
|
init_paths();
|
|
10812
|
+
init_hydra_version();
|
|
9307
10813
|
|
|
9308
10814
|
// src/daemon/auth.ts
|
|
9309
10815
|
var BEARER_PREFIX = "Bearer ";
|
|
@@ -9359,78 +10865,10 @@ function constantTimeEqual(a, b) {
|
|
|
9359
10865
|
|
|
9360
10866
|
// src/daemon/routes/sessions.ts
|
|
9361
10867
|
init_config();
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
// src/core/bundle.ts
|
|
9365
|
-
import { z as z5 } from "zod";
|
|
9366
|
-
var HistoryEntrySchema = z5.object({
|
|
9367
|
-
method: z5.string(),
|
|
9368
|
-
params: z5.unknown(),
|
|
9369
|
-
recordedAt: z5.number()
|
|
9370
|
-
});
|
|
9371
|
-
var BundleSession = z5.object({
|
|
9372
|
-
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
9373
|
-
// the local namespace; lineageId is what survives across hops).
|
|
9374
|
-
sessionId: z5.string(),
|
|
9375
|
-
// Required on bundles — the export path backfills if the source
|
|
9376
|
-
// record was written before lineageId existed.
|
|
9377
|
-
lineageId: z5.string(),
|
|
9378
|
-
agentId: z5.string(),
|
|
9379
|
-
cwd: z5.string(),
|
|
9380
|
-
title: z5.string().optional(),
|
|
9381
|
-
currentModel: z5.string().optional(),
|
|
9382
|
-
currentMode: z5.string().optional(),
|
|
9383
|
-
currentUsage: PersistedUsage.optional(),
|
|
9384
|
-
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
9385
|
-
createdAt: z5.string(),
|
|
9386
|
-
updatedAt: z5.string()
|
|
9387
|
-
});
|
|
9388
|
-
var Bundle = z5.object({
|
|
9389
|
-
version: z5.literal(1),
|
|
9390
|
-
exportedAt: z5.string(),
|
|
9391
|
-
exportedFrom: z5.object({
|
|
9392
|
-
hydraVersion: z5.string(),
|
|
9393
|
-
machine: z5.string()
|
|
9394
|
-
}),
|
|
9395
|
-
session: BundleSession,
|
|
9396
|
-
history: z5.array(HistoryEntrySchema),
|
|
9397
|
-
promptHistory: z5.array(z5.string()).optional()
|
|
9398
|
-
});
|
|
9399
|
-
function encodeBundle(params) {
|
|
9400
|
-
const bundle = {
|
|
9401
|
-
version: 1,
|
|
9402
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9403
|
-
exportedFrom: {
|
|
9404
|
-
hydraVersion: params.hydraVersion,
|
|
9405
|
-
machine: params.machine
|
|
9406
|
-
},
|
|
9407
|
-
session: {
|
|
9408
|
-
sessionId: params.record.sessionId,
|
|
9409
|
-
lineageId: params.record.lineageId,
|
|
9410
|
-
agentId: params.record.agentId,
|
|
9411
|
-
cwd: params.record.cwd,
|
|
9412
|
-
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
9413
|
-
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
9414
|
-
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
9415
|
-
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
9416
|
-
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
9417
|
-
createdAt: params.record.createdAt,
|
|
9418
|
-
updatedAt: params.record.updatedAt
|
|
9419
|
-
},
|
|
9420
|
-
history: params.history
|
|
9421
|
-
};
|
|
9422
|
-
if (params.promptHistory !== void 0) {
|
|
9423
|
-
bundle.promptHistory = params.promptHistory;
|
|
9424
|
-
}
|
|
9425
|
-
return bundle;
|
|
9426
|
-
}
|
|
9427
|
-
function decodeBundle(raw) {
|
|
9428
|
-
return Bundle.parse(raw);
|
|
9429
|
-
}
|
|
9430
|
-
|
|
9431
|
-
// src/daemon/routes/sessions.ts
|
|
10868
|
+
init_bundle();
|
|
9432
10869
|
init_types();
|
|
9433
|
-
|
|
10870
|
+
init_hydra_version();
|
|
10871
|
+
import * as os3 from "os";
|
|
9434
10872
|
function registerSessionRoutes(app, manager, defaults) {
|
|
9435
10873
|
app.get("/v1/sessions", async (request) => {
|
|
9436
10874
|
const query = request.query;
|
|
@@ -9501,12 +10939,12 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9501
10939
|
history: exported.history,
|
|
9502
10940
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
9503
10941
|
hydraVersion: HYDRA_VERSION,
|
|
9504
|
-
machine:
|
|
10942
|
+
machine: os3.hostname()
|
|
9505
10943
|
});
|
|
9506
10944
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
9507
10945
|
reply.header(
|
|
9508
10946
|
"Content-Disposition",
|
|
9509
|
-
`attachment; filename="
|
|
10947
|
+
`attachment; filename="${id}-${stamp}.hydra"`
|
|
9510
10948
|
);
|
|
9511
10949
|
reply.code(200).send(bundle);
|
|
9512
10950
|
});
|
|
@@ -9516,6 +10954,14 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9516
10954
|
reply.code(400).send({ error: "missing bundle" });
|
|
9517
10955
|
return;
|
|
9518
10956
|
}
|
|
10957
|
+
let cwdOverride;
|
|
10958
|
+
if (body.cwd !== void 0) {
|
|
10959
|
+
if (typeof body.cwd !== "string" || body.cwd.length === 0) {
|
|
10960
|
+
reply.code(400).send({ error: "cwd must be a non-empty string" });
|
|
10961
|
+
return;
|
|
10962
|
+
}
|
|
10963
|
+
cwdOverride = body.cwd;
|
|
10964
|
+
}
|
|
9519
10965
|
let bundle;
|
|
9520
10966
|
try {
|
|
9521
10967
|
bundle = decodeBundle(body.bundle);
|
|
@@ -9528,7 +10974,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9528
10974
|
}
|
|
9529
10975
|
try {
|
|
9530
10976
|
const result = await manager.importBundle(bundle, {
|
|
9531
|
-
replace: body.replace === true
|
|
10977
|
+
replace: body.replace === true,
|
|
10978
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
9532
10979
|
});
|
|
9533
10980
|
reply.code(201).send(result);
|
|
9534
10981
|
} catch (err) {
|
|
@@ -9769,8 +11216,7 @@ init_connection();
|
|
|
9769
11216
|
init_ws_stream();
|
|
9770
11217
|
init_types();
|
|
9771
11218
|
import { nanoid as nanoid2 } from "nanoid";
|
|
9772
|
-
|
|
9773
|
-
var HYDRA_PROTOCOL_VERSION = 1;
|
|
11219
|
+
init_hydra_version();
|
|
9774
11220
|
function registerAcpWsEndpoint(app, deps) {
|
|
9775
11221
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
9776
11222
|
const token = tokenFromUpgradeRequest({
|
|
@@ -9864,15 +11310,20 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9864
11310
|
connection,
|
|
9865
11311
|
session,
|
|
9866
11312
|
state,
|
|
9867
|
-
params.clientInfo
|
|
11313
|
+
params.clientInfo,
|
|
11314
|
+
params.clientId
|
|
11315
|
+
);
|
|
11316
|
+
const { entries: replay, appliedPolicy } = await session.attach(
|
|
11317
|
+
client,
|
|
11318
|
+
params.historyPolicy,
|
|
11319
|
+
{ afterMessageId: params.afterMessageId }
|
|
9868
11320
|
);
|
|
9869
|
-
const replay = await session.attach(client, params.historyPolicy);
|
|
9870
11321
|
state.attached.set(session.sessionId, {
|
|
9871
11322
|
sessionId: session.sessionId,
|
|
9872
11323
|
clientId: client.clientId
|
|
9873
11324
|
});
|
|
9874
11325
|
app.log.info(
|
|
9875
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} replayed=${replay.length}`
|
|
11326
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length}`
|
|
9876
11327
|
);
|
|
9877
11328
|
for (const note of replay) {
|
|
9878
11329
|
await connection.notify(note.method, note.params);
|
|
@@ -9880,6 +11331,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9880
11331
|
session.replayPendingPermissions(client);
|
|
9881
11332
|
return {
|
|
9882
11333
|
sessionId: session.sessionId,
|
|
11334
|
+
clientId: client.clientId,
|
|
11335
|
+
connectedClients: session.connectedClients(client.clientId),
|
|
11336
|
+
// appliedPolicy surfaces whether after_message fell back to full
|
|
11337
|
+
// (because afterMessageId wasn't found in history) — RFD #533
|
|
11338
|
+
// says the response.historyPolicy should reflect what actually
|
|
11339
|
+
// ran, not what was asked for.
|
|
11340
|
+
historyPolicy: appliedPolicy,
|
|
9883
11341
|
replayed: replay.length,
|
|
9884
11342
|
_meta: buildResponseMeta(session)
|
|
9885
11343
|
};
|
|
@@ -9895,7 +11353,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9895
11353
|
const session = deps.manager.get(params.sessionId);
|
|
9896
11354
|
session?.detach(att.clientId);
|
|
9897
11355
|
state.attached.delete(params.sessionId);
|
|
9898
|
-
return {
|
|
11356
|
+
return { sessionId: params.sessionId, status: "detached" };
|
|
9899
11357
|
});
|
|
9900
11358
|
connection.onRequest("session/list", async (raw) => {
|
|
9901
11359
|
const params = SessionListParams.parse(raw ?? {});
|
|
@@ -9968,7 +11426,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9968
11426
|
session = await deps.manager.resurrect(fromDisk);
|
|
9969
11427
|
}
|
|
9970
11428
|
const client = bindClientToSession(connection, session, state);
|
|
9971
|
-
const replay = await session.attach(client, "pending_only");
|
|
11429
|
+
const { entries: replay } = await session.attach(client, "pending_only");
|
|
9972
11430
|
state.attached.set(session.sessionId, {
|
|
9973
11431
|
sessionId: session.sessionId,
|
|
9974
11432
|
clientId: client.clientId
|
|
@@ -10033,8 +11491,8 @@ function buildResponseMeta(session) {
|
|
|
10033
11491
|
}
|
|
10034
11492
|
function buildInitializeResult() {
|
|
10035
11493
|
return {
|
|
10036
|
-
protocolVersion:
|
|
10037
|
-
agentInfo: { name: "hydra", version:
|
|
11494
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
11495
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION },
|
|
10038
11496
|
agentCapabilities: {
|
|
10039
11497
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
10040
11498
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -10062,25 +11520,24 @@ function buildInitializeResult() {
|
|
|
10062
11520
|
]
|
|
10063
11521
|
};
|
|
10064
11522
|
}
|
|
10065
|
-
function bindClientToSession(connection, session, state, clientInfo) {
|
|
11523
|
+
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
10066
11524
|
void state;
|
|
10067
11525
|
void session;
|
|
10068
11526
|
return {
|
|
10069
|
-
clientId: `cli_${nanoid2(8)}`,
|
|
11527
|
+
clientId: callerClientId ?? `cli_${nanoid2(8)}`,
|
|
10070
11528
|
connection,
|
|
10071
11529
|
clientInfo
|
|
10072
11530
|
};
|
|
10073
11531
|
}
|
|
10074
11532
|
|
|
10075
11533
|
// src/daemon/server.ts
|
|
10076
|
-
var HYDRA_VERSION3 = "0.1.0";
|
|
10077
11534
|
async function startDaemon(config) {
|
|
10078
11535
|
ensureLoopbackOrTls(config);
|
|
10079
11536
|
const httpsOptions = config.daemon.tls ? {
|
|
10080
|
-
key: await
|
|
10081
|
-
cert: await
|
|
11537
|
+
key: await fsp4.readFile(config.daemon.tls.key),
|
|
11538
|
+
cert: await fsp4.readFile(config.daemon.tls.cert)
|
|
10082
11539
|
} : void 0;
|
|
10083
|
-
await
|
|
11540
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
10084
11541
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
10085
11542
|
config.daemon.logLevel
|
|
10086
11543
|
);
|
|
@@ -10089,12 +11546,18 @@ async function startDaemon(config) {
|
|
|
10089
11546
|
level: config.daemon.logLevel,
|
|
10090
11547
|
stream: logStream
|
|
10091
11548
|
},
|
|
10092
|
-
https: httpsOptions ?? null
|
|
11549
|
+
https: httpsOptions ?? null,
|
|
11550
|
+
// Session bundles can be large (full history + tool output);
|
|
11551
|
+
// the 1MB Fastify default rejects ordinary imports.
|
|
11552
|
+
bodyLimit: 256 * 1024 * 1024
|
|
10093
11553
|
});
|
|
10094
11554
|
await app.register(websocketPlugin);
|
|
10095
11555
|
setBinaryInstallLogger((msg) => {
|
|
10096
11556
|
app.log.info(msg);
|
|
10097
11557
|
});
|
|
11558
|
+
setNpmInstallLogger((msg) => {
|
|
11559
|
+
app.log.info(msg);
|
|
11560
|
+
});
|
|
10098
11561
|
const auth = bearerAuth({ config });
|
|
10099
11562
|
app.addHook("onRequest", async (request, reply) => {
|
|
10100
11563
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -10106,12 +11569,14 @@ async function startDaemon(config) {
|
|
|
10106
11569
|
await auth(request, reply);
|
|
10107
11570
|
});
|
|
10108
11571
|
const registry = new Registry(config);
|
|
10109
|
-
const
|
|
11572
|
+
const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
|
|
11573
|
+
const manager = new SessionManager(registry, spawner, void 0, {
|
|
10110
11574
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
10111
|
-
defaultModels: config.defaultModels
|
|
11575
|
+
defaultModels: config.defaultModels,
|
|
11576
|
+
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
|
|
10112
11577
|
});
|
|
10113
11578
|
const extensions = new ExtensionManager(extensionList(config));
|
|
10114
|
-
registerHealthRoutes(app,
|
|
11579
|
+
registerHealthRoutes(app, HYDRA_VERSION);
|
|
10115
11580
|
registerSessionRoutes(app, manager, {
|
|
10116
11581
|
agentId: config.defaultAgent,
|
|
10117
11582
|
cwd: config.defaultCwd
|
|
@@ -10130,8 +11595,8 @@ async function startDaemon(config) {
|
|
|
10130
11595
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
10131
11596
|
const address = app.server.address();
|
|
10132
11597
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
10133
|
-
await
|
|
10134
|
-
await
|
|
11598
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
11599
|
+
await fsp4.writeFile(
|
|
10135
11600
|
paths.pidFile(),
|
|
10136
11601
|
JSON.stringify({
|
|
10137
11602
|
pid: process.pid,
|
|
@@ -10157,9 +11622,10 @@ async function startDaemon(config) {
|
|
|
10157
11622
|
await manager.closeAll();
|
|
10158
11623
|
await manager.flushMetaWrites();
|
|
10159
11624
|
setBinaryInstallLogger(null);
|
|
11625
|
+
setNpmInstallLogger(null);
|
|
10160
11626
|
await app.close();
|
|
10161
11627
|
try {
|
|
10162
|
-
|
|
11628
|
+
fs11.unlinkSync(paths.pidFile());
|
|
10163
11629
|
} catch {
|
|
10164
11630
|
}
|
|
10165
11631
|
try {
|
|
@@ -10198,13 +11664,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
10198
11664
|
init_daemon_bootstrap();
|
|
10199
11665
|
|
|
10200
11666
|
// src/cli/commands/log-tail.ts
|
|
10201
|
-
import * as
|
|
10202
|
-
import * as
|
|
11667
|
+
import * as fs12 from "fs";
|
|
11668
|
+
import * as fsp5 from "fs/promises";
|
|
10203
11669
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
10204
11670
|
const opts = parseLogTailFlags(argv);
|
|
10205
|
-
let
|
|
11671
|
+
let stat4;
|
|
10206
11672
|
try {
|
|
10207
|
-
|
|
11673
|
+
stat4 = await fsp5.stat(logPath);
|
|
10208
11674
|
} catch (err) {
|
|
10209
11675
|
const e = err;
|
|
10210
11676
|
if (e.code === "ENOENT") {
|
|
@@ -10215,14 +11681,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
10215
11681
|
}
|
|
10216
11682
|
throw err;
|
|
10217
11683
|
}
|
|
10218
|
-
let position = await printTail(logPath,
|
|
11684
|
+
let position = await printTail(logPath, stat4.size, opts.tail);
|
|
10219
11685
|
if (!opts.follow) {
|
|
10220
11686
|
return;
|
|
10221
11687
|
}
|
|
10222
11688
|
process.stdout.write(`-- following ${logPath} --
|
|
10223
11689
|
`);
|
|
10224
11690
|
let pending = false;
|
|
10225
|
-
const watcher =
|
|
11691
|
+
const watcher = fs12.watch(logPath, () => {
|
|
10226
11692
|
if (pending) {
|
|
10227
11693
|
return;
|
|
10228
11694
|
}
|
|
@@ -10230,14 +11696,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
10230
11696
|
setImmediate(async () => {
|
|
10231
11697
|
pending = false;
|
|
10232
11698
|
try {
|
|
10233
|
-
const s = await
|
|
11699
|
+
const s = await fsp5.stat(logPath);
|
|
10234
11700
|
if (s.size <= position) {
|
|
10235
11701
|
if (s.size < position) {
|
|
10236
11702
|
position = s.size;
|
|
10237
11703
|
}
|
|
10238
11704
|
return;
|
|
10239
11705
|
}
|
|
10240
|
-
const fd = await
|
|
11706
|
+
const fd = await fsp5.open(logPath, "r");
|
|
10241
11707
|
try {
|
|
10242
11708
|
const buf = Buffer.alloc(s.size - position);
|
|
10243
11709
|
await fd.read(buf, 0, buf.length, position);
|
|
@@ -10264,7 +11730,7 @@ async function printTail(logPath, fileSize, lines) {
|
|
|
10264
11730
|
return fileSize;
|
|
10265
11731
|
}
|
|
10266
11732
|
const CHUNK = 64 * 1024;
|
|
10267
|
-
const fd = await
|
|
11733
|
+
const fd = await fsp5.open(logPath, "r");
|
|
10268
11734
|
try {
|
|
10269
11735
|
let position = fileSize;
|
|
10270
11736
|
let collected = "";
|
|
@@ -10433,7 +11899,7 @@ async function runDaemonStatus() {
|
|
|
10433
11899
|
}
|
|
10434
11900
|
async function readPidFile() {
|
|
10435
11901
|
try {
|
|
10436
|
-
const raw = await
|
|
11902
|
+
const raw = await fsp6.readFile(paths.pidFile(), "utf8");
|
|
10437
11903
|
return JSON.parse(raw);
|
|
10438
11904
|
} catch (err) {
|
|
10439
11905
|
const e = err;
|
|
@@ -10458,7 +11924,7 @@ init_sessions();
|
|
|
10458
11924
|
// src/cli/commands/extensions.ts
|
|
10459
11925
|
init_config();
|
|
10460
11926
|
init_paths();
|
|
10461
|
-
import * as
|
|
11927
|
+
import * as fsp7 from "fs/promises";
|
|
10462
11928
|
init_sessions();
|
|
10463
11929
|
async function runExtensionsList() {
|
|
10464
11930
|
const config = await loadConfig();
|
|
@@ -10654,11 +12120,11 @@ async function runExtensionsRemove(name) {
|
|
|
10654
12120
|
}
|
|
10655
12121
|
}
|
|
10656
12122
|
async function readRawConfig() {
|
|
10657
|
-
const raw = await
|
|
12123
|
+
const raw = await fsp7.readFile(paths.config(), "utf8");
|
|
10658
12124
|
return JSON.parse(raw);
|
|
10659
12125
|
}
|
|
10660
12126
|
async function writeRawConfig(raw) {
|
|
10661
|
-
await
|
|
12127
|
+
await fsp7.writeFile(
|
|
10662
12128
|
paths.config(),
|
|
10663
12129
|
JSON.stringify(raw, null, 2) + "\n",
|
|
10664
12130
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -11001,10 +12467,22 @@ var SessionTracker = class {
|
|
|
11001
12467
|
contexts = /* @__PURE__ */ new Map();
|
|
11002
12468
|
pending = /* @__PURE__ */ new Map();
|
|
11003
12469
|
pendingPermissions = /* @__PURE__ */ new Map();
|
|
12470
|
+
// Secondary index — same entries as `pendingPermissions`, keyed by the
|
|
12471
|
+
// tool call id from the request_permission params. Used to correlate
|
|
12472
|
+
// the daemon's `session/update`/`permission_resolved` events back to the
|
|
12473
|
+
// pending downstream request, since per-recipient JSON-RPC ids are no
|
|
12474
|
+
// longer carried on the wire.
|
|
12475
|
+
pendingPermissionsByToolCall = /* @__PURE__ */ new Map();
|
|
12476
|
+
// Most recent messageId observed on a session/update from the daemon
|
|
12477
|
+
// (prompt_received / turn_complete), keyed by sessionId. Used by the
|
|
12478
|
+
// reconnect-replay path to send historyPolicy:"after_message" with
|
|
12479
|
+
// afterMessageId so the daemon only replays the delta we missed.
|
|
12480
|
+
lastMessageIds = /* @__PURE__ */ new Map();
|
|
11004
12481
|
observeFromClient(msg) {
|
|
11005
12482
|
if (isResponse2(msg)) {
|
|
11006
|
-
|
|
11007
|
-
|
|
12483
|
+
const existing = this.pendingPermissions.get(msg.id);
|
|
12484
|
+
if (existing) {
|
|
12485
|
+
this.deletePendingPermission(existing);
|
|
11008
12486
|
}
|
|
11009
12487
|
return;
|
|
11010
12488
|
}
|
|
@@ -11031,16 +12509,34 @@ var SessionTracker = class {
|
|
|
11031
12509
|
}
|
|
11032
12510
|
}
|
|
11033
12511
|
observeFromServer(msg) {
|
|
12512
|
+
if (!isRequest(msg) && !isResponse2(msg) && "method" in msg) {
|
|
12513
|
+
if (msg.method === "session/update") {
|
|
12514
|
+
const params = msg.params ?? {};
|
|
12515
|
+
const sessionId2 = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
12516
|
+
const messageId = typeof params.update?.messageId === "string" ? params.update.messageId : void 0;
|
|
12517
|
+
if (sessionId2 && messageId) {
|
|
12518
|
+
this.lastMessageIds.set(sessionId2, messageId);
|
|
12519
|
+
}
|
|
12520
|
+
}
|
|
12521
|
+
return;
|
|
12522
|
+
}
|
|
11034
12523
|
if (isRequest(msg)) {
|
|
11035
12524
|
if (msg.method === "session/request_permission") {
|
|
11036
12525
|
const params = msg.params ?? {};
|
|
11037
12526
|
const sessionId2 = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
11038
12527
|
if (sessionId2) {
|
|
11039
|
-
|
|
12528
|
+
const toolCall = params.toolCall;
|
|
12529
|
+
const toolCallId = toolCall && typeof toolCall.toolCallId === "string" ? toolCall.toolCallId : void 0;
|
|
12530
|
+
const entry = {
|
|
11040
12531
|
requestId: msg.id,
|
|
11041
12532
|
sessionId: sessionId2,
|
|
12533
|
+
toolCallId,
|
|
11042
12534
|
params
|
|
11043
|
-
}
|
|
12535
|
+
};
|
|
12536
|
+
this.pendingPermissions.set(msg.id, entry);
|
|
12537
|
+
if (toolCallId) {
|
|
12538
|
+
this.pendingPermissionsByToolCall.set(toolCallId, entry);
|
|
12539
|
+
}
|
|
11044
12540
|
}
|
|
11045
12541
|
}
|
|
11046
12542
|
return;
|
|
@@ -11088,6 +12584,14 @@ var SessionTracker = class {
|
|
|
11088
12584
|
}
|
|
11089
12585
|
forget(sessionId) {
|
|
11090
12586
|
this.contexts.delete(sessionId);
|
|
12587
|
+
this.lastMessageIds.delete(sessionId);
|
|
12588
|
+
}
|
|
12589
|
+
// Latest messageId observed for `sessionId`, or undefined if we
|
|
12590
|
+
// haven't seen one (no prompt_received/turn_complete has flowed
|
|
12591
|
+
// through yet). Used by reconnect-replay to issue
|
|
12592
|
+
// historyPolicy:"after_message" with afterMessageId.
|
|
12593
|
+
lastMessageId(sessionId) {
|
|
12594
|
+
return this.lastMessageIds.get(sessionId);
|
|
11091
12595
|
}
|
|
11092
12596
|
clearPending() {
|
|
11093
12597
|
this.pending.clear();
|
|
@@ -11095,15 +12599,29 @@ var SessionTracker = class {
|
|
|
11095
12599
|
takePendingPermissions() {
|
|
11096
12600
|
const out = [...this.pendingPermissions.values()];
|
|
11097
12601
|
this.pendingPermissions.clear();
|
|
12602
|
+
this.pendingPermissionsByToolCall.clear();
|
|
11098
12603
|
return out;
|
|
11099
12604
|
}
|
|
11100
12605
|
takePendingPermission(requestId) {
|
|
11101
12606
|
const found = this.pendingPermissions.get(requestId);
|
|
11102
12607
|
if (found) {
|
|
11103
|
-
this.
|
|
12608
|
+
this.deletePendingPermission(found);
|
|
12609
|
+
}
|
|
12610
|
+
return found;
|
|
12611
|
+
}
|
|
12612
|
+
takePendingPermissionByToolCall(toolCallId) {
|
|
12613
|
+
const found = this.pendingPermissionsByToolCall.get(toolCallId);
|
|
12614
|
+
if (found) {
|
|
12615
|
+
this.deletePendingPermission(found);
|
|
11104
12616
|
}
|
|
11105
12617
|
return found;
|
|
11106
12618
|
}
|
|
12619
|
+
deletePendingPermission(entry) {
|
|
12620
|
+
this.pendingPermissions.delete(entry.requestId);
|
|
12621
|
+
if (entry.toolCallId) {
|
|
12622
|
+
this.pendingPermissionsByToolCall.delete(entry.toolCallId);
|
|
12623
|
+
}
|
|
12624
|
+
}
|
|
11107
12625
|
};
|
|
11108
12626
|
function isRequest(msg) {
|
|
11109
12627
|
return "method" in msg && "id" in msg && msg.id !== void 0;
|
|
@@ -11139,7 +12657,7 @@ async function runShim(opts) {
|
|
|
11139
12657
|
`
|
|
11140
12658
|
);
|
|
11141
12659
|
for (const ctx of contexts) {
|
|
11142
|
-
await replayAttach(upstream, ctx);
|
|
12660
|
+
await replayAttach(upstream, ctx, tracker.lastMessageId(ctx.sessionId));
|
|
11143
12661
|
}
|
|
11144
12662
|
}
|
|
11145
12663
|
});
|
|
@@ -11198,25 +12716,47 @@ function wireShim({
|
|
|
11198
12716
|
});
|
|
11199
12717
|
}
|
|
11200
12718
|
function maybeReplyToResolvedPermission(msg, tracker, downstream) {
|
|
11201
|
-
|
|
12719
|
+
const update = extractPermissionResolvedUpdate(msg);
|
|
12720
|
+
if (!update) {
|
|
11202
12721
|
return;
|
|
11203
12722
|
}
|
|
11204
|
-
const
|
|
11205
|
-
if (
|
|
12723
|
+
const toolCallId = typeof update.toolCallId === "string" ? update.toolCallId : void 0;
|
|
12724
|
+
if (!toolCallId) {
|
|
11206
12725
|
return;
|
|
11207
12726
|
}
|
|
11208
|
-
const pending = tracker.
|
|
12727
|
+
const pending = tracker.takePendingPermissionByToolCall(toolCallId);
|
|
11209
12728
|
if (!pending) {
|
|
11210
12729
|
return;
|
|
11211
12730
|
}
|
|
12731
|
+
const outcome = reconstructOutcome(update);
|
|
11212
12732
|
void downstream.send({
|
|
11213
12733
|
jsonrpc: "2.0",
|
|
11214
12734
|
id: pending.requestId,
|
|
11215
|
-
result:
|
|
12735
|
+
result: outcome ? { outcome } : null
|
|
11216
12736
|
}).catch(() => void 0);
|
|
11217
12737
|
}
|
|
11218
|
-
function
|
|
11219
|
-
|
|
12738
|
+
function extractPermissionResolvedUpdate(msg) {
|
|
12739
|
+
if (!isSessionUpdateNotification(msg)) {
|
|
12740
|
+
return void 0;
|
|
12741
|
+
}
|
|
12742
|
+
const params = msg.params ?? {};
|
|
12743
|
+
const update = params.update;
|
|
12744
|
+
if (!update || typeof update !== "object" || update.sessionUpdate !== "permission_resolved") {
|
|
12745
|
+
return void 0;
|
|
12746
|
+
}
|
|
12747
|
+
return update;
|
|
12748
|
+
}
|
|
12749
|
+
function isSessionUpdateNotification(msg) {
|
|
12750
|
+
return "method" in msg && msg.method === "session/update" && !("id" in msg && msg.id !== void 0);
|
|
12751
|
+
}
|
|
12752
|
+
function reconstructOutcome(update) {
|
|
12753
|
+
if (update.outcome && typeof update.outcome === "object") {
|
|
12754
|
+
return update.outcome;
|
|
12755
|
+
}
|
|
12756
|
+
if (typeof update.chosenOptionId === "string") {
|
|
12757
|
+
return { kind: "selected", optionId: update.chosenOptionId };
|
|
12758
|
+
}
|
|
12759
|
+
return void 0;
|
|
11220
12760
|
}
|
|
11221
12761
|
async function cancelPendingPermissions(tracker, downstream) {
|
|
11222
12762
|
const pendings = tracker.takePendingPermissions();
|
|
@@ -11228,21 +12768,26 @@ async function cancelPendingPermissions(tracker, downstream) {
|
|
|
11228
12768
|
`
|
|
11229
12769
|
);
|
|
11230
12770
|
for (const pending of pendings) {
|
|
11231
|
-
const
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
12771
|
+
const sessionId = typeof pending.params.sessionId === "string" ? pending.params.sessionId : void 0;
|
|
12772
|
+
if (!sessionId) {
|
|
12773
|
+
continue;
|
|
12774
|
+
}
|
|
12775
|
+
const update = {
|
|
12776
|
+
sessionUpdate: "permission_resolved",
|
|
12777
|
+
outcome: { kind: "cancelled", reason: "daemon-disconnected" },
|
|
12778
|
+
resolvedBy: { clientId: "hydra-acp" }
|
|
11237
12779
|
};
|
|
12780
|
+
if (pending.toolCallId) {
|
|
12781
|
+
update.toolCallId = pending.toolCallId;
|
|
12782
|
+
}
|
|
11238
12783
|
await downstream.send({
|
|
11239
12784
|
jsonrpc: "2.0",
|
|
11240
|
-
method: "session/
|
|
11241
|
-
params
|
|
12785
|
+
method: "session/update",
|
|
12786
|
+
params: { sessionId, update }
|
|
11242
12787
|
}).catch(() => void 0);
|
|
11243
12788
|
}
|
|
11244
12789
|
}
|
|
11245
|
-
async function replayAttach(stream, ctx) {
|
|
12790
|
+
async function replayAttach(stream, ctx, afterMessageId) {
|
|
11246
12791
|
const resumeHints = {
|
|
11247
12792
|
upstreamSessionId: ctx.upstreamSessionId,
|
|
11248
12793
|
agentId: ctx.agentId,
|
|
@@ -11254,19 +12799,21 @@ async function replayAttach(stream, ctx) {
|
|
|
11254
12799
|
if (ctx.agentArgs && ctx.agentArgs.length > 0) {
|
|
11255
12800
|
resumeHints.agentArgs = ctx.agentArgs;
|
|
11256
12801
|
}
|
|
12802
|
+
const params = {
|
|
12803
|
+
sessionId: ctx.sessionId,
|
|
12804
|
+
_meta: { "hydra-acp": { resume: resumeHints } }
|
|
12805
|
+
};
|
|
12806
|
+
if (afterMessageId) {
|
|
12807
|
+
params.historyPolicy = "after_message";
|
|
12808
|
+
params.afterMessageId = afterMessageId;
|
|
12809
|
+
} else {
|
|
12810
|
+
params.historyPolicy = "pending_only";
|
|
12811
|
+
}
|
|
11257
12812
|
const request = {
|
|
11258
12813
|
jsonrpc: "2.0",
|
|
11259
12814
|
id: `resume-${ctx.sessionId}-${Date.now()}`,
|
|
11260
12815
|
method: "session/attach",
|
|
11261
|
-
params
|
|
11262
|
-
sessionId: ctx.sessionId,
|
|
11263
|
-
historyPolicy: "pending_only",
|
|
11264
|
-
_meta: {
|
|
11265
|
-
"hydra-acp": {
|
|
11266
|
-
resume: resumeHints
|
|
11267
|
-
}
|
|
11268
|
-
}
|
|
11269
|
-
}
|
|
12816
|
+
params
|
|
11270
12817
|
};
|
|
11271
12818
|
try {
|
|
11272
12819
|
const resp = await stream.request(request);
|
|
@@ -11423,8 +12970,8 @@ async function main() {
|
|
|
11423
12970
|
await runSessionsKill(positional[2]);
|
|
11424
12971
|
return;
|
|
11425
12972
|
}
|
|
11426
|
-
if (sub === "
|
|
11427
|
-
await
|
|
12973
|
+
if (sub === "remove") {
|
|
12974
|
+
await runSessionsRemove(positional[2]);
|
|
11428
12975
|
return;
|
|
11429
12976
|
}
|
|
11430
12977
|
if (sub === "export") {
|
|
@@ -11433,8 +12980,11 @@ async function main() {
|
|
|
11433
12980
|
return;
|
|
11434
12981
|
}
|
|
11435
12982
|
if (sub === "import") {
|
|
12983
|
+
const cwd = resolveOption(flags, "cwd");
|
|
11436
12984
|
await runSessionsImport(positional[2], {
|
|
11437
|
-
replace: flags.replace === true
|
|
12985
|
+
replace: flags.replace === true,
|
|
12986
|
+
info: flags.info === true,
|
|
12987
|
+
...cwd !== void 0 ? { cwd } : {}
|
|
11438
12988
|
});
|
|
11439
12989
|
return;
|
|
11440
12990
|
}
|
|
@@ -11537,9 +13087,9 @@ async function dispatchTui(flags, base) {
|
|
|
11537
13087
|
}
|
|
11538
13088
|
function readVersion() {
|
|
11539
13089
|
try {
|
|
11540
|
-
const here =
|
|
13090
|
+
const here = dirname6(fileURLToPath2(import.meta.url));
|
|
11541
13091
|
const pkg = JSON.parse(
|
|
11542
|
-
|
|
13092
|
+
readFileSync2(resolve4(here, "../package.json"), "utf8")
|
|
11543
13093
|
);
|
|
11544
13094
|
return pkg.version ?? "unknown";
|
|
11545
13095
|
} catch {
|
|
@@ -11566,11 +13116,11 @@ function printHelp() {
|
|
|
11566
13116
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
11567
13117
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
11568
13118
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
11569
|
-
" hydra-acp sessions
|
|
13119
|
+
" hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
|
|
11570
13120
|
" hydra-acp sessions export <id> [--out <file>|.]",
|
|
11571
13121
|
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
11572
|
-
" hydra-acp sessions import <file>|- [--replace]",
|
|
11573
|
-
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live)",
|
|
13122
|
+
" hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
|
|
13123
|
+
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live); --cwd overrides the bundle's recorded working directory; --info prints the bundle's meta without importing",
|
|
11574
13124
|
" hydra-acp extensions list List configured extensions and live state",
|
|
11575
13125
|
" hydra-acp extensions add <name> [opts] Add an extension to config",
|
|
11576
13126
|
" hydra-acp extensions remove <name> Remove an extension from config",
|