@hydra-acp/cli 0.1.1 → 0.1.3
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 -2
- package/dist/cli.js +2139 -635
- package/dist/index.d.ts +88 -6
- package/dist/index.js +669 -61
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,11 @@ function hydraHome() {
|
|
|
17
17
|
if (override && override.length > 0) {
|
|
18
18
|
return path.resolve(override);
|
|
19
19
|
}
|
|
20
|
+
if (process.env.VITEST) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"HYDRA_ACP_HOME is unset under VITEST; vitest.setup.ts must run first"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
20
25
|
return path.join(os.homedir(), ".hydra-acp");
|
|
21
26
|
}
|
|
22
27
|
var ROOT_ENV, paths;
|
|
@@ -34,11 +39,17 @@ var init_paths = __esm({
|
|
|
34
39
|
agentsDir: () => path.join(hydraHome(), "agents"),
|
|
35
40
|
agentDir: (id) => path.join(hydraHome(), "agents", id),
|
|
36
41
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
37
|
-
|
|
42
|
+
// One directory per session id under sessions/. Co-locates the
|
|
43
|
+
// session record, its transcript, and any future per-session state
|
|
44
|
+
// (uploads, scratch, etc.) so the lifecycle is just "rm -rf the dir".
|
|
45
|
+
sessionDir: (id) => path.join(hydraHome(), "sessions", id),
|
|
46
|
+
sessionFile: (id) => path.join(hydraHome(), "sessions", id, "meta.json"),
|
|
47
|
+
historyFile: (id) => path.join(hydraHome(), "sessions", id, "history.jsonl"),
|
|
38
48
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
39
49
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
40
50
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
41
|
-
tuiHistoryFile: () => path.join(hydraHome(), "
|
|
51
|
+
tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
|
|
52
|
+
tuiLogFile: () => path.join(hydraHome(), "tui.log")
|
|
42
53
|
};
|
|
43
54
|
}
|
|
44
55
|
});
|
|
@@ -152,7 +163,11 @@ var init_config = __esm({
|
|
|
152
163
|
// /clear, ^L, resize — bypass this throttle. Default 1000 (1 Hz) keeps
|
|
153
164
|
// CPU low during heavy streaming; bump to 250 for 4 Hz, 100 for ~10 Hz,
|
|
154
165
|
// or 0 to disable throttling entirely.
|
|
155
|
-
repaintThrottleMs: z.number().int().nonnegative().default(1e3)
|
|
166
|
+
repaintThrottleMs: z.number().int().nonnegative().default(1e3),
|
|
167
|
+
// Cap on logical lines retained in the in-memory scrollback render
|
|
168
|
+
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
169
|
+
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
170
|
+
maxScrollbackLines: z.number().int().positive().default(1e4)
|
|
156
171
|
});
|
|
157
172
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
158
173
|
ExtensionBody = z.object({
|
|
@@ -177,7 +192,7 @@ var init_config = __esm({
|
|
|
177
192
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
178
193
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
179
194
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
180
|
-
tui: TuiConfig.default({ repaintThrottleMs: 1e3 })
|
|
195
|
+
tui: TuiConfig.default({ repaintThrottleMs: 1e3, maxScrollbackLines: 1e4 })
|
|
181
196
|
});
|
|
182
197
|
}
|
|
183
198
|
});
|
|
@@ -215,6 +230,35 @@ function extractHydraMeta(meta) {
|
|
|
215
230
|
out.resume = parsed.data;
|
|
216
231
|
}
|
|
217
232
|
}
|
|
233
|
+
if (typeof obj.currentModel === "string") {
|
|
234
|
+
out.currentModel = obj.currentModel;
|
|
235
|
+
}
|
|
236
|
+
if (typeof obj.currentMode === "string") {
|
|
237
|
+
out.currentMode = obj.currentMode;
|
|
238
|
+
}
|
|
239
|
+
if (typeof obj.turnStartedAt === "number" && obj.turnStartedAt > 0) {
|
|
240
|
+
out.turnStartedAt = obj.turnStartedAt;
|
|
241
|
+
}
|
|
242
|
+
if (Array.isArray(obj.availableCommands)) {
|
|
243
|
+
const cmds = [];
|
|
244
|
+
for (const raw of obj.availableCommands) {
|
|
245
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const c = raw;
|
|
249
|
+
if (typeof c.name !== "string") {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const cmd = { name: c.name };
|
|
253
|
+
if (typeof c.description === "string") {
|
|
254
|
+
cmd.description = c.description;
|
|
255
|
+
}
|
|
256
|
+
cmds.push(cmd);
|
|
257
|
+
}
|
|
258
|
+
if (cmds.length > 0) {
|
|
259
|
+
out.availableCommands = cmds;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
218
262
|
return out;
|
|
219
263
|
}
|
|
220
264
|
function mergeMeta(passthrough, ours) {
|
|
@@ -502,6 +546,25 @@ function withCode(err, code) {
|
|
|
502
546
|
err.code = code;
|
|
503
547
|
return err;
|
|
504
548
|
}
|
|
549
|
+
function isStateUpdate(method, params) {
|
|
550
|
+
if (method !== "session/update") {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
const obj = params ?? {};
|
|
554
|
+
const kind = obj.update?.sessionUpdate;
|
|
555
|
+
return typeof kind === "string" && STATE_UPDATE_KINDS.has(kind);
|
|
556
|
+
}
|
|
557
|
+
function sameAdvertisedCommands(a, b) {
|
|
558
|
+
if (a.length !== b.length) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
for (let i = 0; i < a.length; i++) {
|
|
562
|
+
if (a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
505
568
|
function captureInternalChunk(capture, params) {
|
|
506
569
|
const obj = params ?? {};
|
|
507
570
|
const update = obj.update ?? {};
|
|
@@ -564,7 +627,7 @@ function firstLine(text, max) {
|
|
|
564
627
|
}
|
|
565
628
|
return void 0;
|
|
566
629
|
}
|
|
567
|
-
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, Session;
|
|
630
|
+
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, Session, STATE_UPDATE_KINDS;
|
|
568
631
|
var init_session = __esm({
|
|
569
632
|
"src/core/session.ts"() {
|
|
570
633
|
"use strict";
|
|
@@ -586,14 +649,26 @@ var init_session = __esm({
|
|
|
586
649
|
agentMeta;
|
|
587
650
|
agentArgs;
|
|
588
651
|
title;
|
|
652
|
+
// Snapshot state delivered to attaching clients via the attach
|
|
653
|
+
// response _meta rather than via history replay (which would be
|
|
654
|
+
// stale-prone for snapshot-shaped events).
|
|
655
|
+
currentModel;
|
|
656
|
+
currentMode;
|
|
589
657
|
updatedAt;
|
|
590
658
|
clients = /* @__PURE__ */ new Map();
|
|
591
659
|
history = [];
|
|
660
|
+
historyStore;
|
|
592
661
|
promptQueue = [];
|
|
593
662
|
promptInFlight = false;
|
|
594
663
|
closed = false;
|
|
595
664
|
closeHandlers = [];
|
|
596
665
|
titleHandlers = [];
|
|
666
|
+
// Subscribers notified after every entry that's actually persisted to
|
|
667
|
+
// history (skipping snapshot-shaped events filtered by
|
|
668
|
+
// recordAndBroadcast). The HTTP /v1/sessions/:id/history?follow=1
|
|
669
|
+
// endpoint uses this to tail a live session's conversation stream
|
|
670
|
+
// without participating in turns or prompts.
|
|
671
|
+
broadcastHandlers = [];
|
|
597
672
|
// True once we've observed our first session/prompt; gates the
|
|
598
673
|
// first-prompt-seeded title so subsequent prompts don't churn it.
|
|
599
674
|
firstPromptSeeded = false;
|
|
@@ -613,12 +688,18 @@ var init_session = __esm({
|
|
|
613
688
|
idleTimer;
|
|
614
689
|
spawnReplacementAgent;
|
|
615
690
|
agentChangeHandlers = [];
|
|
616
|
-
// Last available_commands_update we observed from the agent. Stored
|
|
617
|
-
// we can re-broadcast a merged (hydra ∪ agent) list whenever
|
|
618
|
-
// half changes
|
|
619
|
-
//
|
|
620
|
-
//
|
|
691
|
+
// Last available_commands_update we observed from the agent. Stored
|
|
692
|
+
// so we can re-broadcast a merged (hydra ∪ agent) list whenever
|
|
693
|
+
// either half changes, and persisted to meta.json so a fresh attach
|
|
694
|
+
// can deliver the merged list via _meta without depending on history
|
|
695
|
+
// replay.
|
|
621
696
|
agentAdvertisedCommands = [];
|
|
697
|
+
// Persist hooks for snapshot-shaped state. SessionManager hooks these
|
|
698
|
+
// to mirror changes into meta.json so cold-resurrect attaches can
|
|
699
|
+
// surface the latest snapshot via the attach response _meta.
|
|
700
|
+
agentCommandsHandlers = [];
|
|
701
|
+
modelHandlers = [];
|
|
702
|
+
modeHandlers = [];
|
|
622
703
|
constructor(init) {
|
|
623
704
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
624
705
|
this.cwd = init.cwd;
|
|
@@ -628,11 +709,22 @@ var init_session = __esm({
|
|
|
628
709
|
this.agentMeta = init.agentMeta;
|
|
629
710
|
this.agentArgs = init.agentArgs;
|
|
630
711
|
this.title = init.title;
|
|
712
|
+
this.currentModel = init.currentModel;
|
|
713
|
+
this.currentMode = init.currentMode;
|
|
714
|
+
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
715
|
+
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
716
|
+
}
|
|
631
717
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
632
718
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
719
|
+
if (init.firstPromptSeeded) {
|
|
720
|
+
this.firstPromptSeeded = true;
|
|
721
|
+
}
|
|
722
|
+
this.historyStore = init.historyStore;
|
|
723
|
+
if (init.seedHistory && init.seedHistory.length > 0) {
|
|
724
|
+
this.history = [...init.seedHistory];
|
|
725
|
+
}
|
|
633
726
|
this.updatedAt = Date.now();
|
|
634
727
|
this.wireAgent(this.agent);
|
|
635
|
-
this.broadcastMergedCommands();
|
|
636
728
|
}
|
|
637
729
|
broadcastMergedCommands() {
|
|
638
730
|
const merged = [
|
|
@@ -661,8 +753,15 @@ var init_session = __esm({
|
|
|
661
753
|
}
|
|
662
754
|
const agentCmds = extractAdvertisedCommands(params);
|
|
663
755
|
if (agentCmds !== null) {
|
|
664
|
-
this.
|
|
665
|
-
|
|
756
|
+
this.setAgentAdvertisedCommands(agentCmds);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (this.maybeApplyAgentModel(params)) {
|
|
760
|
+
this.recordAndBroadcast("session/update", params);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (this.maybeApplyAgentMode(params)) {
|
|
764
|
+
this.recordAndBroadcast("session/update", params);
|
|
666
765
|
return;
|
|
667
766
|
}
|
|
668
767
|
this.maybeApplyAgentSessionInfo(params);
|
|
@@ -684,6 +783,50 @@ var init_session = __esm({
|
|
|
684
783
|
get attachedCount() {
|
|
685
784
|
return this.clients.size;
|
|
686
785
|
}
|
|
786
|
+
// Wall-clock when the in-flight agent turn began, or undefined when
|
|
787
|
+
// idle. Derived from history: the most recent prompt_received without
|
|
788
|
+
// a later turn_complete is the outstanding turn, and its recordedAt
|
|
789
|
+
// is when the prompt was first broadcast. Used by buildResponseMeta
|
|
790
|
+
// so a fresh client reattaching mid-turn boots up with the busy
|
|
791
|
+
// banner showing real elapsed time.
|
|
792
|
+
get turnStartedAt() {
|
|
793
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
794
|
+
const entry = this.history[i];
|
|
795
|
+
if (!entry) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const params = entry.params;
|
|
799
|
+
const kind = params?.update?.sessionUpdate;
|
|
800
|
+
if (kind === "turn_complete") {
|
|
801
|
+
return void 0;
|
|
802
|
+
}
|
|
803
|
+
if (kind === "prompt_received") {
|
|
804
|
+
return entry.recordedAt;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return void 0;
|
|
808
|
+
}
|
|
809
|
+
// Snapshot of the current in-memory replay history. Used by the
|
|
810
|
+
// HTTP history endpoint to deliver the "what's accumulated so far"
|
|
811
|
+
// prefix before optionally tailing with onBroadcast. Returns a copy
|
|
812
|
+
// so callers can't mutate our cache.
|
|
813
|
+
getHistorySnapshot() {
|
|
814
|
+
return [...this.history];
|
|
815
|
+
}
|
|
816
|
+
// Subscribe to recordable broadcast entries — fires once per entry
|
|
817
|
+
// that lands in history (so snapshot-shaped session_info/model/mode/
|
|
818
|
+
// available_commands updates do NOT trigger this; they're broadcast
|
|
819
|
+
// live but not recorded). Returns an unsubscribe function the caller
|
|
820
|
+
// must invoke when done.
|
|
821
|
+
onBroadcast(handler) {
|
|
822
|
+
this.broadcastHandlers.push(handler);
|
|
823
|
+
return () => {
|
|
824
|
+
const i = this.broadcastHandlers.indexOf(handler);
|
|
825
|
+
if (i >= 0) {
|
|
826
|
+
this.broadcastHandlers.splice(i, 1);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
687
830
|
attach(client, historyPolicy) {
|
|
688
831
|
if (this.closed) {
|
|
689
832
|
throw withCode(
|
|
@@ -740,13 +883,19 @@ var init_session = __esm({
|
|
|
740
883
|
this.broadcastPromptReceived(client, params);
|
|
741
884
|
this.maybeSeedTitleFromPrompt(params);
|
|
742
885
|
return this.enqueuePrompt(async () => {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
886
|
+
let response;
|
|
887
|
+
try {
|
|
888
|
+
response = await this.agent.connection.request(
|
|
889
|
+
"session/prompt",
|
|
890
|
+
{
|
|
891
|
+
...params,
|
|
892
|
+
sessionId: this.upstreamSessionId
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
this.broadcastTurnComplete(client.clientId, { stopReason: "error" });
|
|
897
|
+
throw err;
|
|
898
|
+
}
|
|
750
899
|
this.broadcastTurnComplete(client.clientId, response);
|
|
751
900
|
return response;
|
|
752
901
|
});
|
|
@@ -834,6 +983,13 @@ var init_session = __esm({
|
|
|
834
983
|
return;
|
|
835
984
|
}
|
|
836
985
|
this.cancelIdleTimer();
|
|
986
|
+
if (opts.regenTitle && this.firstPromptSeeded) {
|
|
987
|
+
const timeoutMs = opts.regenTitleTimeoutMs ?? 5e3;
|
|
988
|
+
await Promise.race([
|
|
989
|
+
this.runTitleRegen().catch(() => void 0),
|
|
990
|
+
new Promise((r) => setTimeout(r, timeoutMs).unref?.())
|
|
991
|
+
]);
|
|
992
|
+
}
|
|
837
993
|
await this.agent.kill().catch(() => void 0);
|
|
838
994
|
this.markClosed({ deleteRecord: opts.deleteRecord ?? false });
|
|
839
995
|
}
|
|
@@ -882,13 +1038,98 @@ var init_session = __esm({
|
|
|
882
1038
|
}
|
|
883
1039
|
const promptParams = params ?? {};
|
|
884
1040
|
const text = extractPromptText(promptParams.prompt);
|
|
885
|
-
const seed = firstLine(text,
|
|
1041
|
+
const seed = firstLine(text, 200);
|
|
886
1042
|
if (!seed) {
|
|
887
1043
|
return;
|
|
888
1044
|
}
|
|
889
1045
|
this.firstPromptSeeded = true;
|
|
890
1046
|
this.setTitle(seed);
|
|
891
1047
|
}
|
|
1048
|
+
// Apply an agent-emitted current_model_update. Returns true if the
|
|
1049
|
+
// notification was a model update (caller still needs to broadcast
|
|
1050
|
+
// it). Returns false otherwise so the caller can try the next kind.
|
|
1051
|
+
maybeApplyAgentModel(params) {
|
|
1052
|
+
const obj = params ?? {};
|
|
1053
|
+
const update = obj.update ?? {};
|
|
1054
|
+
if (update.sessionUpdate !== "current_model_update") {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
const raw = typeof update.currentModel === "string" ? update.currentModel : typeof update.model === "string" ? update.model : void 0;
|
|
1058
|
+
if (raw === void 0) {
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
const trimmed = raw.trim();
|
|
1062
|
+
if (!trimmed || trimmed === this.currentModel) {
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
this.currentModel = trimmed;
|
|
1066
|
+
for (const handler of this.modelHandlers) {
|
|
1067
|
+
try {
|
|
1068
|
+
handler(trimmed);
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
maybeApplyAgentMode(params) {
|
|
1075
|
+
const obj = params ?? {};
|
|
1076
|
+
const update = obj.update ?? {};
|
|
1077
|
+
if (update.sessionUpdate !== "current_mode_update") {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
const raw = typeof update.currentMode === "string" ? update.currentMode : typeof update.mode === "string" ? update.mode : void 0;
|
|
1081
|
+
if (raw === void 0) {
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
const trimmed = raw.trim();
|
|
1085
|
+
if (!trimmed || trimmed === this.currentMode) {
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
this.currentMode = trimmed;
|
|
1089
|
+
for (const handler of this.modeHandlers) {
|
|
1090
|
+
try {
|
|
1091
|
+
handler(trimmed);
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
// Update the cached agent command list, fire persist handlers, and
|
|
1098
|
+
// broadcast the merged list to attached clients. Idempotent on a
|
|
1099
|
+
// structurally identical list so we don't churn meta.json on noisy
|
|
1100
|
+
// re-emissions.
|
|
1101
|
+
setAgentAdvertisedCommands(commands) {
|
|
1102
|
+
if (sameAdvertisedCommands(this.agentAdvertisedCommands, commands)) {
|
|
1103
|
+
this.broadcastMergedCommands();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
this.agentAdvertisedCommands = commands;
|
|
1107
|
+
for (const handler of this.agentCommandsHandlers) {
|
|
1108
|
+
try {
|
|
1109
|
+
handler(commands);
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
this.broadcastMergedCommands();
|
|
1114
|
+
}
|
|
1115
|
+
// Subscribe to snapshot-state updates. SessionManager wires these to
|
|
1116
|
+
// persist the new value into meta.json so cold resurrect can restore
|
|
1117
|
+
// them via the attach response _meta.
|
|
1118
|
+
onAgentCommandsChange(handler) {
|
|
1119
|
+
this.agentCommandsHandlers.push(handler);
|
|
1120
|
+
}
|
|
1121
|
+
onModelChange(handler) {
|
|
1122
|
+
this.modelHandlers.push(handler);
|
|
1123
|
+
}
|
|
1124
|
+
onModeChange(handler) {
|
|
1125
|
+
this.modeHandlers.push(handler);
|
|
1126
|
+
}
|
|
1127
|
+
// Returns a freshly merged command list (hydra ∪ agent) for callers
|
|
1128
|
+
// that need a snapshot — notably acp-ws.ts's buildResponseMeta when
|
|
1129
|
+
// assembling the attach response.
|
|
1130
|
+
mergedAvailableCommands() {
|
|
1131
|
+
return [...hydraCommandsAsAdvertised(), ...this.agentAdvertisedCommands];
|
|
1132
|
+
}
|
|
892
1133
|
// Pick up an agent-emitted session_info_update and store its title
|
|
893
1134
|
// as our canonical record. The notification is also forwarded to
|
|
894
1135
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -1176,7 +1417,8 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1176
1417
|
}
|
|
1177
1418
|
this.idleTimer = setTimeout(() => {
|
|
1178
1419
|
this.idleTimer = void 0;
|
|
1179
|
-
|
|
1420
|
+
const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
|
|
1421
|
+
void this.close(opts).catch(() => void 0);
|
|
1180
1422
|
}, this.idleTimeoutMs);
|
|
1181
1423
|
if (typeof this.idleTimer.unref === "function") {
|
|
1182
1424
|
this.idleTimer.unref();
|
|
@@ -1199,9 +1441,34 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1199
1441
|
}
|
|
1200
1442
|
recordAndBroadcast(method, params, excludeClientId) {
|
|
1201
1443
|
const rewritten = this.rewriteForClient(params);
|
|
1202
|
-
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1444
|
+
const recordable = !isStateUpdate(method, rewritten);
|
|
1445
|
+
if (recordable) {
|
|
1446
|
+
const entry = {
|
|
1447
|
+
method,
|
|
1448
|
+
params: rewritten,
|
|
1449
|
+
recordedAt: Date.now()
|
|
1450
|
+
};
|
|
1451
|
+
this.history.push(entry);
|
|
1452
|
+
let trimmed = false;
|
|
1453
|
+
if (this.history.length > 1e3) {
|
|
1454
|
+
this.history = this.history.slice(-500);
|
|
1455
|
+
trimmed = true;
|
|
1456
|
+
}
|
|
1457
|
+
if (this.historyStore) {
|
|
1458
|
+
if (trimmed) {
|
|
1459
|
+
void this.historyStore.rewrite(this.sessionId, [...this.history]).catch(() => void 0);
|
|
1460
|
+
} else {
|
|
1461
|
+
void this.historyStore.append(this.sessionId, entry).catch(
|
|
1462
|
+
() => void 0
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
for (const handler of this.broadcastHandlers) {
|
|
1467
|
+
try {
|
|
1468
|
+
handler(entry);
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1205
1472
|
}
|
|
1206
1473
|
this.updatedAt = Date.now();
|
|
1207
1474
|
for (const client of this.clients.values()) {
|
|
@@ -1297,6 +1564,12 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1297
1564
|
}
|
|
1298
1565
|
}
|
|
1299
1566
|
};
|
|
1567
|
+
STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
|
|
1568
|
+
"session_info_update",
|
|
1569
|
+
"current_model_update",
|
|
1570
|
+
"current_mode_update",
|
|
1571
|
+
"available_commands_update"
|
|
1572
|
+
]);
|
|
1300
1573
|
}
|
|
1301
1574
|
});
|
|
1302
1575
|
|
|
@@ -1430,6 +1703,147 @@ var init_daemon_bootstrap = __esm({
|
|
|
1430
1703
|
}
|
|
1431
1704
|
});
|
|
1432
1705
|
|
|
1706
|
+
// src/cli/session-row.ts
|
|
1707
|
+
function toRow(s, now = Date.now()) {
|
|
1708
|
+
return {
|
|
1709
|
+
session: stripHydraSessionPrefix(s.sessionId),
|
|
1710
|
+
upstream: s.upstreamSessionId ?? "-",
|
|
1711
|
+
status: (s.status ?? "live").toUpperCase(),
|
|
1712
|
+
clients: s.status === "cold" ? "-" : String(s.attachedClients),
|
|
1713
|
+
agent: s.agentId ?? "?",
|
|
1714
|
+
age: formatRelativeAge(s.updatedAt, now),
|
|
1715
|
+
title: s.title ?? "-",
|
|
1716
|
+
cwd: s.cwd
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function computeWidths(rows) {
|
|
1720
|
+
return {
|
|
1721
|
+
session: maxLen(HEADER.session, rows.map((r) => r.session)),
|
|
1722
|
+
upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
1723
|
+
status: maxLen(HEADER.status, rows.map((r) => r.status)),
|
|
1724
|
+
clients: maxLen(HEADER.clients, rows.map((r) => r.clients)),
|
|
1725
|
+
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
1726
|
+
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
1727
|
+
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function formatRelativeAge(iso, now) {
|
|
1731
|
+
if (!iso) {
|
|
1732
|
+
return "?";
|
|
1733
|
+
}
|
|
1734
|
+
const t = Date.parse(iso);
|
|
1735
|
+
if (Number.isNaN(t)) {
|
|
1736
|
+
return "?";
|
|
1737
|
+
}
|
|
1738
|
+
const diff = Math.max(0, now - t);
|
|
1739
|
+
const sec = Math.floor(diff / 1e3);
|
|
1740
|
+
if (sec < 60) {
|
|
1741
|
+
return "<1m";
|
|
1742
|
+
}
|
|
1743
|
+
const min = Math.floor(sec / 60);
|
|
1744
|
+
if (min < 60) {
|
|
1745
|
+
return `${min}m`;
|
|
1746
|
+
}
|
|
1747
|
+
const hr = Math.floor(min / 60);
|
|
1748
|
+
if (hr < 24) {
|
|
1749
|
+
return `${hr}h`;
|
|
1750
|
+
}
|
|
1751
|
+
const day = Math.floor(hr / 24);
|
|
1752
|
+
if (day < 14) {
|
|
1753
|
+
return `${day}d`;
|
|
1754
|
+
}
|
|
1755
|
+
const week = Math.floor(day / 7);
|
|
1756
|
+
if (week < 9) {
|
|
1757
|
+
return `${week}w`;
|
|
1758
|
+
}
|
|
1759
|
+
const month = Math.floor(day / 30);
|
|
1760
|
+
if (month < 12) {
|
|
1761
|
+
return `${month}mo`;
|
|
1762
|
+
}
|
|
1763
|
+
const year = Math.floor(day / 365);
|
|
1764
|
+
return `${year}y`;
|
|
1765
|
+
}
|
|
1766
|
+
function maxLen(headerCell, values) {
|
|
1767
|
+
let max = headerCell.length;
|
|
1768
|
+
for (const v of values) {
|
|
1769
|
+
if (v.length > max) {
|
|
1770
|
+
max = v.length;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return max;
|
|
1774
|
+
}
|
|
1775
|
+
function formatRow(r, w, maxWidth) {
|
|
1776
|
+
const fixed = [
|
|
1777
|
+
r.session.padEnd(w.session),
|
|
1778
|
+
r.upstream.padEnd(w.upstream),
|
|
1779
|
+
r.status.padEnd(w.status),
|
|
1780
|
+
r.clients.padStart(w.clients),
|
|
1781
|
+
r.agent.padEnd(w.agent),
|
|
1782
|
+
r.age.padStart(w.age)
|
|
1783
|
+
].join(SEP);
|
|
1784
|
+
if (maxWidth === void 0) {
|
|
1785
|
+
return [fixed, r.title.padEnd(w.title), r.cwd].join(SEP);
|
|
1786
|
+
}
|
|
1787
|
+
const titleCap = Math.min(w.title, TITLE_MAX_WIDTH);
|
|
1788
|
+
const budget = maxWidth - fixed.length - SEP.length;
|
|
1789
|
+
if (budget <= 0) {
|
|
1790
|
+
return fixed.slice(0, maxWidth);
|
|
1791
|
+
}
|
|
1792
|
+
const titleNatural = Math.min(r.title.length, titleCap);
|
|
1793
|
+
let titleAlloc = titleNatural + SEP.length + MIN_CWD <= budget ? titleCap : Math.max(0, budget - SEP.length - MIN_CWD);
|
|
1794
|
+
titleAlloc = Math.min(titleAlloc, Math.max(0, budget - SEP.length - 1));
|
|
1795
|
+
const titleCell = truncateRight(r.title, titleAlloc).padEnd(titleAlloc);
|
|
1796
|
+
const cwdBudget = Math.max(0, budget - titleAlloc - SEP.length);
|
|
1797
|
+
const cwdCell = truncateMiddle(r.cwd, cwdBudget);
|
|
1798
|
+
return [fixed, titleCell, cwdCell].join(SEP);
|
|
1799
|
+
}
|
|
1800
|
+
function truncateRight(s, max) {
|
|
1801
|
+
if (max <= 0) {
|
|
1802
|
+
return "";
|
|
1803
|
+
}
|
|
1804
|
+
if (s.length <= max) {
|
|
1805
|
+
return s;
|
|
1806
|
+
}
|
|
1807
|
+
if (max === 1) {
|
|
1808
|
+
return "\u2026";
|
|
1809
|
+
}
|
|
1810
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
1811
|
+
}
|
|
1812
|
+
function truncateMiddle(s, max) {
|
|
1813
|
+
if (max <= 0) {
|
|
1814
|
+
return "";
|
|
1815
|
+
}
|
|
1816
|
+
if (s.length <= max) {
|
|
1817
|
+
return s;
|
|
1818
|
+
}
|
|
1819
|
+
if (max === 1) {
|
|
1820
|
+
return "\u2026";
|
|
1821
|
+
}
|
|
1822
|
+
const head = Math.ceil((max - 1) / 2);
|
|
1823
|
+
const tail = max - 1 - head;
|
|
1824
|
+
return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
|
|
1825
|
+
}
|
|
1826
|
+
var HEADER, SEP, MIN_CWD, TITLE_MAX_WIDTH;
|
|
1827
|
+
var init_session_row = __esm({
|
|
1828
|
+
"src/cli/session-row.ts"() {
|
|
1829
|
+
"use strict";
|
|
1830
|
+
init_session();
|
|
1831
|
+
HEADER = {
|
|
1832
|
+
session: "SESSION",
|
|
1833
|
+
upstream: "UPSTREAM",
|
|
1834
|
+
status: "STATUS",
|
|
1835
|
+
clients: "CLIENTS",
|
|
1836
|
+
agent: "AGENT",
|
|
1837
|
+
age: "AGE",
|
|
1838
|
+
title: "TITLE",
|
|
1839
|
+
cwd: "CWD"
|
|
1840
|
+
};
|
|
1841
|
+
SEP = " ";
|
|
1842
|
+
MIN_CWD = 8;
|
|
1843
|
+
TITLE_MAX_WIDTH = 40;
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1433
1847
|
// src/cli/commands/sessions.ts
|
|
1434
1848
|
async function runSessionsList(opts = {}) {
|
|
1435
1849
|
const config = await loadConfig();
|
|
@@ -1465,44 +1879,13 @@ async function runSessionsList(opts = {}) {
|
|
|
1465
1879
|
visible = [...sorted.slice(0, liveCount), ...coldSlice];
|
|
1466
1880
|
truncated = hiddenCold;
|
|
1467
1881
|
}
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
agent: s.agentId ?? "?",
|
|
1474
|
-
title: s.title ?? "-",
|
|
1475
|
-
cwd: s.cwd
|
|
1476
|
-
}));
|
|
1477
|
-
const header = {
|
|
1478
|
-
session: "SESSION",
|
|
1479
|
-
upstream: "UPSTREAM",
|
|
1480
|
-
status: "STATUS",
|
|
1481
|
-
clients: "CLIENTS",
|
|
1482
|
-
agent: "AGENT",
|
|
1483
|
-
title: "TITLE",
|
|
1484
|
-
cwd: "CWD"
|
|
1485
|
-
};
|
|
1486
|
-
const widths = {
|
|
1487
|
-
session: maxLen(header.session, rows.map((r) => r.session)),
|
|
1488
|
-
upstream: maxLen(header.upstream, rows.map((r) => r.upstream)),
|
|
1489
|
-
status: maxLen(header.status, rows.map((r) => r.status)),
|
|
1490
|
-
clients: maxLen(header.clients, rows.map((r) => r.clients)),
|
|
1491
|
-
agent: maxLen(header.agent, rows.map((r) => r.agent)),
|
|
1492
|
-
title: maxLen(header.title, rows.map((r) => r.title))
|
|
1493
|
-
};
|
|
1494
|
-
const formatRow2 = (r) => [
|
|
1495
|
-
r.session.padEnd(widths.session),
|
|
1496
|
-
r.upstream.padEnd(widths.upstream),
|
|
1497
|
-
r.status.padEnd(widths.status),
|
|
1498
|
-
r.clients.padStart(widths.clients),
|
|
1499
|
-
r.agent.padEnd(widths.agent),
|
|
1500
|
-
r.title.padEnd(widths.title),
|
|
1501
|
-
r.cwd
|
|
1502
|
-
].join(" ");
|
|
1503
|
-
process.stdout.write(formatRow2(header) + "\n");
|
|
1882
|
+
const now = Date.now();
|
|
1883
|
+
const rows = visible.map((s) => toRow(s, now));
|
|
1884
|
+
const widths = computeWidths(rows);
|
|
1885
|
+
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
1886
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth) + "\n");
|
|
1504
1887
|
for (const r of rows) {
|
|
1505
|
-
process.stdout.write(
|
|
1888
|
+
process.stdout.write(formatRow(r, widths, maxWidth) + "\n");
|
|
1506
1889
|
}
|
|
1507
1890
|
if (truncated > 0) {
|
|
1508
1891
|
process.stdout.write(
|
|
@@ -1512,15 +1895,6 @@ async function runSessionsList(opts = {}) {
|
|
|
1512
1895
|
);
|
|
1513
1896
|
}
|
|
1514
1897
|
}
|
|
1515
|
-
function maxLen(headerCell, values) {
|
|
1516
|
-
let max = headerCell.length;
|
|
1517
|
-
for (const v of values) {
|
|
1518
|
-
if (v.length > max) {
|
|
1519
|
-
max = v.length;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
return max;
|
|
1523
|
-
}
|
|
1524
1898
|
async function runSessionsKill(id) {
|
|
1525
1899
|
if (!id) {
|
|
1526
1900
|
process.stderr.write("Usage: hydra-acp sessions kill <session-id>\n");
|
|
@@ -1528,6 +1902,25 @@ async function runSessionsKill(id) {
|
|
|
1528
1902
|
}
|
|
1529
1903
|
const config = await loadConfig();
|
|
1530
1904
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
1905
|
+
const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
|
|
1906
|
+
method: "POST",
|
|
1907
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
1908
|
+
});
|
|
1909
|
+
if (!response.ok && response.status !== 204) {
|
|
1910
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
1911
|
+
`);
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
process.stdout.write(`Killed ${id}
|
|
1915
|
+
`);
|
|
1916
|
+
}
|
|
1917
|
+
async function runSessionsRm(id) {
|
|
1918
|
+
if (!id) {
|
|
1919
|
+
process.stderr.write("Usage: hydra-acp sessions rm <session-id>\n");
|
|
1920
|
+
process.exit(2);
|
|
1921
|
+
}
|
|
1922
|
+
const config = await loadConfig();
|
|
1923
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
1531
1924
|
const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
|
|
1532
1925
|
method: "DELETE",
|
|
1533
1926
|
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
@@ -1537,7 +1930,7 @@ async function runSessionsKill(id) {
|
|
|
1537
1930
|
`);
|
|
1538
1931
|
process.exit(1);
|
|
1539
1932
|
}
|
|
1540
|
-
process.stdout.write(`
|
|
1933
|
+
process.stdout.write(`Removed ${id}
|
|
1541
1934
|
`);
|
|
1542
1935
|
}
|
|
1543
1936
|
function httpBase(host, port, tls) {
|
|
@@ -1548,17 +1941,252 @@ var init_sessions = __esm({
|
|
|
1548
1941
|
"src/cli/commands/sessions.ts"() {
|
|
1549
1942
|
"use strict";
|
|
1550
1943
|
init_config();
|
|
1551
|
-
|
|
1944
|
+
init_session_row();
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
// src/shim/resilient-ws.ts
|
|
1949
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
1950
|
+
import { WebSocket } from "ws";
|
|
1951
|
+
function isResponse(msg) {
|
|
1952
|
+
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
1953
|
+
}
|
|
1954
|
+
async function openWs(url, subprotocols) {
|
|
1955
|
+
return new Promise((resolve2, reject) => {
|
|
1956
|
+
const ws = new WebSocket(url, subprotocols);
|
|
1957
|
+
const onOpen = () => {
|
|
1958
|
+
ws.off("error", onError);
|
|
1959
|
+
resolve2(wsToMessageStream(ws));
|
|
1960
|
+
};
|
|
1961
|
+
const onError = (err) => {
|
|
1962
|
+
ws.off("open", onOpen);
|
|
1963
|
+
reject(err);
|
|
1964
|
+
};
|
|
1965
|
+
ws.once("open", onOpen);
|
|
1966
|
+
ws.once("error", onError);
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
var BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_MULTIPLIER, MAX_RECONNECT_ATTEMPTS, ResilientWsStream;
|
|
1970
|
+
var init_resilient_ws = __esm({
|
|
1971
|
+
"src/shim/resilient-ws.ts"() {
|
|
1972
|
+
"use strict";
|
|
1973
|
+
init_ws_stream();
|
|
1974
|
+
init_types();
|
|
1975
|
+
BACKOFF_INITIAL_MS = 200;
|
|
1976
|
+
BACKOFF_MAX_MS = 5e3;
|
|
1977
|
+
BACKOFF_MULTIPLIER = 2;
|
|
1978
|
+
MAX_RECONNECT_ATTEMPTS = 60;
|
|
1979
|
+
ResilientWsStream = class {
|
|
1980
|
+
constructor(opts) {
|
|
1981
|
+
this.opts = opts;
|
|
1982
|
+
}
|
|
1983
|
+
opts;
|
|
1984
|
+
current;
|
|
1985
|
+
outboundQueue = [];
|
|
1986
|
+
messageHandlers = [];
|
|
1987
|
+
closeHandlers = [];
|
|
1988
|
+
destroyed = false;
|
|
1989
|
+
firstConnect = true;
|
|
1990
|
+
reconnectInFlight;
|
|
1991
|
+
connectGate;
|
|
1992
|
+
releaseConnectGate;
|
|
1993
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1994
|
+
async start() {
|
|
1995
|
+
await this.connectWithRetry();
|
|
1996
|
+
}
|
|
1997
|
+
onMessage(handler) {
|
|
1998
|
+
this.messageHandlers.push(handler);
|
|
1999
|
+
}
|
|
2000
|
+
onClose(handler) {
|
|
2001
|
+
this.closeHandlers.push(handler);
|
|
2002
|
+
}
|
|
2003
|
+
async send(message) {
|
|
2004
|
+
if (this.destroyed) {
|
|
2005
|
+
throw new Error("resilient ws stream is destroyed");
|
|
2006
|
+
}
|
|
2007
|
+
if (this.connectGate || !this.current) {
|
|
2008
|
+
this.outboundQueue.push(message);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
try {
|
|
2012
|
+
await this.current.send(message);
|
|
2013
|
+
} catch (err) {
|
|
2014
|
+
this.outboundQueue.push(message);
|
|
2015
|
+
this.scheduleReconnect(err);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
// Send a request directly and resolve when the matching response arrives
|
|
2019
|
+
// on the same connection. Used by onConnect handlers to await replay-attach
|
|
2020
|
+
// responses before letting the outbound queue drain. Bypasses the
|
|
2021
|
+
// connectGate intentionally.
|
|
2022
|
+
async request(message) {
|
|
2023
|
+
if (this.destroyed) {
|
|
2024
|
+
throw new Error("resilient ws stream is destroyed");
|
|
2025
|
+
}
|
|
2026
|
+
if (!this.current) {
|
|
2027
|
+
throw new Error("resilient ws stream not connected");
|
|
2028
|
+
}
|
|
2029
|
+
const id = message.id;
|
|
2030
|
+
const promise = new Promise((resolve2, reject) => {
|
|
2031
|
+
this.pendingRequests.set(id, { resolve: resolve2, reject });
|
|
2032
|
+
});
|
|
2033
|
+
try {
|
|
2034
|
+
await this.current.send(message);
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
this.pendingRequests.delete(id);
|
|
2037
|
+
throw err;
|
|
2038
|
+
}
|
|
2039
|
+
return promise;
|
|
2040
|
+
}
|
|
2041
|
+
async close() {
|
|
2042
|
+
this.destroyed = true;
|
|
2043
|
+
if (this.current) {
|
|
2044
|
+
await this.current.close().catch(() => void 0);
|
|
2045
|
+
}
|
|
2046
|
+
for (const handler of this.closeHandlers) {
|
|
2047
|
+
handler();
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async connectWithRetry() {
|
|
2051
|
+
let attempt = 0;
|
|
2052
|
+
let backoff = BACKOFF_INITIAL_MS;
|
|
2053
|
+
while (!this.destroyed) {
|
|
2054
|
+
try {
|
|
2055
|
+
const stream = await openWs(this.opts.url, this.opts.subprotocols);
|
|
2056
|
+
this.bindStream(stream);
|
|
2057
|
+
const wasFirst = this.firstConnect;
|
|
2058
|
+
this.firstConnect = false;
|
|
2059
|
+
this.connectGate = new Promise((resolve2) => {
|
|
2060
|
+
this.releaseConnectGate = resolve2;
|
|
2061
|
+
});
|
|
2062
|
+
try {
|
|
2063
|
+
if (this.opts.onConnect) {
|
|
2064
|
+
try {
|
|
2065
|
+
await this.opts.onConnect(wasFirst);
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
this.log(
|
|
2068
|
+
`hydra-acp: post-connect handler failed: ${err.message}`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
} finally {
|
|
2073
|
+
this.releaseConnectGate?.();
|
|
2074
|
+
this.releaseConnectGate = void 0;
|
|
2075
|
+
this.connectGate = void 0;
|
|
2076
|
+
}
|
|
2077
|
+
await this.flushQueue();
|
|
2078
|
+
return;
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
attempt += 1;
|
|
2081
|
+
if (this.opts.onConnectFailure) {
|
|
2082
|
+
this.opts.onConnectFailure(err);
|
|
2083
|
+
}
|
|
2084
|
+
if (attempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
2085
|
+
throw new Error(
|
|
2086
|
+
`hydra-acp: gave up reconnecting after ${attempt} attempts: ${err.message}`
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
this.log(
|
|
2090
|
+
`hydra-acp: connect attempt ${attempt} failed (${err.message}); retrying in ${backoff}ms`
|
|
2091
|
+
);
|
|
2092
|
+
await sleep3(backoff);
|
|
2093
|
+
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
bindStream(stream) {
|
|
2098
|
+
this.current = stream;
|
|
2099
|
+
stream.onMessage((msg) => {
|
|
2100
|
+
if (isResponse(msg)) {
|
|
2101
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
2102
|
+
if (pending) {
|
|
2103
|
+
this.pendingRequests.delete(msg.id);
|
|
2104
|
+
pending.resolve(msg);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
for (const handler of this.messageHandlers) {
|
|
2108
|
+
handler(msg);
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
stream.onClose((err) => {
|
|
2112
|
+
if (this.destroyed) {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
this.current = void 0;
|
|
2116
|
+
if (this.pendingRequests.size > 0) {
|
|
2117
|
+
const reason = err ?? new Error("ws closed before response");
|
|
2118
|
+
for (const { reject } of this.pendingRequests.values()) {
|
|
2119
|
+
reject(reason);
|
|
2120
|
+
}
|
|
2121
|
+
this.pendingRequests.clear();
|
|
2122
|
+
}
|
|
2123
|
+
this.scheduleReconnect(err);
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
async flushQueue() {
|
|
2127
|
+
if (!this.current) {
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
const queue = this.outboundQueue;
|
|
2131
|
+
this.outboundQueue = [];
|
|
2132
|
+
for (const msg of queue) {
|
|
2133
|
+
try {
|
|
2134
|
+
await this.current.send(msg);
|
|
2135
|
+
} catch (err) {
|
|
2136
|
+
this.outboundQueue.unshift(msg);
|
|
2137
|
+
this.scheduleReconnect(err);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
scheduleReconnect(err) {
|
|
2143
|
+
if (this.destroyed || this.reconnectInFlight) {
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
this.log(
|
|
2147
|
+
`hydra-acp: connection lost (${err?.message ?? "no error"}); reconnecting...`
|
|
2148
|
+
);
|
|
2149
|
+
if (this.opts.onDisconnect) {
|
|
2150
|
+
try {
|
|
2151
|
+
this.opts.onDisconnect(err);
|
|
2152
|
+
} catch (hookErr) {
|
|
2153
|
+
this.log(
|
|
2154
|
+
`hydra-acp: onDisconnect handler threw: ${hookErr.message}`
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
this.reconnectInFlight = (async () => {
|
|
2159
|
+
try {
|
|
2160
|
+
await this.connectWithRetry();
|
|
2161
|
+
} catch (final) {
|
|
2162
|
+
for (const handler of this.closeHandlers) {
|
|
2163
|
+
handler(final);
|
|
2164
|
+
}
|
|
2165
|
+
this.destroyed = true;
|
|
2166
|
+
} finally {
|
|
2167
|
+
this.reconnectInFlight = void 0;
|
|
2168
|
+
}
|
|
2169
|
+
})();
|
|
2170
|
+
}
|
|
2171
|
+
log(line) {
|
|
2172
|
+
if (this.opts.log) {
|
|
2173
|
+
this.opts.log(line);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
process.stderr.write(`${line}
|
|
2177
|
+
`);
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
1552
2180
|
}
|
|
1553
2181
|
});
|
|
1554
2182
|
|
|
1555
2183
|
// src/tui/history.ts
|
|
1556
|
-
import { promises as
|
|
2184
|
+
import { promises as fs10 } from "fs";
|
|
1557
2185
|
import * as path4 from "path";
|
|
1558
2186
|
async function loadHistory(file) {
|
|
1559
2187
|
let text;
|
|
1560
2188
|
try {
|
|
1561
|
-
text = await
|
|
2189
|
+
text = await fs10.readFile(file, "utf8");
|
|
1562
2190
|
} catch (err) {
|
|
1563
2191
|
if (err.code === "ENOENT") {
|
|
1564
2192
|
return [];
|
|
@@ -1598,9 +2226,9 @@ function appendEntry(history, entry) {
|
|
|
1598
2226
|
return out;
|
|
1599
2227
|
}
|
|
1600
2228
|
async function saveHistory(file, history) {
|
|
1601
|
-
await
|
|
2229
|
+
await fs10.mkdir(path4.dirname(file), { recursive: true });
|
|
1602
2230
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1603
|
-
await
|
|
2231
|
+
await fs10.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1604
2232
|
}
|
|
1605
2233
|
var HISTORY_CAP;
|
|
1606
2234
|
var init_history = __esm({
|
|
@@ -1641,6 +2269,26 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
1641
2269
|
title: s.title
|
|
1642
2270
|
}));
|
|
1643
2271
|
}
|
|
2272
|
+
async function killSession(config, id, fetchImpl = fetch) {
|
|
2273
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2274
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}/kill`, {
|
|
2275
|
+
method: "POST",
|
|
2276
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2277
|
+
});
|
|
2278
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
2279
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
async function deleteSession(config, id, fetchImpl = fetch) {
|
|
2283
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2284
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
2285
|
+
method: "DELETE",
|
|
2286
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2287
|
+
});
|
|
2288
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
2289
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
1644
2292
|
function pickMostRecent(sessions, cwd) {
|
|
1645
2293
|
const matching = sessions.filter((s) => s.cwd === cwd);
|
|
1646
2294
|
if (matching.length === 0) {
|
|
@@ -1668,147 +2316,401 @@ async function pickSession(term, opts) {
|
|
|
1668
2316
|
if (opts.sessions.length === 0) {
|
|
1669
2317
|
return { kind: "new" };
|
|
1670
2318
|
}
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
|
|
2319
|
+
const sortSessions = (sessions) => {
|
|
2320
|
+
const score = (s) => {
|
|
2321
|
+
if (s.status !== "live") {
|
|
2322
|
+
return 0;
|
|
2323
|
+
}
|
|
2324
|
+
return s.cwd === opts.cwd ? 2 : 1;
|
|
2325
|
+
};
|
|
2326
|
+
return [...sessions].sort((a, b) => {
|
|
2327
|
+
const tier = score(b) - score(a);
|
|
2328
|
+
if (tier !== 0) {
|
|
2329
|
+
return tier;
|
|
2330
|
+
}
|
|
2331
|
+
return b.updatedAt.localeCompare(a.updatedAt);
|
|
2332
|
+
});
|
|
2333
|
+
};
|
|
2334
|
+
let visible = sortSessions(opts.sessions);
|
|
2335
|
+
let rows = visible.map((s) => toRow(s, Date.now()));
|
|
2336
|
+
let widths = computeWidths(rows);
|
|
2337
|
+
let total = 1 + visible.length;
|
|
2338
|
+
let selectedIdx = 0;
|
|
2339
|
+
let scrollOffset = 0;
|
|
2340
|
+
let mode = "normal";
|
|
2341
|
+
let pendingAction = null;
|
|
2342
|
+
let transientStatus = null;
|
|
2343
|
+
let termHeight = readTermHeight(term);
|
|
2344
|
+
let termWidth = readTermWidth(term);
|
|
2345
|
+
let viewportSize = 0;
|
|
2346
|
+
let newSessionLabel = "";
|
|
2347
|
+
let headerLine = "";
|
|
2348
|
+
let sessionLines = [];
|
|
2349
|
+
let startRow = 1;
|
|
2350
|
+
const computeLayout = () => {
|
|
2351
|
+
termHeight = readTermHeight(term);
|
|
2352
|
+
termWidth = readTermWidth(term);
|
|
2353
|
+
const maxViewportRows = Math.max(3, termHeight - 6);
|
|
2354
|
+
viewportSize = Math.min(visible.length, maxViewportRows);
|
|
2355
|
+
const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
|
|
2356
|
+
newSessionLabel = formatNewSessionLabel(opts.cwd, rowMaxWidth);
|
|
2357
|
+
headerLine = formatRow(HEADER, widths, rowMaxWidth);
|
|
2358
|
+
sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth));
|
|
2359
|
+
};
|
|
2360
|
+
const rebuildRows = () => {
|
|
2361
|
+
rows = visible.map((s) => toRow(s, Date.now()));
|
|
2362
|
+
widths = computeWidths(rows);
|
|
2363
|
+
total = 1 + visible.length;
|
|
2364
|
+
computeLayout();
|
|
2365
|
+
};
|
|
2366
|
+
const adjustScroll = () => {
|
|
2367
|
+
if (selectedIdx === 0) {
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
const sessionIdx = selectedIdx - 1;
|
|
2371
|
+
if (sessionIdx < scrollOffset) {
|
|
2372
|
+
scrollOffset = sessionIdx;
|
|
2373
|
+
} else if (sessionIdx >= scrollOffset + viewportSize) {
|
|
2374
|
+
scrollOffset = sessionIdx - viewportSize + 1;
|
|
2375
|
+
} else if (scrollOffset + viewportSize > visible.length) {
|
|
2376
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
1674
2377
|
}
|
|
1675
|
-
return s.cwd === opts.cwd ? 2 : 1;
|
|
1676
2378
|
};
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2379
|
+
const paintNewItem = () => {
|
|
2380
|
+
if (selectedIdx === 0) {
|
|
2381
|
+
term.brightWhite.bgBlue.noFormat(`\u276F ${newSessionLabel}`);
|
|
2382
|
+
} else {
|
|
2383
|
+
term.noFormat(` ${newSessionLabel}`);
|
|
1681
2384
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const widths = computeWidths(rows);
|
|
1690
|
-
const newSessionLabel = `+ New session in ${opts.cwd}`;
|
|
1691
|
-
const items = [newSessionLabel, ...rows.map((r) => formatRow(r, widths))];
|
|
1692
|
-
term("\n");
|
|
1693
|
-
term.bold("Select a session")("\n");
|
|
1694
|
-
if (hiddenCold > 0) {
|
|
1695
|
-
term.dim(`(${hiddenCold} older cold session${hiddenCold === 1 ? "" : "s"} hidden; use \`hydra-acp sessions --all\` to view)
|
|
1696
|
-
`);
|
|
1697
|
-
}
|
|
1698
|
-
term.dim(formatRow(HEADER, widths))("\n");
|
|
1699
|
-
const onCtrlC = (name) => {
|
|
1700
|
-
if (name === "CTRL_C") {
|
|
1701
|
-
term.grabInput(false);
|
|
1702
|
-
term("\n");
|
|
1703
|
-
process.exit(130);
|
|
2385
|
+
};
|
|
2386
|
+
const paintSessionRow = (sessionIdx) => {
|
|
2387
|
+
const label = sessionLines[sessionIdx] ?? "";
|
|
2388
|
+
if (selectedIdx === sessionIdx + 1) {
|
|
2389
|
+
term.brightWhite.bgBlue.noFormat(`\u276F ${label}`);
|
|
2390
|
+
} else {
|
|
2391
|
+
term.noFormat(` ${label}`);
|
|
1704
2392
|
}
|
|
1705
2393
|
};
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
TAB: "next",
|
|
1721
|
-
SHIFT_TAB: "previous",
|
|
1722
|
-
HOME: "first",
|
|
1723
|
-
END: "last",
|
|
1724
|
-
ESCAPE: "cancel",
|
|
1725
|
-
CTRL_C: "cancel"
|
|
1726
|
-
}
|
|
1727
|
-
}).promise;
|
|
1728
|
-
} finally {
|
|
1729
|
-
term.off("key", onCtrlC);
|
|
1730
|
-
}
|
|
1731
|
-
term("\n");
|
|
1732
|
-
if (response.canceled || response.selectedIndex === void 0) {
|
|
1733
|
-
return { kind: "abort" };
|
|
1734
|
-
}
|
|
1735
|
-
if (response.selectedIndex === 0) {
|
|
1736
|
-
return { kind: "new" };
|
|
1737
|
-
}
|
|
1738
|
-
const session = visible[response.selectedIndex - 1];
|
|
1739
|
-
if (!session) {
|
|
1740
|
-
return { kind: "abort" };
|
|
1741
|
-
}
|
|
1742
|
-
const result = {
|
|
1743
|
-
kind: "attach",
|
|
1744
|
-
sessionId: session.sessionId
|
|
2394
|
+
const formatIndicator = () => {
|
|
2395
|
+
const above = scrollOffset;
|
|
2396
|
+
const below = Math.max(0, visible.length - scrollOffset - viewportSize);
|
|
2397
|
+
if (above === 0 && below === 0) {
|
|
2398
|
+
return "";
|
|
2399
|
+
}
|
|
2400
|
+
const parts = [];
|
|
2401
|
+
if (above > 0) {
|
|
2402
|
+
parts.push(`\u2191 ${above} above`);
|
|
2403
|
+
}
|
|
2404
|
+
if (below > 0) {
|
|
2405
|
+
parts.push(`\u2193 ${below} below`);
|
|
2406
|
+
}
|
|
2407
|
+
return ` ${parts.join(" \xB7 ")}`;
|
|
1745
2408
|
};
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2409
|
+
const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
|
|
2410
|
+
const paintIndicator = () => {
|
|
2411
|
+
term.moveTo(1, indicatorRow()).eraseLineAfter();
|
|
2412
|
+
if (mode === "confirm-kill" && pendingAction) {
|
|
2413
|
+
term.brightYellow.noFormat(` kill ${shortId2(pendingAction.sessionId)}? [y/N]`);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
if (mode === "confirm-delete" && pendingAction) {
|
|
2417
|
+
term.brightRed.noFormat(` delete ${shortId2(pendingAction.sessionId)}? [y/N]`);
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (mode === "busy" && pendingAction) {
|
|
2421
|
+
term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (transientStatus !== null) {
|
|
2425
|
+
term.dim.noFormat(` ${transientStatus}`);
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
term.dim.noFormat(formatIndicator());
|
|
1760
2429
|
};
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2430
|
+
const indicatorRow = () => startRow + 3 + viewportSize;
|
|
2431
|
+
const sessionRow = (sessionIdx) => startRow + 3 + (sessionIdx - scrollOffset);
|
|
2432
|
+
const renderFromScratch = () => {
|
|
2433
|
+
computeLayout();
|
|
2434
|
+
adjustScroll();
|
|
2435
|
+
startRow = 1;
|
|
2436
|
+
term.moveTo(1, 1).eraseDisplayBelow();
|
|
2437
|
+
paintNewItem();
|
|
2438
|
+
term("\n\n");
|
|
2439
|
+
term.dim.noFormat(` ${headerLine}`)("\n");
|
|
2440
|
+
for (let v = 0; v < viewportSize; v++) {
|
|
2441
|
+
paintSessionRow(scrollOffset + v);
|
|
2442
|
+
term("\n");
|
|
2443
|
+
}
|
|
2444
|
+
paintIndicator();
|
|
2445
|
+
term("\n");
|
|
1770
2446
|
};
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2447
|
+
const repaintNewItem = () => {
|
|
2448
|
+
term.moveTo(1, startRow).eraseLineAfter();
|
|
2449
|
+
paintNewItem();
|
|
2450
|
+
};
|
|
2451
|
+
const repaintSessionRow = (sessionIdx) => {
|
|
2452
|
+
if (sessionIdx < scrollOffset || sessionIdx >= scrollOffset + viewportSize) {
|
|
2453
|
+
return;
|
|
1777
2454
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
2455
|
+
term.moveTo(1, sessionRow(sessionIdx)).eraseLineAfter();
|
|
2456
|
+
paintSessionRow(sessionIdx);
|
|
2457
|
+
};
|
|
2458
|
+
const repaintViewport = () => {
|
|
2459
|
+
for (let v = 0; v < viewportSize; v++) {
|
|
2460
|
+
const row = startRow + 3 + v;
|
|
2461
|
+
term.moveTo(1, row).eraseLineAfter();
|
|
2462
|
+
const sessionIdx = scrollOffset + v;
|
|
2463
|
+
if (sessionIdx < visible.length) {
|
|
2464
|
+
paintSessionRow(sessionIdx);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
paintIndicator();
|
|
2468
|
+
};
|
|
2469
|
+
renderFromScratch();
|
|
2470
|
+
term.hideCursor();
|
|
2471
|
+
return await new Promise((resolve2) => {
|
|
2472
|
+
let resolved = false;
|
|
2473
|
+
const onResize = () => {
|
|
2474
|
+
if (resolved) {
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
renderFromScratch();
|
|
2478
|
+
};
|
|
2479
|
+
const cleanup = () => {
|
|
2480
|
+
if (resolved) {
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
resolved = true;
|
|
2484
|
+
term.off("key", onKey);
|
|
2485
|
+
term.off("resize", onResize);
|
|
2486
|
+
term.grabInput(false);
|
|
2487
|
+
term.hideCursor(false);
|
|
2488
|
+
term.moveTo(1, indicatorRow() + 1);
|
|
2489
|
+
term("\n");
|
|
2490
|
+
};
|
|
2491
|
+
const refresh = async (preferredId) => {
|
|
2492
|
+
try {
|
|
2493
|
+
const next = await listSessions(opts.config);
|
|
2494
|
+
visible = sortSessions(next);
|
|
2495
|
+
rebuildRows();
|
|
2496
|
+
if (preferredId !== void 0) {
|
|
2497
|
+
const idx = visible.findIndex((s) => s.sessionId === preferredId);
|
|
2498
|
+
if (idx >= 0) {
|
|
2499
|
+
selectedIdx = idx + 1;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
if (selectedIdx > total - 1) {
|
|
2503
|
+
selectedIdx = Math.max(0, total - 1);
|
|
2504
|
+
}
|
|
2505
|
+
if (scrollOffset + viewportSize > visible.length) {
|
|
2506
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
2507
|
+
}
|
|
2508
|
+
adjustScroll();
|
|
2509
|
+
renderFromScratch();
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
transientStatus = `refresh failed: ${err.message}`;
|
|
2512
|
+
renderFromScratch();
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
const performAction = async (kind) => {
|
|
2516
|
+
if (!pendingAction) {
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const target = pendingAction;
|
|
2520
|
+
mode = "busy";
|
|
2521
|
+
paintIndicator();
|
|
2522
|
+
try {
|
|
2523
|
+
if (kind === "kill") {
|
|
2524
|
+
await killSession(opts.config, target.sessionId);
|
|
2525
|
+
} else {
|
|
2526
|
+
await deleteSession(opts.config, target.sessionId);
|
|
2527
|
+
}
|
|
2528
|
+
mode = "normal";
|
|
2529
|
+
pendingAction = null;
|
|
2530
|
+
await refresh(kind === "kill" ? target.sessionId : void 0);
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
mode = "normal";
|
|
2533
|
+
pendingAction = null;
|
|
2534
|
+
transientStatus = `${kind} failed: ${err.message}`;
|
|
2535
|
+
paintIndicator();
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
const move = (delta) => {
|
|
2539
|
+
const next = Math.min(total - 1, Math.max(0, selectedIdx + delta));
|
|
2540
|
+
if (next === selectedIdx) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const old = selectedIdx;
|
|
2544
|
+
const oldScroll = scrollOffset;
|
|
2545
|
+
selectedIdx = next;
|
|
2546
|
+
adjustScroll();
|
|
2547
|
+
if (scrollOffset !== oldScroll) {
|
|
2548
|
+
repaintViewport();
|
|
2549
|
+
if (old === 0 || selectedIdx === 0) {
|
|
2550
|
+
repaintNewItem();
|
|
2551
|
+
}
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (old === 0) {
|
|
2555
|
+
repaintNewItem();
|
|
2556
|
+
} else {
|
|
2557
|
+
repaintSessionRow(old - 1);
|
|
2558
|
+
}
|
|
2559
|
+
if (selectedIdx === 0) {
|
|
2560
|
+
repaintNewItem();
|
|
2561
|
+
} else {
|
|
2562
|
+
repaintSessionRow(selectedIdx - 1);
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
const clearTransient = () => {
|
|
2566
|
+
if (transientStatus === null) {
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
transientStatus = null;
|
|
2570
|
+
paintIndicator();
|
|
2571
|
+
return true;
|
|
2572
|
+
};
|
|
2573
|
+
const onKey = (name, _matches, data) => {
|
|
2574
|
+
if (mode === "busy") {
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
if (mode === "confirm-kill" || mode === "confirm-delete") {
|
|
2578
|
+
if (data?.isCharacter && (name === "y" || name === "Y")) {
|
|
2579
|
+
const kind = mode === "confirm-kill" ? "kill" : "delete";
|
|
2580
|
+
void performAction(kind);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
if (name === "ESCAPE" || name === "CTRL_C" || name === "ENTER" || name === "KP_ENTER" || data?.isCharacter && (name === "n" || name === "N")) {
|
|
2584
|
+
mode = "normal";
|
|
2585
|
+
pendingAction = null;
|
|
2586
|
+
paintIndicator();
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
clearTransient();
|
|
2592
|
+
if (data?.isCharacter) {
|
|
2593
|
+
if ((name === "k" || name === "K") && selectedIdx > 0) {
|
|
2594
|
+
const session = visible[selectedIdx - 1];
|
|
2595
|
+
if (!session) {
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
pendingAction = {
|
|
2599
|
+
sessionId: session.sessionId,
|
|
2600
|
+
cwd: session.cwd,
|
|
2601
|
+
status: session.status
|
|
2602
|
+
};
|
|
2603
|
+
mode = "confirm-kill";
|
|
2604
|
+
paintIndicator();
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
if ((name === "d" || name === "D") && selectedIdx > 0) {
|
|
2608
|
+
const session = visible[selectedIdx - 1];
|
|
2609
|
+
if (!session) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (session.status === "live") {
|
|
2613
|
+
transientStatus = "session is live \u2014 press k to kill it first";
|
|
2614
|
+
paintIndicator();
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
pendingAction = {
|
|
2618
|
+
sessionId: session.sessionId,
|
|
2619
|
+
cwd: session.cwd,
|
|
2620
|
+
status: session.status
|
|
2621
|
+
};
|
|
2622
|
+
mode = "confirm-delete";
|
|
2623
|
+
paintIndicator();
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
switch (name) {
|
|
2629
|
+
case "UP":
|
|
2630
|
+
case "SHIFT_TAB":
|
|
2631
|
+
move(-1);
|
|
2632
|
+
return;
|
|
2633
|
+
case "DOWN":
|
|
2634
|
+
case "TAB":
|
|
2635
|
+
move(1);
|
|
2636
|
+
return;
|
|
2637
|
+
case "PAGE_UP":
|
|
2638
|
+
move(-viewportSize);
|
|
2639
|
+
return;
|
|
2640
|
+
case "PAGE_DOWN":
|
|
2641
|
+
move(viewportSize);
|
|
2642
|
+
return;
|
|
2643
|
+
case "HOME":
|
|
2644
|
+
move(-total);
|
|
2645
|
+
return;
|
|
2646
|
+
case "END":
|
|
2647
|
+
move(total);
|
|
2648
|
+
return;
|
|
2649
|
+
case "ENTER":
|
|
2650
|
+
case "KP_ENTER": {
|
|
2651
|
+
cleanup();
|
|
2652
|
+
if (selectedIdx === 0) {
|
|
2653
|
+
resolve2({ kind: "new" });
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
const session = visible[selectedIdx - 1];
|
|
2657
|
+
if (!session) {
|
|
2658
|
+
resolve2({ kind: "abort" });
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
const result = {
|
|
2662
|
+
kind: "attach",
|
|
2663
|
+
sessionId: session.sessionId
|
|
2664
|
+
};
|
|
2665
|
+
if (session.agentId !== void 0) {
|
|
2666
|
+
result.agentId = session.agentId;
|
|
2667
|
+
}
|
|
2668
|
+
resolve2(result);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
case "ESCAPE":
|
|
2672
|
+
case "CTRL_C":
|
|
2673
|
+
cleanup();
|
|
2674
|
+
resolve2({ kind: "abort" });
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
term.grabInput({});
|
|
2679
|
+
term.on("key", onKey);
|
|
2680
|
+
term.on("resize", onResize);
|
|
2681
|
+
});
|
|
1780
2682
|
}
|
|
1781
|
-
function
|
|
1782
|
-
return
|
|
1783
|
-
r.session.padEnd(w.session),
|
|
1784
|
-
r.upstream.padEnd(w.upstream),
|
|
1785
|
-
r.status.padEnd(w.status),
|
|
1786
|
-
r.clients.padStart(w.clients),
|
|
1787
|
-
r.agent.padEnd(w.agent),
|
|
1788
|
-
r.title.padEnd(w.title),
|
|
1789
|
-
r.cwd
|
|
1790
|
-
].join(" ");
|
|
2683
|
+
function readTermHeight(term) {
|
|
2684
|
+
return term.height ?? 24;
|
|
1791
2685
|
}
|
|
1792
|
-
|
|
2686
|
+
function readTermWidth(term) {
|
|
2687
|
+
return term.width ?? 80;
|
|
2688
|
+
}
|
|
2689
|
+
function formatNewSessionLabel(cwd, maxWidth) {
|
|
2690
|
+
const prefix = "+ New session in ";
|
|
2691
|
+
const budget = Math.max(1, maxWidth - prefix.length);
|
|
2692
|
+
return prefix + truncateMiddle(cwd, budget);
|
|
2693
|
+
}
|
|
2694
|
+
var ROW_PREFIX_WIDTH;
|
|
1793
2695
|
var init_picker = __esm({
|
|
1794
2696
|
"src/tui/picker.ts"() {
|
|
1795
2697
|
"use strict";
|
|
2698
|
+
init_session_row();
|
|
1796
2699
|
init_session();
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
upstream: "UPSTREAM",
|
|
1800
|
-
status: "STATUS",
|
|
1801
|
-
clients: "CLIENTS",
|
|
1802
|
-
agent: "AGENT",
|
|
1803
|
-
title: "TITLE",
|
|
1804
|
-
cwd: "CWD"
|
|
1805
|
-
};
|
|
2700
|
+
init_discovery();
|
|
2701
|
+
ROW_PREFIX_WIDTH = 2;
|
|
1806
2702
|
}
|
|
1807
2703
|
});
|
|
1808
2704
|
|
|
1809
2705
|
// src/tui/screen.ts
|
|
1810
2706
|
import stringWidth from "string-width";
|
|
1811
2707
|
import wrapAnsi from "wrap-ansi";
|
|
2708
|
+
function formattedLineSig(zone, width, line) {
|
|
2709
|
+
if (!line) {
|
|
2710
|
+
return `${zone}|${width}|empty`;
|
|
2711
|
+
}
|
|
2712
|
+
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}`;
|
|
2713
|
+
}
|
|
1812
2714
|
function computePromptVisualRows(buffer, room) {
|
|
1813
2715
|
const rows = [];
|
|
1814
2716
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -1893,16 +2795,16 @@ function writeStyled(term, text, style) {
|
|
|
1893
2795
|
term.bold.red.noFormat(text);
|
|
1894
2796
|
return;
|
|
1895
2797
|
case "tool-status-pending":
|
|
1896
|
-
term.dim.
|
|
2798
|
+
term.dim.noFormat(text);
|
|
1897
2799
|
return;
|
|
1898
2800
|
case "tool-status-running":
|
|
1899
|
-
term.
|
|
2801
|
+
term.brightYellow.noFormat(text);
|
|
1900
2802
|
return;
|
|
1901
2803
|
case "tool-status-cancelled":
|
|
1902
2804
|
term.dim.noFormat(text);
|
|
1903
2805
|
return;
|
|
1904
2806
|
case "plan":
|
|
1905
|
-
term.
|
|
2807
|
+
term.brightYellow.noFormat(text);
|
|
1906
2808
|
return;
|
|
1907
2809
|
case "plan-done":
|
|
1908
2810
|
term.green.noFormat(text);
|
|
@@ -2087,13 +2989,15 @@ function mapKeyName(name) {
|
|
|
2087
2989
|
return "ctrl-u";
|
|
2088
2990
|
case "CTRL_W":
|
|
2089
2991
|
return "ctrl-w";
|
|
2992
|
+
case "CTRL_Y":
|
|
2993
|
+
return "ctrl-y";
|
|
2090
2994
|
case "ESCAPE":
|
|
2091
2995
|
return "escape";
|
|
2092
2996
|
default:
|
|
2093
2997
|
return null;
|
|
2094
2998
|
}
|
|
2095
2999
|
}
|
|
2096
|
-
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, Screen, shortId;
|
|
3000
|
+
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, shortId;
|
|
2097
3001
|
var init_screen = __esm({
|
|
2098
3002
|
"src/tui/screen.ts"() {
|
|
2099
3003
|
"use strict";
|
|
@@ -2107,6 +3011,7 @@ var init_screen = __esm({
|
|
|
2107
3011
|
MAX_COMPLETION_ROWS = 6;
|
|
2108
3012
|
CONFIRM_PROMPT_ROWS = 2;
|
|
2109
3013
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
3014
|
+
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
2110
3015
|
Screen = class {
|
|
2111
3016
|
term;
|
|
2112
3017
|
dispatcher;
|
|
@@ -2127,6 +3032,27 @@ var init_screen = __esm({
|
|
|
2127
3032
|
lastRepaintAt = 0;
|
|
2128
3033
|
throttledRepaintTimer = null;
|
|
2129
3034
|
contentRepaintThrottleMs;
|
|
3035
|
+
maxScrollbackLines;
|
|
3036
|
+
// Wrap memoization: each FormattedLine that lands in this.lines gets a
|
|
3037
|
+
// monotonic id assigned via trackLine(); wrapCache holds the pre-wrapped
|
|
3038
|
+
// FormattedLine[] for that id at wrapCacheWidth. Width changes flush the
|
|
3039
|
+
// whole cache; in-place body mutation (streaming) and splices invalidate
|
|
3040
|
+
// affected ids. Result: steady-state repaints only wrap newly-appended
|
|
3041
|
+
// lines, not the entire history.
|
|
3042
|
+
nextLineId = 1;
|
|
3043
|
+
lineIds = /* @__PURE__ */ new WeakMap();
|
|
3044
|
+
wrapCache = /* @__PURE__ */ new Map();
|
|
3045
|
+
wrapCacheWidth = 0;
|
|
3046
|
+
// Per-row signature of what was painted to each terminal row on the
|
|
3047
|
+
// previous repaint. drawX methods funnel through paintRow(), which
|
|
3048
|
+
// skips the moveTo+eraseLineAfter+write sequence when the new
|
|
3049
|
+
// signature matches the previous frame. Eliminates flicker during
|
|
3050
|
+
// the 1Hz busy-tick: only rows whose content actually changed
|
|
3051
|
+
// (banner elapsed, tools-block summary) get re-emitted instead of
|
|
3052
|
+
// every visible row. Cleared on dimension change.
|
|
3053
|
+
lastFrameRows = /* @__PURE__ */ new Map();
|
|
3054
|
+
lastFrameW = 0;
|
|
3055
|
+
lastFrameH = 0;
|
|
2130
3056
|
permissionPrompt = null;
|
|
2131
3057
|
confirmPrompt = null;
|
|
2132
3058
|
completions = [];
|
|
@@ -2141,6 +3067,7 @@ var init_screen = __esm({
|
|
|
2141
3067
|
queued: 0
|
|
2142
3068
|
};
|
|
2143
3069
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
3070
|
+
lastWindowTitle = null;
|
|
2144
3071
|
resizeHandler;
|
|
2145
3072
|
keyHandler;
|
|
2146
3073
|
mouseHandler;
|
|
@@ -2161,6 +3088,7 @@ var init_screen = __esm({
|
|
|
2161
3088
|
this.dispatcher = opts.dispatcher;
|
|
2162
3089
|
this.onKey = opts.onKey;
|
|
2163
3090
|
this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
|
|
3091
|
+
this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
|
|
2164
3092
|
this.resizeHandler = () => this.repaint();
|
|
2165
3093
|
this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
|
|
2166
3094
|
this.mouseHandler = (name) => this.handleMouse(name);
|
|
@@ -2261,13 +3189,17 @@ var init_screen = __esm({
|
|
|
2261
3189
|
}
|
|
2262
3190
|
this.streamingActive = false;
|
|
2263
3191
|
this.lines.push(...lines);
|
|
3192
|
+
this.trackLines(lines);
|
|
2264
3193
|
this.adjustScrollForLineChange(lines.length);
|
|
3194
|
+
this.trimScrollback();
|
|
2265
3195
|
this.scheduleRepaint();
|
|
2266
3196
|
}
|
|
2267
3197
|
appendLine(line) {
|
|
2268
3198
|
this.streamingActive = false;
|
|
2269
3199
|
this.lines.push(line);
|
|
3200
|
+
this.trackLine(line);
|
|
2270
3201
|
this.adjustScrollForLineChange(1);
|
|
3202
|
+
this.trimScrollback();
|
|
2271
3203
|
this.scheduleRepaint();
|
|
2272
3204
|
}
|
|
2273
3205
|
// When scrolled away from the bottom, shift scrollOffset to keep the
|
|
@@ -2279,6 +3211,40 @@ var init_screen = __esm({
|
|
|
2279
3211
|
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
2280
3212
|
}
|
|
2281
3213
|
}
|
|
3214
|
+
trackLine(line) {
|
|
3215
|
+
this.lineIds.set(line, this.nextLineId++);
|
|
3216
|
+
}
|
|
3217
|
+
trackLines(lines) {
|
|
3218
|
+
for (const line of lines) {
|
|
3219
|
+
this.trackLine(line);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
forgetLine(line) {
|
|
3223
|
+
const id = this.lineIds.get(line);
|
|
3224
|
+
if (id !== void 0) {
|
|
3225
|
+
this.wrapCache.delete(id);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
// Drop oldest lines once scrollback exceeds the configured cap. Removes
|
|
3229
|
+
// their wrap-cache entries and shifts keyedBlocks indices in sync;
|
|
3230
|
+
// blocks whose lines fully fell off the head are dropped (a later
|
|
3231
|
+
// upsert for that key will start a fresh block at the bottom).
|
|
3232
|
+
trimScrollback() {
|
|
3233
|
+
const overflow = this.lines.length - this.maxScrollbackLines;
|
|
3234
|
+
if (overflow <= 0) {
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
const removed = this.lines.splice(0, overflow);
|
|
3238
|
+
for (const line of removed) {
|
|
3239
|
+
this.forgetLine(line);
|
|
3240
|
+
}
|
|
3241
|
+
for (const [key, range] of [...this.keyedBlocks.entries()]) {
|
|
3242
|
+
range.start -= overflow;
|
|
3243
|
+
if (range.start < 0) {
|
|
3244
|
+
this.keyedBlocks.delete(key);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
2282
3248
|
// Append-or-replace a single-line block keyed by `key`. Thin wrapper
|
|
2283
3249
|
// around upsertLines for the common one-row case (tool calls).
|
|
2284
3250
|
upsertLine(key, line) {
|
|
@@ -2300,7 +3266,15 @@ var init_screen = __esm({
|
|
|
2300
3266
|
touchesEnd = oldEnd >= this.lines.length;
|
|
2301
3267
|
const delta = newLines.length - existing.count;
|
|
2302
3268
|
scrollDelta = delta;
|
|
2303
|
-
this.lines.splice(
|
|
3269
|
+
const removed = this.lines.splice(
|
|
3270
|
+
existing.start,
|
|
3271
|
+
existing.count,
|
|
3272
|
+
...newLines
|
|
3273
|
+
);
|
|
3274
|
+
for (const line of removed) {
|
|
3275
|
+
this.forgetLine(line);
|
|
3276
|
+
}
|
|
3277
|
+
this.trackLines(newLines);
|
|
2304
3278
|
existing.count = newLines.length;
|
|
2305
3279
|
if (delta !== 0) {
|
|
2306
3280
|
for (const [k, range] of this.keyedBlocks) {
|
|
@@ -2317,11 +3291,13 @@ var init_screen = __esm({
|
|
|
2317
3291
|
count: newLines.length
|
|
2318
3292
|
});
|
|
2319
3293
|
this.lines.push(...newLines);
|
|
3294
|
+
this.trackLines(newLines);
|
|
2320
3295
|
}
|
|
2321
3296
|
if (touchesEnd) {
|
|
2322
3297
|
this.streamingActive = false;
|
|
2323
3298
|
}
|
|
2324
3299
|
this.adjustScrollForLineChange(scrollDelta);
|
|
3300
|
+
this.trimScrollback();
|
|
2325
3301
|
this.scheduleRepaint();
|
|
2326
3302
|
}
|
|
2327
3303
|
// Append fragments of a streaming message (e.g. agent_message_chunk). The
|
|
@@ -2338,6 +3314,7 @@ var init_screen = __esm({
|
|
|
2338
3314
|
if (this.streamingActive && this.lines.length > 0) {
|
|
2339
3315
|
const last = this.lines[this.lines.length - 1];
|
|
2340
3316
|
if (last) {
|
|
3317
|
+
this.forgetLine(last);
|
|
2341
3318
|
last.body += first ?? "";
|
|
2342
3319
|
}
|
|
2343
3320
|
} else {
|
|
@@ -2345,7 +3322,9 @@ var init_screen = __esm({
|
|
|
2345
3322
|
const last = this.lines[this.lines.length - 1];
|
|
2346
3323
|
const isBlank = last && last.body === "" && (!last.prefix || last.prefix === "");
|
|
2347
3324
|
if (!isBlank) {
|
|
2348
|
-
|
|
3325
|
+
const sep = { body: "" };
|
|
3326
|
+
this.lines.push(sep);
|
|
3327
|
+
this.trackLine(sep);
|
|
2349
3328
|
added += 1;
|
|
2350
3329
|
}
|
|
2351
3330
|
}
|
|
@@ -2358,25 +3337,48 @@ var init_screen = __esm({
|
|
|
2358
3337
|
initial.prefixStyle = prefixStyle;
|
|
2359
3338
|
}
|
|
2360
3339
|
this.lines.push(initial);
|
|
3340
|
+
this.trackLine(initial);
|
|
2361
3341
|
added += 1;
|
|
2362
3342
|
}
|
|
2363
3343
|
const continuationPrefix = " ".repeat(prefix.length);
|
|
2364
3344
|
for (const piece of rest) {
|
|
2365
|
-
|
|
3345
|
+
const cont = {
|
|
2366
3346
|
prefix: continuationPrefix,
|
|
2367
3347
|
body: piece,
|
|
2368
3348
|
bodyStyle
|
|
2369
|
-
}
|
|
3349
|
+
};
|
|
3350
|
+
this.lines.push(cont);
|
|
3351
|
+
this.trackLine(cont);
|
|
2370
3352
|
added += 1;
|
|
2371
3353
|
}
|
|
2372
3354
|
this.streamingActive = true;
|
|
2373
3355
|
this.adjustScrollForLineChange(added);
|
|
3356
|
+
this.trimScrollback();
|
|
2374
3357
|
this.scheduleRepaint();
|
|
2375
3358
|
}
|
|
2376
3359
|
setHeader(header) {
|
|
2377
3360
|
this.header = { ...this.header, ...header };
|
|
3361
|
+
this.syncWindowTitle();
|
|
2378
3362
|
this.repaint();
|
|
2379
3363
|
}
|
|
3364
|
+
// Push the current session title (or short session id, as fallback) to
|
|
3365
|
+
// the host terminal via OSC 2. Supported by xterm/foot/iTerm2/Alacritty/
|
|
3366
|
+
// most modern emulators; ignored harmlessly elsewhere.
|
|
3367
|
+
syncWindowTitle() {
|
|
3368
|
+
const title = this.header.title?.trim();
|
|
3369
|
+
const fallback = shortId(this.header.sessionId) || "hydra";
|
|
3370
|
+
const raw = title && title.length > 0 ? title : fallback;
|
|
3371
|
+
const clean = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
3372
|
+
if (clean === this.lastWindowTitle) {
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
this.lastWindowTitle = clean;
|
|
3376
|
+
process.stdout.write(`\x1B]2;${clean}\x1B\\`);
|
|
3377
|
+
}
|
|
3378
|
+
clearWindowTitle() {
|
|
3379
|
+
this.lastWindowTitle = null;
|
|
3380
|
+
process.stdout.write("\x1B]2;\x1B\\");
|
|
3381
|
+
}
|
|
2380
3382
|
setBanner(banner) {
|
|
2381
3383
|
this.banner = { ...this.banner, ...banner };
|
|
2382
3384
|
this.drawBanner();
|
|
@@ -2385,6 +3387,8 @@ var init_screen = __esm({
|
|
|
2385
3387
|
clearScrollback() {
|
|
2386
3388
|
this.lines = [];
|
|
2387
3389
|
this.keyedBlocks.clear();
|
|
3390
|
+
this.wrapCache.clear();
|
|
3391
|
+
this.wrapCacheWidth = 0;
|
|
2388
3392
|
this.streamingActive = false;
|
|
2389
3393
|
this.scrollOffset = 0;
|
|
2390
3394
|
this.repaint();
|
|
@@ -2407,7 +3411,10 @@ var init_screen = __esm({
|
|
|
2407
3411
|
return;
|
|
2408
3412
|
}
|
|
2409
3413
|
const touchesEnd = existing.start + existing.count >= this.lines.length;
|
|
2410
|
-
this.lines.splice(existing.start, existing.count);
|
|
3414
|
+
const removed = this.lines.splice(existing.start, existing.count);
|
|
3415
|
+
for (const line of removed) {
|
|
3416
|
+
this.forgetLine(line);
|
|
3417
|
+
}
|
|
2411
3418
|
this.keyedBlocks.delete(key);
|
|
2412
3419
|
for (const [, range] of this.keyedBlocks) {
|
|
2413
3420
|
if (range.start > existing.start) {
|
|
@@ -2481,9 +3488,12 @@ var init_screen = __esm({
|
|
|
2481
3488
|
if (last && last.body === "" && (last.prefix === void 0 || last.prefix === "")) {
|
|
2482
3489
|
return;
|
|
2483
3490
|
}
|
|
2484
|
-
|
|
3491
|
+
const sep = { body: "" };
|
|
3492
|
+
this.lines.push(sep);
|
|
3493
|
+
this.trackLine(sep);
|
|
2485
3494
|
this.streamingActive = false;
|
|
2486
3495
|
this.adjustScrollForLineChange(1);
|
|
3496
|
+
this.trimScrollback();
|
|
2487
3497
|
this.scheduleRepaint();
|
|
2488
3498
|
}
|
|
2489
3499
|
// The dispatcher is the source of truth for prompt state. If the prompt
|
|
@@ -2555,9 +3565,11 @@ var init_screen = __esm({
|
|
|
2555
3565
|
return Math.max(0, bottom - top + 1);
|
|
2556
3566
|
}
|
|
2557
3567
|
maxScrollOffset() {
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
3568
|
+
const { rows } = this.wrapTail(
|
|
3569
|
+
this.term.width,
|
|
3570
|
+
Number.POSITIVE_INFINITY
|
|
3571
|
+
);
|
|
3572
|
+
return Math.max(0, rows.length - this.scrollbackVisibleRows());
|
|
2561
3573
|
}
|
|
2562
3574
|
// Used by content mutators to coalesce rapid updates. Repaints fire
|
|
2563
3575
|
// at most once per contentRepaintThrottleMs; if a paint happened
|
|
@@ -2590,6 +3602,22 @@ var init_screen = __esm({
|
|
|
2590
3602
|
this.repaint();
|
|
2591
3603
|
}, this.contentRepaintThrottleMs - elapsed);
|
|
2592
3604
|
}
|
|
3605
|
+
// Funnel for every row that any drawX method renders. Skips emitting
|
|
3606
|
+
// moveTo+eraseLineAfter+paint when the row's signature matches the
|
|
3607
|
+
// previous frame's. The signature must capture everything that affects
|
|
3608
|
+
// visible output for that row (width, FormattedLine fields, banner
|
|
3609
|
+
// state, etc.) so identical sigs guarantee identical bytes.
|
|
3610
|
+
paintRow(row, signature, paint) {
|
|
3611
|
+
if (row < 1 || row > this.term.height) {
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
if (this.lastFrameRows.get(row) === signature) {
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
this.lastFrameRows.set(row, signature);
|
|
3618
|
+
this.term.moveTo(1, row).eraseLineAfter();
|
|
3619
|
+
paint();
|
|
3620
|
+
}
|
|
2593
3621
|
repaint() {
|
|
2594
3622
|
if (this.repaintPaused > 0) {
|
|
2595
3623
|
this.repaintPending = true;
|
|
@@ -2605,6 +3633,11 @@ var init_screen = __esm({
|
|
|
2605
3633
|
if (w < 20 || h < 8) {
|
|
2606
3634
|
return;
|
|
2607
3635
|
}
|
|
3636
|
+
if (w !== this.lastFrameW || h !== this.lastFrameH) {
|
|
3637
|
+
this.lastFrameRows.clear();
|
|
3638
|
+
this.lastFrameW = w;
|
|
3639
|
+
this.lastFrameH = h;
|
|
3640
|
+
}
|
|
2608
3641
|
this.drawHeader();
|
|
2609
3642
|
this.drawSeparator(HEADER_ROWS);
|
|
2610
3643
|
this.drawScrollback();
|
|
@@ -2620,22 +3653,40 @@ var init_screen = __esm({
|
|
|
2620
3653
|
}
|
|
2621
3654
|
drawHeader() {
|
|
2622
3655
|
const w = this.term.width;
|
|
2623
|
-
this.term.moveTo(1, 1).eraseLineAfter();
|
|
2624
3656
|
const usage = formatUsage(this.header.usage);
|
|
2625
|
-
const
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
this.
|
|
2630
|
-
|
|
2631
|
-
|
|
3657
|
+
const sid = shortId(this.header.sessionId);
|
|
3658
|
+
const title = this.header.title?.trim();
|
|
3659
|
+
const sig = `hdr|${w}|${this.header.agent}|${this.header.cwd}|${sid}|${title ?? ""}|${usage ?? ""}`;
|
|
3660
|
+
this.paintRow(1, sig, () => {
|
|
3661
|
+
const fixed = "hydra \xB7 ".length + this.header.agent.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
|
|
3662
|
+
const variableRoom = Math.max(8, w - fixed);
|
|
3663
|
+
let cwdRoom;
|
|
3664
|
+
let titleRoom;
|
|
3665
|
+
if (title) {
|
|
3666
|
+
const cwdMin = Math.min(this.header.cwd.length, 12);
|
|
3667
|
+
const titleCap = Math.max(8, variableRoom - cwdMin);
|
|
3668
|
+
titleRoom = Math.min(title.length, titleCap);
|
|
3669
|
+
cwdRoom = Math.max(8, variableRoom - titleRoom);
|
|
3670
|
+
} else {
|
|
3671
|
+
titleRoom = 0;
|
|
3672
|
+
cwdRoom = variableRoom;
|
|
3673
|
+
}
|
|
3674
|
+
this.term.bold("hydra")(" \xB7 ").cyan.noFormat(this.header.agent)(" \xB7 ").dim.noFormat(truncate(this.header.cwd, cwdRoom))(" \xB7 ").yellow(sid);
|
|
3675
|
+
if (title) {
|
|
3676
|
+
this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
|
|
3677
|
+
}
|
|
3678
|
+
if (usage) {
|
|
3679
|
+
const col = Math.max(1, w - usage.length + 1);
|
|
3680
|
+
this.term.moveTo(col, 1);
|
|
3681
|
+
this.term.dim(usage);
|
|
3682
|
+
}
|
|
3683
|
+
});
|
|
2632
3684
|
}
|
|
2633
3685
|
drawSeparator(row) {
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
this.term.dim("\u2500".repeat(this.term.width));
|
|
3686
|
+
const w = this.term.width;
|
|
3687
|
+
this.paintRow(row, `sep|${w}`, () => {
|
|
3688
|
+
this.term.dim("\u2500".repeat(w));
|
|
3689
|
+
});
|
|
2639
3690
|
}
|
|
2640
3691
|
drawScrollback() {
|
|
2641
3692
|
const w = this.term.width;
|
|
@@ -2644,21 +3695,30 @@ var init_screen = __esm({
|
|
|
2644
3695
|
if (visibleRows <= 0) {
|
|
2645
3696
|
return;
|
|
2646
3697
|
}
|
|
2647
|
-
const wrapped = this.
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3698
|
+
const { rows: wrapped, exhausted } = this.wrapTail(
|
|
3699
|
+
w,
|
|
3700
|
+
visibleRows + this.scrollOffset
|
|
3701
|
+
);
|
|
3702
|
+
if (exhausted) {
|
|
3703
|
+
const max = Math.max(0, wrapped.length - visibleRows);
|
|
3704
|
+
if (this.scrollOffset > max) {
|
|
3705
|
+
this.scrollOffset = max;
|
|
3706
|
+
}
|
|
2651
3707
|
}
|
|
2652
3708
|
const end = wrapped.length - this.scrollOffset;
|
|
2653
3709
|
const start = Math.max(0, end - visibleRows);
|
|
2654
3710
|
const slice = wrapped.slice(start, end);
|
|
3711
|
+
const padTop = Math.max(0, visibleRows - slice.length);
|
|
2655
3712
|
for (let i = 0; i < visibleRows; i++) {
|
|
2656
3713
|
const row = top + i;
|
|
2657
|
-
|
|
2658
|
-
const line = slice[
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
3714
|
+
const sliceIdx = i - padTop;
|
|
3715
|
+
const line = sliceIdx >= 0 ? slice[sliceIdx] : void 0;
|
|
3716
|
+
const sig = formattedLineSig("sb", w, line);
|
|
3717
|
+
this.paintRow(row, sig, () => {
|
|
3718
|
+
if (line) {
|
|
3719
|
+
this.writeFormattedLine(line, w);
|
|
3720
|
+
}
|
|
3721
|
+
});
|
|
2662
3722
|
}
|
|
2663
3723
|
}
|
|
2664
3724
|
queuedRows() {
|
|
@@ -2689,26 +3749,27 @@ var init_screen = __esm({
|
|
|
2689
3749
|
}
|
|
2690
3750
|
for (let i = 0; i < rows; i++) {
|
|
2691
3751
|
const row = completionTop + i;
|
|
2692
|
-
this.term.moveTo(1, row).eraseLineAfter();
|
|
2693
3752
|
const item = this.completions[i];
|
|
2694
|
-
if (!item) {
|
|
2695
|
-
continue;
|
|
2696
|
-
}
|
|
2697
3753
|
const isLast = i === rows - 1 && this.completions.length > MAX_COMPLETION_ROWS;
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
)
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
3754
|
+
const overflow = this.completions.length - MAX_COMPLETION_ROWS + 1;
|
|
3755
|
+
const sig = item ? isLast ? `comp|${w}|overflow|${overflow}` : `comp|${w}|${nameWidth}|${item.name}|${item.description ?? ""}` : `comp|${w}|empty`;
|
|
3756
|
+
this.paintRow(row, sig, () => {
|
|
3757
|
+
if (!item) {
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
if (isLast) {
|
|
3761
|
+
this.term.dim(` + ${overflow} more match(es)`);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
const namePadded = item.name.padEnd(nameWidth);
|
|
3765
|
+
const desc = item.description ?? "";
|
|
3766
|
+
const remaining = w - namePadded.length - 4;
|
|
3767
|
+
const truncated = remaining > 0 ? truncate(desc, remaining) : "";
|
|
3768
|
+
this.term(" ").brightCyan(namePadded);
|
|
3769
|
+
if (truncated.length > 0) {
|
|
3770
|
+
this.term(" ").dim(truncated);
|
|
3771
|
+
}
|
|
3772
|
+
});
|
|
2712
3773
|
}
|
|
2713
3774
|
}
|
|
2714
3775
|
drawQueuedZone() {
|
|
@@ -2723,17 +3784,19 @@ var init_screen = __esm({
|
|
|
2723
3784
|
const queuedTop = queuedBottom - rows + 1;
|
|
2724
3785
|
for (let i = 0; i < rows; i++) {
|
|
2725
3786
|
const row = queuedTop + i;
|
|
2726
|
-
this.term.moveTo(1, row).eraseLineAfter();
|
|
2727
3787
|
const text = this.queuedTexts[i];
|
|
2728
|
-
if (text === void 0) {
|
|
2729
|
-
continue;
|
|
2730
|
-
}
|
|
2731
3788
|
const isLast = i === rows - 1 && this.queuedTexts.length > MAX_QUEUED_ROWS;
|
|
2732
3789
|
const overflow = this.queuedTexts.length - MAX_QUEUED_ROWS;
|
|
2733
|
-
const summary = isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
3790
|
+
const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
|
|
3791
|
+
const sig = text === void 0 ? `queued|${w}|empty` : `queued|${w}|${isLast ? "ovf" : "row"}|${summary}`;
|
|
3792
|
+
this.paintRow(row, sig, () => {
|
|
3793
|
+
if (text === void 0) {
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
const display = ` \u23F3 ${summary}`;
|
|
3797
|
+
const padded = display + " ".repeat(Math.max(0, w - display.length));
|
|
3798
|
+
this.term.bgBlue.brightWhite.noFormat(padded);
|
|
3799
|
+
});
|
|
2737
3800
|
}
|
|
2738
3801
|
}
|
|
2739
3802
|
drawPrompt() {
|
|
@@ -2754,19 +3817,30 @@ var init_screen = __esm({
|
|
|
2754
3817
|
for (let i = 0; i < layout.rendered; i++) {
|
|
2755
3818
|
const vr = visualRows[layout.windowStart + i];
|
|
2756
3819
|
const row = top + i;
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
this.term(" ");
|
|
3820
|
+
let gutter = "wrap";
|
|
3821
|
+
let slice = "";
|
|
3822
|
+
if (vr) {
|
|
3823
|
+
if (vr.bufferIdx === 0 && vr.startCol === 0) {
|
|
3824
|
+
gutter = "first";
|
|
3825
|
+
} else if (vr.startCol === 0) {
|
|
3826
|
+
gutter = "newline";
|
|
3827
|
+
}
|
|
3828
|
+
slice = (state.buffer[vr.bufferIdx] ?? "").slice(vr.startCol, vr.endCol);
|
|
2767
3829
|
}
|
|
2768
|
-
const
|
|
2769
|
-
this.
|
|
3830
|
+
const sig = vr ? `prompt|${this.term.width}|${gutter}|${slice}` : `prompt|${this.term.width}|empty`;
|
|
3831
|
+
this.paintRow(row, sig, () => {
|
|
3832
|
+
if (!vr) {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
if (gutter === "first") {
|
|
3836
|
+
this.term.brightWhite("> ");
|
|
3837
|
+
} else if (gutter === "newline") {
|
|
3838
|
+
this.term.dim("\xB7 ");
|
|
3839
|
+
} else {
|
|
3840
|
+
this.term(" ");
|
|
3841
|
+
}
|
|
3842
|
+
this.term.noFormat(slice);
|
|
3843
|
+
});
|
|
2770
3844
|
}
|
|
2771
3845
|
}
|
|
2772
3846
|
drawConfirmPrompt() {
|
|
@@ -2776,10 +3850,12 @@ var init_screen = __esm({
|
|
|
2776
3850
|
}
|
|
2777
3851
|
const w = this.term.width;
|
|
2778
3852
|
const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
|
|
2779
|
-
this.
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
this.
|
|
3853
|
+
this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
|
|
3854
|
+
this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
|
|
3855
|
+
});
|
|
3856
|
+
this.paintRow(top + 1, `confirm|h|${w}|${spec.hint}`, () => {
|
|
3857
|
+
this.term.dim(` ${truncate(spec.hint, w - 2)}`);
|
|
3858
|
+
});
|
|
2783
3859
|
}
|
|
2784
3860
|
drawPermissionPrompt() {
|
|
2785
3861
|
const spec = this.permissionPrompt;
|
|
@@ -2790,21 +3866,20 @@ var init_screen = __esm({
|
|
|
2790
3866
|
const rows = this.permissionRows();
|
|
2791
3867
|
const top = this.term.height - rows - BANNER_ROWS + 1;
|
|
2792
3868
|
let row = top;
|
|
2793
|
-
const writeRow = (paint) => {
|
|
3869
|
+
const writeRow = (sig, paint) => {
|
|
2794
3870
|
if (row >= top + rows) {
|
|
2795
3871
|
return;
|
|
2796
3872
|
}
|
|
2797
|
-
this.
|
|
2798
|
-
paint();
|
|
3873
|
+
this.paintRow(row, sig, paint);
|
|
2799
3874
|
row += 1;
|
|
2800
3875
|
};
|
|
2801
|
-
writeRow(() => {
|
|
3876
|
+
writeRow(`perm|t|${w}|${spec.title}`, () => {
|
|
2802
3877
|
this.term.brightYellow(` \u{1F512} ${truncate(spec.title, w - 5)}`);
|
|
2803
3878
|
});
|
|
2804
|
-
writeRow(() => {
|
|
3879
|
+
writeRow(`perm|sub|${w}`, () => {
|
|
2805
3880
|
this.term.dim(" This action requires approval");
|
|
2806
3881
|
});
|
|
2807
|
-
writeRow(() => {
|
|
3882
|
+
writeRow(`perm|q|${w}`, () => {
|
|
2808
3883
|
this.term(" Do you want to proceed?");
|
|
2809
3884
|
});
|
|
2810
3885
|
for (let i = 0; i < spec.options.length; i++) {
|
|
@@ -2818,7 +3893,7 @@ var init_screen = __esm({
|
|
|
2818
3893
|
const isSel = i === spec.selectedIndex;
|
|
2819
3894
|
const marker = isSel ? "\u276F" : " ";
|
|
2820
3895
|
const body = ` ${marker} ${i + 1}. ${truncate(opt.label, w - 8)}`;
|
|
2821
|
-
writeRow(() => {
|
|
3896
|
+
writeRow(`perm|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.label}`, () => {
|
|
2822
3897
|
if (isSel) {
|
|
2823
3898
|
this.term.brightCyan(body);
|
|
2824
3899
|
} else {
|
|
@@ -2826,36 +3901,42 @@ var init_screen = __esm({
|
|
|
2826
3901
|
}
|
|
2827
3902
|
});
|
|
2828
3903
|
}
|
|
2829
|
-
writeRow(() => {
|
|
3904
|
+
writeRow(`perm|hint|${w}`, () => {
|
|
2830
3905
|
this.term.dim(" \u2191/\u2193 choose \xB7 Enter submit \xB7 Esc cancel \xB7 1\u20139 quick-pick");
|
|
2831
3906
|
});
|
|
2832
3907
|
}
|
|
2833
3908
|
drawBanner() {
|
|
2834
3909
|
const row = this.term.height;
|
|
2835
|
-
this.term.
|
|
2836
|
-
const
|
|
2837
|
-
const
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3910
|
+
const w = this.term.width;
|
|
3911
|
+
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
3912
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.planMode ? "1" : "0"}|${this.banner.hint}`;
|
|
3913
|
+
this.paintRow(row, sig, () => {
|
|
3914
|
+
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
3915
|
+
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
3916
|
+
if (this.banner.status === "busy") {
|
|
3917
|
+
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
3918
|
+
if (elapsedStr) {
|
|
3919
|
+
this.term(" ").dim(elapsedStr);
|
|
3920
|
+
}
|
|
3921
|
+
} else if (this.banner.status === "disconnected") {
|
|
3922
|
+
this.term.brightRed(`${dot} ${this.banner.status}`);
|
|
3923
|
+
} else {
|
|
3924
|
+
this.term.brightGreen(`${dot} ${this.banner.status}`);
|
|
2842
3925
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
this.
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
}
|
|
2858
|
-
this.term(" \xB7 ").dim(this.banner.hint);
|
|
3926
|
+
if (this.banner.queued > 0) {
|
|
3927
|
+
this.term(" \xB7 ").brightYellow(`${this.banner.queued} queued`);
|
|
3928
|
+
}
|
|
3929
|
+
if (this.scrollOffset > 0) {
|
|
3930
|
+
this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
|
|
3931
|
+
}
|
|
3932
|
+
this.term(" \xB7 ");
|
|
3933
|
+
if (this.banner.planMode) {
|
|
3934
|
+
this.term.brightYellow(planLabel);
|
|
3935
|
+
} else {
|
|
3936
|
+
this.term.dim(planLabel);
|
|
3937
|
+
}
|
|
3938
|
+
this.term(" \xB7 ").dim(this.banner.hint);
|
|
3939
|
+
});
|
|
2859
3940
|
}
|
|
2860
3941
|
placeCursor() {
|
|
2861
3942
|
if (this.permissionPrompt) {
|
|
@@ -2905,37 +3986,83 @@ var init_screen = __esm({
|
|
|
2905
3986
|
4 + this.permissionPrompt.options.length
|
|
2906
3987
|
);
|
|
2907
3988
|
}
|
|
2908
|
-
|
|
3989
|
+
// Walk this.lines from the tail, accumulating wrapped rows via the
|
|
3990
|
+
// wrap cache, until we have at least `needed` rows or run out. Returns
|
|
3991
|
+
// the collected rows in original (top-down) order plus an `exhausted`
|
|
3992
|
+
// flag that's true iff we reached the head of this.lines. The hot path
|
|
3993
|
+
// (drawScrollback) only ever asks for `visibleRows + scrollOffset`
|
|
3994
|
+
// rows, so a 10k-line scrollback costs ~50 cache hits per repaint
|
|
3995
|
+
// instead of 10k. With `needed = Infinity` this walks everything and
|
|
3996
|
+
// doubles as a total-row counter for maxScrollOffset.
|
|
3997
|
+
wrapTail(width, needed) {
|
|
2909
3998
|
if (width <= 4) {
|
|
2910
|
-
|
|
3999
|
+
const take = Math.min(needed, this.lines.length);
|
|
4000
|
+
return {
|
|
4001
|
+
rows: this.lines.slice(this.lines.length - take),
|
|
4002
|
+
exhausted: needed >= this.lines.length
|
|
4003
|
+
};
|
|
2911
4004
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
4005
|
+
if (this.wrapCacheWidth !== width) {
|
|
4006
|
+
this.wrapCache.clear();
|
|
4007
|
+
this.wrapCacheWidth = width;
|
|
4008
|
+
}
|
|
4009
|
+
if (needed <= 0 || this.lines.length === 0) {
|
|
4010
|
+
return { rows: [], exhausted: true };
|
|
4011
|
+
}
|
|
4012
|
+
const batches = [];
|
|
4013
|
+
let total = 0;
|
|
4014
|
+
let stoppedAt = 0;
|
|
4015
|
+
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
4016
|
+
const wrapped = this.wrapOne(this.lines[i], width);
|
|
4017
|
+
batches.push(wrapped);
|
|
4018
|
+
total += wrapped.length;
|
|
4019
|
+
stoppedAt = i;
|
|
4020
|
+
if (total >= needed) {
|
|
4021
|
+
break;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
const rows = [];
|
|
4025
|
+
for (let i = batches.length - 1; i >= 0; i--) {
|
|
4026
|
+
rows.push(...batches[i]);
|
|
4027
|
+
}
|
|
4028
|
+
return { rows, exhausted: stoppedAt === 0 };
|
|
4029
|
+
}
|
|
4030
|
+
wrapOne(line, width) {
|
|
4031
|
+
const id = this.lineIds.get(line);
|
|
4032
|
+
if (id !== void 0) {
|
|
4033
|
+
const cached = this.wrapCache.get(id);
|
|
4034
|
+
if (cached) {
|
|
4035
|
+
return cached;
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
const prefix = line.prefix ?? "";
|
|
4039
|
+
const room = Math.max(1, width - prefix.length);
|
|
4040
|
+
const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room);
|
|
4041
|
+
const wrapped = [];
|
|
4042
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
4043
|
+
const chunk = chunks[i] ?? "";
|
|
4044
|
+
const wrappedLine = {
|
|
4045
|
+
prefix: i === 0 ? line.prefix : " ".repeat(prefix.length),
|
|
4046
|
+
body: chunk
|
|
4047
|
+
};
|
|
4048
|
+
if (line.prefixStyle !== void 0) {
|
|
4049
|
+
wrappedLine.prefixStyle = line.prefixStyle;
|
|
4050
|
+
}
|
|
4051
|
+
if (line.bodyStyle !== void 0) {
|
|
4052
|
+
wrappedLine.bodyStyle = line.bodyStyle;
|
|
4053
|
+
}
|
|
4054
|
+
if (line.fillRow) {
|
|
4055
|
+
wrappedLine.fillRow = true;
|
|
2936
4056
|
}
|
|
4057
|
+
if (line.ansi) {
|
|
4058
|
+
wrappedLine.ansi = true;
|
|
4059
|
+
}
|
|
4060
|
+
wrapped.push(wrappedLine);
|
|
4061
|
+
}
|
|
4062
|
+
if (id !== void 0) {
|
|
4063
|
+
this.wrapCache.set(id, wrapped);
|
|
2937
4064
|
}
|
|
2938
|
-
return
|
|
4065
|
+
return wrapped;
|
|
2939
4066
|
}
|
|
2940
4067
|
writeFormattedLine(line, width) {
|
|
2941
4068
|
if (line.prefix) {
|
|
@@ -2974,6 +4101,10 @@ var init_input = __esm({
|
|
|
2974
4101
|
savedDraft = null;
|
|
2975
4102
|
history = [];
|
|
2976
4103
|
turnRunning = false;
|
|
4104
|
+
// Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
|
|
4105
|
+
// here so ^Y can yank it back. Standard readline keeps a stack; we
|
|
4106
|
+
// only keep one slot because that's what 99% of yank uses look like.
|
|
4107
|
+
killBuffer = "";
|
|
2977
4108
|
constructor(opts = {}) {
|
|
2978
4109
|
this.history = [...opts.history ?? []];
|
|
2979
4110
|
this.planMode = opts.planMode ?? false;
|
|
@@ -3082,6 +4213,9 @@ var init_input = __esm({
|
|
|
3082
4213
|
case "ctrl-w":
|
|
3083
4214
|
this.killWord();
|
|
3084
4215
|
return [];
|
|
4216
|
+
case "ctrl-y":
|
|
4217
|
+
this.yank();
|
|
4218
|
+
return [];
|
|
3085
4219
|
case "escape":
|
|
3086
4220
|
return [];
|
|
3087
4221
|
}
|
|
@@ -3175,11 +4309,19 @@ var init_input = __esm({
|
|
|
3175
4309
|
}
|
|
3176
4310
|
killLine() {
|
|
3177
4311
|
const line = this.currentLine();
|
|
4312
|
+
const killed = line.slice(0, this.col);
|
|
4313
|
+
if (killed.length > 0) {
|
|
4314
|
+
this.killBuffer = killed;
|
|
4315
|
+
}
|
|
3178
4316
|
this.setCurrentLine(line.slice(this.col));
|
|
3179
4317
|
this.col = 0;
|
|
3180
4318
|
}
|
|
3181
4319
|
killToEnd() {
|
|
3182
4320
|
const line = this.currentLine();
|
|
4321
|
+
const killed = line.slice(this.col);
|
|
4322
|
+
if (killed.length > 0) {
|
|
4323
|
+
this.killBuffer = killed;
|
|
4324
|
+
}
|
|
3183
4325
|
this.setCurrentLine(line.slice(0, this.col));
|
|
3184
4326
|
}
|
|
3185
4327
|
killWord() {
|
|
@@ -3195,9 +4337,19 @@ var init_input = __esm({
|
|
|
3195
4337
|
while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
|
|
3196
4338
|
i -= 1;
|
|
3197
4339
|
}
|
|
4340
|
+
const killed = line.slice(i, this.col);
|
|
4341
|
+
if (killed.length > 0) {
|
|
4342
|
+
this.killBuffer = killed;
|
|
4343
|
+
}
|
|
3198
4344
|
this.setCurrentLine(line.slice(0, i) + line.slice(this.col));
|
|
3199
4345
|
this.col = i;
|
|
3200
4346
|
}
|
|
4347
|
+
yank() {
|
|
4348
|
+
if (this.killBuffer.length === 0) {
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
this.insertText(this.killBuffer);
|
|
4352
|
+
}
|
|
3201
4353
|
moveLeft() {
|
|
3202
4354
|
if (this.col > 0) {
|
|
3203
4355
|
this.col -= 1;
|
|
@@ -3340,10 +4492,19 @@ function mapUpdate(update) {
|
|
|
3340
4492
|
return mapUsage(u);
|
|
3341
4493
|
case "available_commands_update":
|
|
3342
4494
|
return mapAvailableCommands(u);
|
|
4495
|
+
case "session_info_update":
|
|
4496
|
+
return mapSessionInfo(u);
|
|
3343
4497
|
default:
|
|
3344
4498
|
return { kind: "unknown", sessionUpdate: tag, raw: update };
|
|
3345
4499
|
}
|
|
3346
4500
|
}
|
|
4501
|
+
function mapSessionInfo(u) {
|
|
4502
|
+
const title = readString(u, "title");
|
|
4503
|
+
if (title === void 0) {
|
|
4504
|
+
return null;
|
|
4505
|
+
}
|
|
4506
|
+
return { kind: "session-info", title };
|
|
4507
|
+
}
|
|
3347
4508
|
function mapAvailableCommands(u) {
|
|
3348
4509
|
const list = u.availableCommands ?? u.commands;
|
|
3349
4510
|
if (!Array.isArray(list)) {
|
|
@@ -3584,6 +4745,8 @@ function formatEvent(event) {
|
|
|
3584
4745
|
return [];
|
|
3585
4746
|
case "available-commands":
|
|
3586
4747
|
return [];
|
|
4748
|
+
case "session-info":
|
|
4749
|
+
return [];
|
|
3587
4750
|
case "unknown":
|
|
3588
4751
|
return [];
|
|
3589
4752
|
}
|
|
@@ -3849,8 +5012,8 @@ var init_format = __esm({
|
|
|
3849
5012
|
});
|
|
3850
5013
|
|
|
3851
5014
|
// src/tui/app.ts
|
|
3852
|
-
import
|
|
3853
|
-
import {
|
|
5015
|
+
import { appendFileSync, statSync, renameSync } from "fs";
|
|
5016
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
3854
5017
|
import termkit from "terminal-kit";
|
|
3855
5018
|
async function runTuiApp(opts) {
|
|
3856
5019
|
const config = await ensureConfig();
|
|
@@ -3867,9 +5030,33 @@ async function runSession(term, config, opts) {
|
|
|
3867
5030
|
term.grabInput(false);
|
|
3868
5031
|
process.exit(0);
|
|
3869
5032
|
}
|
|
3870
|
-
const
|
|
3871
|
-
|
|
5033
|
+
const launchLabel = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
5034
|
+
term.cyan(launchLabel)("\n");
|
|
5035
|
+
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
5036
|
+
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
5037
|
+
const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
|
|
5038
|
+
let onReconnect = null;
|
|
5039
|
+
let onDisconnectHook = null;
|
|
5040
|
+
const stream = new ResilientWsStream({
|
|
5041
|
+
url: wsUrl,
|
|
5042
|
+
subprotocols,
|
|
5043
|
+
onConnect: async (firstConnect) => {
|
|
5044
|
+
if (firstConnect) {
|
|
5045
|
+
return;
|
|
5046
|
+
}
|
|
5047
|
+
if (onReconnect) {
|
|
5048
|
+
await onReconnect();
|
|
5049
|
+
}
|
|
5050
|
+
},
|
|
5051
|
+
onDisconnect: (err) => {
|
|
5052
|
+
if (onDisconnectHook) {
|
|
5053
|
+
onDisconnectHook(err);
|
|
5054
|
+
}
|
|
5055
|
+
},
|
|
5056
|
+
log: () => void 0
|
|
5057
|
+
});
|
|
3872
5058
|
const conn = new JsonRpcConnection(stream);
|
|
5059
|
+
await stream.start();
|
|
3873
5060
|
let bufferedEvents = [];
|
|
3874
5061
|
let applyRenderEvent = null;
|
|
3875
5062
|
const appendRender = (event) => {
|
|
@@ -3891,8 +5078,9 @@ async function runSession(term, config, opts) {
|
|
|
3891
5078
|
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
3892
5079
|
if (before === 0 && pendingTurns > 0) {
|
|
3893
5080
|
sessionBusySince = Date.now();
|
|
5081
|
+
dispatcherRef?.setTurnRunning(true);
|
|
3894
5082
|
if (screenReady) {
|
|
3895
|
-
screenRef.setBanner({ status: "
|
|
5083
|
+
screenRef.setBanner({ status: "busy", elapsedMs: 0 });
|
|
3896
5084
|
}
|
|
3897
5085
|
if (sessionElapsedTimer === null && screenReady) {
|
|
3898
5086
|
sessionElapsedTimer = setInterval(() => {
|
|
@@ -3901,10 +5089,11 @@ async function runSession(term, config, opts) {
|
|
|
3901
5089
|
}
|
|
3902
5090
|
screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
|
|
3903
5091
|
renderToolsBlock();
|
|
3904
|
-
},
|
|
5092
|
+
}, 1e3);
|
|
3905
5093
|
}
|
|
3906
5094
|
} else if (before > 0 && pendingTurns === 0) {
|
|
3907
5095
|
sessionBusySince = null;
|
|
5096
|
+
dispatcherRef?.setTurnRunning(false);
|
|
3908
5097
|
if (sessionElapsedTimer !== null) {
|
|
3909
5098
|
clearInterval(sessionElapsedTimer);
|
|
3910
5099
|
sessionElapsedTimer = null;
|
|
@@ -3918,9 +5107,11 @@ async function runSession(term, config, opts) {
|
|
|
3918
5107
|
}
|
|
3919
5108
|
};
|
|
3920
5109
|
let screenRef = null;
|
|
5110
|
+
let dispatcherRef = null;
|
|
3921
5111
|
conn.onNotification("session/update", (params) => {
|
|
3922
5112
|
const { update } = params ?? {};
|
|
3923
5113
|
const event = mapUpdate(update);
|
|
5114
|
+
debugLogUpdate(update, event);
|
|
3924
5115
|
if (event?.kind === "user-text") {
|
|
3925
5116
|
adjustPendingTurns(1);
|
|
3926
5117
|
} else if (event?.kind === "turn-complete") {
|
|
@@ -4033,6 +5224,11 @@ async function runSession(term, config, opts) {
|
|
|
4033
5224
|
let resolvedSessionId = ctx.sessionId;
|
|
4034
5225
|
let resolvedAgentId = ctx.agentId;
|
|
4035
5226
|
let resolvedCwd = ctx.cwd;
|
|
5227
|
+
let resolvedTitle;
|
|
5228
|
+
let initialModel;
|
|
5229
|
+
let initialMode;
|
|
5230
|
+
let initialCommands;
|
|
5231
|
+
let initialTurnStartedAt;
|
|
4036
5232
|
if (ctx.sessionId === "__new__") {
|
|
4037
5233
|
const created = await conn.request("session/new", {
|
|
4038
5234
|
cwd: ctx.cwd,
|
|
@@ -4048,6 +5244,17 @@ async function runSession(term, config, opts) {
|
|
|
4048
5244
|
if (hydraMeta.cwd) {
|
|
4049
5245
|
resolvedCwd = hydraMeta.cwd;
|
|
4050
5246
|
}
|
|
5247
|
+
if (hydraMeta.name) {
|
|
5248
|
+
resolvedTitle = hydraMeta.name;
|
|
5249
|
+
}
|
|
5250
|
+
initialModel = hydraMeta.currentModel;
|
|
5251
|
+
initialMode = hydraMeta.currentMode;
|
|
5252
|
+
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
5253
|
+
if (hydraMeta.availableCommands) {
|
|
5254
|
+
initialCommands = hydraMeta.availableCommands.map(
|
|
5255
|
+
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
5256
|
+
);
|
|
5257
|
+
}
|
|
4051
5258
|
} else {
|
|
4052
5259
|
const attached = await conn.request("session/attach", {
|
|
4053
5260
|
sessionId: ctx.sessionId,
|
|
@@ -4063,16 +5270,31 @@ async function runSession(term, config, opts) {
|
|
|
4063
5270
|
if (hydraMeta.cwd) {
|
|
4064
5271
|
resolvedCwd = hydraMeta.cwd;
|
|
4065
5272
|
}
|
|
5273
|
+
if (hydraMeta.name) {
|
|
5274
|
+
resolvedTitle = hydraMeta.name;
|
|
5275
|
+
}
|
|
5276
|
+
initialModel = hydraMeta.currentModel;
|
|
5277
|
+
initialMode = hydraMeta.currentMode;
|
|
5278
|
+
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
5279
|
+
if (hydraMeta.availableCommands) {
|
|
5280
|
+
initialCommands = hydraMeta.availableCommands.map(
|
|
5281
|
+
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
5282
|
+
);
|
|
5283
|
+
}
|
|
4066
5284
|
}
|
|
4067
|
-
|
|
4068
|
-
const historyFile = paths.tuiHistoryFile();
|
|
5285
|
+
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
4069
5286
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
4070
5287
|
const dispatcher = new InputDispatcher({ history });
|
|
5288
|
+
dispatcherRef = dispatcher;
|
|
5289
|
+
if (pendingTurns > 0) {
|
|
5290
|
+
dispatcher.setTurnRunning(true);
|
|
5291
|
+
}
|
|
4071
5292
|
let turnInFlight = null;
|
|
4072
5293
|
const screen = new Screen({
|
|
4073
5294
|
term,
|
|
4074
5295
|
dispatcher,
|
|
4075
5296
|
repaintThrottleMs: config.tui.repaintThrottleMs,
|
|
5297
|
+
maxScrollbackLines: config.tui.maxScrollbackLines,
|
|
4076
5298
|
onKey: (events) => {
|
|
4077
5299
|
for (const ev of events) {
|
|
4078
5300
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
@@ -4102,7 +5324,7 @@ async function runSession(term, config, opts) {
|
|
|
4102
5324
|
{ name: "/demo-plan", description: "Inject synthetic plan events (UI test)" },
|
|
4103
5325
|
{ name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
|
|
4104
5326
|
];
|
|
4105
|
-
let agentCommands = [];
|
|
5327
|
+
let agentCommands = initialCommands ?? [];
|
|
4106
5328
|
const allCommands = () => {
|
|
4107
5329
|
const seen = /* @__PURE__ */ new Set();
|
|
4108
5330
|
const out = [];
|
|
@@ -4226,17 +5448,31 @@ async function runSession(term, config, opts) {
|
|
|
4226
5448
|
screen.setHeader({
|
|
4227
5449
|
agent: headerName,
|
|
4228
5450
|
cwd: resolvedCwd,
|
|
4229
|
-
sessionId: resolvedSessionId
|
|
5451
|
+
sessionId: resolvedSessionId,
|
|
5452
|
+
title: resolvedTitle
|
|
4230
5453
|
});
|
|
5454
|
+
if (initialMode) {
|
|
5455
|
+
screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
|
|
5456
|
+
}
|
|
5457
|
+
if (initialModel) {
|
|
5458
|
+
screen.appendLines(formatEvent({ kind: "model-changed", model: initialModel }));
|
|
5459
|
+
}
|
|
4231
5460
|
let finishSession = null;
|
|
4232
5461
|
const sessionDone = new Promise((resolve2) => {
|
|
4233
5462
|
finishSession = resolve2;
|
|
4234
5463
|
});
|
|
5464
|
+
const cancelRemoteTurn = () => {
|
|
5465
|
+
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
5466
|
+
};
|
|
4235
5467
|
const sigintHandler = () => {
|
|
4236
5468
|
if (turnInFlight) {
|
|
4237
5469
|
turnInFlight.cancel();
|
|
4238
5470
|
return;
|
|
4239
5471
|
}
|
|
5472
|
+
if (pendingTurns > 0) {
|
|
5473
|
+
cancelRemoteTurn();
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
4240
5476
|
void requestExit();
|
|
4241
5477
|
};
|
|
4242
5478
|
let exitConfirmation = null;
|
|
@@ -4311,12 +5547,10 @@ async function runSession(term, config, opts) {
|
|
|
4311
5547
|
};
|
|
4312
5548
|
const teardown = () => {
|
|
4313
5549
|
process.off("SIGINT", sigintHandler);
|
|
5550
|
+
screen.clearWindowTitle();
|
|
4314
5551
|
screen.stop();
|
|
4315
5552
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
4316
|
-
|
|
4317
|
-
ws.close();
|
|
4318
|
-
} catch {
|
|
4319
|
-
}
|
|
5553
|
+
void stream.close().catch(() => void 0);
|
|
4320
5554
|
};
|
|
4321
5555
|
const stop = (code = 0) => {
|
|
4322
5556
|
teardown();
|
|
@@ -4329,22 +5563,32 @@ async function runSession(term, config, opts) {
|
|
|
4329
5563
|
}
|
|
4330
5564
|
};
|
|
4331
5565
|
const switchSession = async () => {
|
|
4332
|
-
|
|
4333
|
-
if (!resume) {
|
|
5566
|
+
if (!finishSession) {
|
|
4334
5567
|
return;
|
|
4335
5568
|
}
|
|
4336
|
-
|
|
4337
|
-
|
|
5569
|
+
const pendingDraft = dispatcher.state().buffer.join("\n");
|
|
5570
|
+
if (pendingDraft.replace(/\s+$/, "").length > 0) {
|
|
5571
|
+
history = appendEntry(history, pendingDraft);
|
|
5572
|
+
dispatcher.setHistory(history);
|
|
5573
|
+
}
|
|
5574
|
+
screen.pauseRepaint();
|
|
5575
|
+
screen.stop();
|
|
5576
|
+
saveHistory(historyFile, history).catch(() => void 0);
|
|
4338
5577
|
const sessions = await listSessions(config);
|
|
4339
5578
|
const choice = await pickSession(term, {
|
|
4340
5579
|
cwd: resolvedCwd,
|
|
4341
5580
|
sessions,
|
|
4342
|
-
|
|
5581
|
+
config
|
|
4343
5582
|
});
|
|
4344
5583
|
if (choice.kind === "abort") {
|
|
4345
|
-
|
|
5584
|
+
screen.start();
|
|
5585
|
+
screen.resumeRepaint();
|
|
4346
5586
|
return;
|
|
4347
5587
|
}
|
|
5588
|
+
const resume = finishSession;
|
|
5589
|
+
finishSession = null;
|
|
5590
|
+
process.off("SIGINT", sigintHandler);
|
|
5591
|
+
void stream.close().catch(() => void 0);
|
|
4348
5592
|
if (choice.kind === "new") {
|
|
4349
5593
|
const { sessionId: _drop, ...rest } = opts;
|
|
4350
5594
|
void _drop;
|
|
@@ -4369,6 +5613,8 @@ async function runSession(term, config, opts) {
|
|
|
4369
5613
|
case "cancel":
|
|
4370
5614
|
if (turnInFlight) {
|
|
4371
5615
|
turnInFlight.cancel();
|
|
5616
|
+
} else if (pendingTurns > 0) {
|
|
5617
|
+
cancelRemoteTurn();
|
|
4372
5618
|
}
|
|
4373
5619
|
if (promptQueue.length > (workerActive ? 1 : 0)) {
|
|
4374
5620
|
promptQueue.length = workerActive ? 1 : 0;
|
|
@@ -4556,7 +5802,6 @@ async function runSession(term, config, opts) {
|
|
|
4556
5802
|
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
4557
5803
|
adjustPendingTurns(1);
|
|
4558
5804
|
appendRender({ kind: "user-text", text });
|
|
4559
|
-
dispatcher.setTurnRunning(true);
|
|
4560
5805
|
let cancelled = false;
|
|
4561
5806
|
turnInFlight = {
|
|
4562
5807
|
cancel: () => {
|
|
@@ -4586,7 +5831,6 @@ async function runSession(term, config, opts) {
|
|
|
4586
5831
|
});
|
|
4587
5832
|
} finally {
|
|
4588
5833
|
turnInFlight = null;
|
|
4589
|
-
dispatcher.setTurnRunning(false);
|
|
4590
5834
|
adjustPendingTurns(-1);
|
|
4591
5835
|
appendRender(
|
|
4592
5836
|
stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
|
|
@@ -4642,7 +5886,7 @@ async function runSession(term, config, opts) {
|
|
|
4642
5886
|
const elapsed = end - toolsBlockStartedAt;
|
|
4643
5887
|
let summary;
|
|
4644
5888
|
if (total === 0) {
|
|
4645
|
-
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `
|
|
5889
|
+
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
|
|
4646
5890
|
} else {
|
|
4647
5891
|
const noun = total === 1 ? "tool" : "tools";
|
|
4648
5892
|
const timing = inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
@@ -4709,6 +5953,12 @@ async function runSession(term, config, opts) {
|
|
|
4709
5953
|
refreshCompletions();
|
|
4710
5954
|
return;
|
|
4711
5955
|
}
|
|
5956
|
+
if (event.kind === "session-info") {
|
|
5957
|
+
if (event.title !== void 0) {
|
|
5958
|
+
screen.setHeader({ title: event.title });
|
|
5959
|
+
}
|
|
5960
|
+
return;
|
|
5961
|
+
}
|
|
4712
5962
|
if (event.kind === "usage-update") {
|
|
4713
5963
|
let changed = false;
|
|
4714
5964
|
if (event.used !== void 0 && usage.used !== event.used) {
|
|
@@ -4739,6 +5989,12 @@ async function runSession(term, config, opts) {
|
|
|
4739
5989
|
if (formatted2.length > 0) {
|
|
4740
5990
|
screen.appendLines(formatted2);
|
|
4741
5991
|
}
|
|
5992
|
+
screen.clearKey("tools");
|
|
5993
|
+
screen.clearKey("plan");
|
|
5994
|
+
toolStates.clear();
|
|
5995
|
+
toolCallOrder.length = 0;
|
|
5996
|
+
toolsExpanded = false;
|
|
5997
|
+
toolsBlockEndedAt = null;
|
|
4742
5998
|
startToolsBlock();
|
|
4743
5999
|
screen.redraw();
|
|
4744
6000
|
return;
|
|
@@ -4779,12 +6035,10 @@ async function runSession(term, config, opts) {
|
|
|
4779
6035
|
if (event.kind === "turn-complete") {
|
|
4780
6036
|
closeAgentText();
|
|
4781
6037
|
screen.clearKey("plan");
|
|
4782
|
-
if (
|
|
6038
|
+
if (toolsBlockStartedAt !== null) {
|
|
4783
6039
|
toolsBlockEndedAt = Date.now();
|
|
4784
6040
|
renderToolsBlock();
|
|
4785
6041
|
screen.clearKey("tools");
|
|
4786
|
-
} else if (toolsBlockStartedAt !== null) {
|
|
4787
|
-
screen.removeBlock("tools");
|
|
4788
6042
|
}
|
|
4789
6043
|
toolStates.clear();
|
|
4790
6044
|
toolCallOrder.length = 0;
|
|
@@ -4804,6 +6058,108 @@ async function runSession(term, config, opts) {
|
|
|
4804
6058
|
} finally {
|
|
4805
6059
|
screen.resumeRepaint();
|
|
4806
6060
|
}
|
|
6061
|
+
if (initialTurnStartedAt !== void 0 && pendingTurns > 0) {
|
|
6062
|
+
sessionBusySince = initialTurnStartedAt;
|
|
6063
|
+
screen.setBanner({
|
|
6064
|
+
status: "busy",
|
|
6065
|
+
elapsedMs: Date.now() - initialTurnStartedAt
|
|
6066
|
+
});
|
|
6067
|
+
if (sessionElapsedTimer === null) {
|
|
6068
|
+
sessionElapsedTimer = setInterval(() => {
|
|
6069
|
+
if (sessionBusySince === null || screenRef === null) {
|
|
6070
|
+
return;
|
|
6071
|
+
}
|
|
6072
|
+
screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
|
|
6073
|
+
renderToolsBlock();
|
|
6074
|
+
}, 1e3);
|
|
6075
|
+
}
|
|
6076
|
+
startToolsBlock();
|
|
6077
|
+
}
|
|
6078
|
+
const resetInFlightUiState = () => {
|
|
6079
|
+
if (pendingPermission) {
|
|
6080
|
+
const resolve2 = pendingPermission.resolve;
|
|
6081
|
+
pendingPermission = null;
|
|
6082
|
+
screen.setPermissionPrompt(null);
|
|
6083
|
+
resolve2({ outcome: { outcome: "cancelled" } });
|
|
6084
|
+
}
|
|
6085
|
+
closeAgentText();
|
|
6086
|
+
if (toolsBlockStartedAt !== null) {
|
|
6087
|
+
toolsBlockEndedAt = Date.now();
|
|
6088
|
+
renderToolsBlock();
|
|
6089
|
+
screen.clearKey("tools");
|
|
6090
|
+
toolStates.clear();
|
|
6091
|
+
toolCallOrder.length = 0;
|
|
6092
|
+
toolsBlockStartedAt = null;
|
|
6093
|
+
toolsBlockEndedAt = null;
|
|
6094
|
+
toolsExpanded = false;
|
|
6095
|
+
}
|
|
6096
|
+
screen.clearKey("plan");
|
|
6097
|
+
if (pendingTurns > 0) {
|
|
6098
|
+
adjustPendingTurns(-pendingTurns);
|
|
6099
|
+
}
|
|
6100
|
+
};
|
|
6101
|
+
onDisconnectHook = () => {
|
|
6102
|
+
screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
|
|
6103
|
+
};
|
|
6104
|
+
onReconnect = async () => {
|
|
6105
|
+
resetInFlightUiState();
|
|
6106
|
+
const initReq = {
|
|
6107
|
+
jsonrpc: "2.0",
|
|
6108
|
+
id: `tui-reinit-${nanoid3()}`,
|
|
6109
|
+
method: "initialize",
|
|
6110
|
+
params: {
|
|
6111
|
+
protocolVersion: 1,
|
|
6112
|
+
clientCapabilities: {
|
|
6113
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
6114
|
+
terminal: false
|
|
6115
|
+
},
|
|
6116
|
+
clientInfo: { name: "hydra-acp-tui", version: "0.1.0" }
|
|
6117
|
+
}
|
|
6118
|
+
};
|
|
6119
|
+
try {
|
|
6120
|
+
await stream.request(initReq);
|
|
6121
|
+
} catch {
|
|
6122
|
+
}
|
|
6123
|
+
const attachReq = {
|
|
6124
|
+
jsonrpc: "2.0",
|
|
6125
|
+
id: `tui-reattach-${nanoid3()}`,
|
|
6126
|
+
method: "session/attach",
|
|
6127
|
+
params: {
|
|
6128
|
+
sessionId: resolvedSessionId,
|
|
6129
|
+
historyPolicy: "none",
|
|
6130
|
+
clientInfo: { name: "hydra-acp-tui", version: "0.1.0" },
|
|
6131
|
+
...upstreamSessionId !== void 0 ? {
|
|
6132
|
+
_meta: {
|
|
6133
|
+
[HYDRA_META_KEY]: {
|
|
6134
|
+
resume: {
|
|
6135
|
+
upstreamSessionId,
|
|
6136
|
+
agentId: resolvedAgentId,
|
|
6137
|
+
cwd: resolvedCwd
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
} : {}
|
|
6142
|
+
}
|
|
6143
|
+
};
|
|
6144
|
+
try {
|
|
6145
|
+
const resp = await stream.request(attachReq);
|
|
6146
|
+
if (resp.error) {
|
|
6147
|
+
throw new Error(resp.error.message);
|
|
6148
|
+
}
|
|
6149
|
+
} catch (err) {
|
|
6150
|
+
screen.appendLines([
|
|
6151
|
+
{
|
|
6152
|
+
prefix: " ",
|
|
6153
|
+
body: `reattach failed: ${err.message}`,
|
|
6154
|
+
bodyStyle: "tool-status-fail"
|
|
6155
|
+
}
|
|
6156
|
+
]);
|
|
6157
|
+
}
|
|
6158
|
+
screen.setBanner({
|
|
6159
|
+
status: pendingTurns > 0 ? "busy" : "ready",
|
|
6160
|
+
elapsedMs: pendingTurns > 0 ? 0 : void 0
|
|
6161
|
+
});
|
|
6162
|
+
};
|
|
4807
6163
|
conn.onClose((err) => {
|
|
4808
6164
|
if (err) {
|
|
4809
6165
|
term.red(`
|
|
@@ -4848,7 +6204,7 @@ async function resolveSession(term, config, opts) {
|
|
|
4848
6204
|
const choice = await pickSession(term, {
|
|
4849
6205
|
cwd,
|
|
4850
6206
|
sessions,
|
|
4851
|
-
|
|
6207
|
+
config
|
|
4852
6208
|
});
|
|
4853
6209
|
if (choice.kind === "abort") {
|
|
4854
6210
|
return null;
|
|
@@ -4869,23 +6225,47 @@ function newCtx(opts, cwd, config) {
|
|
|
4869
6225
|
cwd
|
|
4870
6226
|
};
|
|
4871
6227
|
}
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
6228
|
+
function debugLogUpdate(update, event) {
|
|
6229
|
+
writeDebugLine({
|
|
6230
|
+
src: "session/update",
|
|
6231
|
+
update,
|
|
6232
|
+
event: event === null ? null : { kind: event.kind }
|
|
6233
|
+
});
|
|
6234
|
+
}
|
|
6235
|
+
function writeDebugLine(payload) {
|
|
6236
|
+
const override = process.env.HYDRA_TUI_DEBUG_LOG;
|
|
6237
|
+
const target = override === void 0 ? paths.tuiLogFile() : override;
|
|
6238
|
+
if (target.length === 0) {
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
try {
|
|
6242
|
+
rotateIfBig(target);
|
|
6243
|
+
const line = JSON.stringify({
|
|
6244
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6245
|
+
...payload
|
|
6246
|
+
});
|
|
6247
|
+
appendFileSync(target, `${line}
|
|
6248
|
+
`);
|
|
6249
|
+
} catch {
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
function rotateIfBig(target) {
|
|
6253
|
+
try {
|
|
6254
|
+
const stat3 = statSync(target);
|
|
6255
|
+
if (stat3.size < TUI_LOG_MAX_BYTES) {
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6258
|
+
renameSync(target, `${target}.0`);
|
|
6259
|
+
} catch {
|
|
6260
|
+
}
|
|
4881
6261
|
}
|
|
4882
|
-
var PLAN_PREFIX_TEXT;
|
|
6262
|
+
var PLAN_PREFIX_TEXT, TUI_LOG_MAX_BYTES;
|
|
4883
6263
|
var init_app = __esm({
|
|
4884
6264
|
"src/tui/app.ts"() {
|
|
4885
6265
|
"use strict";
|
|
4886
6266
|
init_connection();
|
|
4887
|
-
init_ws_stream();
|
|
4888
6267
|
init_types();
|
|
6268
|
+
init_resilient_ws();
|
|
4889
6269
|
init_config();
|
|
4890
6270
|
init_daemon_bootstrap();
|
|
4891
6271
|
init_paths();
|
|
@@ -4897,6 +6277,7 @@ var init_app = __esm({
|
|
|
4897
6277
|
init_render_update();
|
|
4898
6278
|
init_format();
|
|
4899
6279
|
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.";
|
|
6280
|
+
TUI_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
4900
6281
|
}
|
|
4901
6282
|
});
|
|
4902
6283
|
|
|
@@ -5013,7 +6394,7 @@ import { setTimeout as sleep2 } from "timers/promises";
|
|
|
5013
6394
|
|
|
5014
6395
|
// src/daemon/server.ts
|
|
5015
6396
|
init_config();
|
|
5016
|
-
import * as
|
|
6397
|
+
import * as fs8 from "fs";
|
|
5017
6398
|
import * as fsp2 from "fs/promises";
|
|
5018
6399
|
import Fastify from "fastify";
|
|
5019
6400
|
import websocketPlugin from "@fastify/websocket";
|
|
@@ -5185,6 +6566,9 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
5185
6566
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
5186
6567
|
}
|
|
5187
6568
|
|
|
6569
|
+
// src/core/session-manager.ts
|
|
6570
|
+
import * as fs6 from "fs/promises";
|
|
6571
|
+
|
|
5188
6572
|
// src/core/agent-instance.ts
|
|
5189
6573
|
import { spawn } from "child_process";
|
|
5190
6574
|
|
|
@@ -5330,6 +6714,10 @@ init_paths();
|
|
|
5330
6714
|
import * as fs4 from "fs/promises";
|
|
5331
6715
|
import * as path2 from "path";
|
|
5332
6716
|
import { z as z4 } from "zod";
|
|
6717
|
+
var PersistedAgentCommand = z4.object({
|
|
6718
|
+
name: z4.string(),
|
|
6719
|
+
description: z4.string().optional()
|
|
6720
|
+
});
|
|
5333
6721
|
var SessionRecord = z4.object({
|
|
5334
6722
|
version: z4.literal(1),
|
|
5335
6723
|
sessionId: z4.string(),
|
|
@@ -5338,6 +6726,13 @@ var SessionRecord = z4.object({
|
|
|
5338
6726
|
cwd: z4.string(),
|
|
5339
6727
|
title: z4.string().optional(),
|
|
5340
6728
|
agentArgs: z4.array(z4.string()).optional(),
|
|
6729
|
+
// Snapshot of "what is currently true about this session" carried in
|
|
6730
|
+
// meta.json so a late-attaching or cold-resurrected client can be
|
|
6731
|
+
// told via the attach response _meta without depending on history
|
|
6732
|
+
// replay of a snapshot-shaped notification.
|
|
6733
|
+
currentModel: z4.string().optional(),
|
|
6734
|
+
currentMode: z4.string().optional(),
|
|
6735
|
+
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
5341
6736
|
createdAt: z4.string(),
|
|
5342
6737
|
updatedAt: z4.string()
|
|
5343
6738
|
});
|
|
@@ -5350,7 +6745,7 @@ function assertSafeId(id) {
|
|
|
5350
6745
|
var SessionStore = class {
|
|
5351
6746
|
async write(record) {
|
|
5352
6747
|
assertSafeId(record.sessionId);
|
|
5353
|
-
await fs4.mkdir(paths.
|
|
6748
|
+
await fs4.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
5354
6749
|
const full = { version: 1, ...record };
|
|
5355
6750
|
await fs4.writeFile(
|
|
5356
6751
|
paths.sessionFile(record.sessionId),
|
|
@@ -5383,10 +6778,18 @@ var SessionStore = class {
|
|
|
5383
6778
|
return;
|
|
5384
6779
|
}
|
|
5385
6780
|
try {
|
|
5386
|
-
await fs4.unlink(paths.sessionFile(sessionId));
|
|
6781
|
+
await fs4.unlink(paths.sessionFile(sessionId));
|
|
6782
|
+
} catch (err) {
|
|
6783
|
+
const e = err;
|
|
6784
|
+
if (e.code !== "ENOENT") {
|
|
6785
|
+
throw err;
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
try {
|
|
6789
|
+
await fs4.rmdir(paths.sessionDir(sessionId));
|
|
5387
6790
|
} catch (err) {
|
|
5388
6791
|
const e = err;
|
|
5389
|
-
if (e.code !== "ENOENT") {
|
|
6792
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
5390
6793
|
throw err;
|
|
5391
6794
|
}
|
|
5392
6795
|
}
|
|
@@ -5404,11 +6807,7 @@ var SessionStore = class {
|
|
|
5404
6807
|
}
|
|
5405
6808
|
const records = [];
|
|
5406
6809
|
for (const entry of entries) {
|
|
5407
|
-
|
|
5408
|
-
continue;
|
|
5409
|
-
}
|
|
5410
|
-
const id = entry.slice(0, -".json".length);
|
|
5411
|
-
const record = await this.read(id);
|
|
6810
|
+
const record = await this.read(entry);
|
|
5412
6811
|
if (record) {
|
|
5413
6812
|
records.push(record);
|
|
5414
6813
|
}
|
|
@@ -5425,18 +6824,146 @@ function recordFromMemorySession(args) {
|
|
|
5425
6824
|
cwd: args.cwd,
|
|
5426
6825
|
title: args.title,
|
|
5427
6826
|
agentArgs: args.agentArgs,
|
|
6827
|
+
currentModel: args.currentModel,
|
|
6828
|
+
currentMode: args.currentMode,
|
|
6829
|
+
agentCommands: args.agentCommands,
|
|
5428
6830
|
createdAt: args.createdAt ?? now,
|
|
5429
6831
|
updatedAt: args.updatedAt ?? now
|
|
5430
6832
|
};
|
|
5431
6833
|
}
|
|
5432
6834
|
|
|
6835
|
+
// src/core/history-store.ts
|
|
6836
|
+
init_paths();
|
|
6837
|
+
import * as fs5 from "fs/promises";
|
|
6838
|
+
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
6839
|
+
var MAX_ENTRIES = 1e3;
|
|
6840
|
+
var HistoryStore = class {
|
|
6841
|
+
// Serialize writes per session id so appends and rewrites don't
|
|
6842
|
+
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
6843
|
+
// failed append doesn't poison every subsequent write.
|
|
6844
|
+
writeQueues = /* @__PURE__ */ new Map();
|
|
6845
|
+
async append(sessionId, entry) {
|
|
6846
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6847
|
+
return;
|
|
6848
|
+
}
|
|
6849
|
+
return this.enqueue(sessionId, async () => {
|
|
6850
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
6851
|
+
const line = JSON.stringify(entry) + "\n";
|
|
6852
|
+
await fs5.appendFile(paths.historyFile(sessionId), line, {
|
|
6853
|
+
encoding: "utf8",
|
|
6854
|
+
mode: 384
|
|
6855
|
+
});
|
|
6856
|
+
});
|
|
6857
|
+
}
|
|
6858
|
+
async rewrite(sessionId, entries) {
|
|
6859
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6860
|
+
return;
|
|
6861
|
+
}
|
|
6862
|
+
return this.enqueue(sessionId, async () => {
|
|
6863
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
6864
|
+
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
6865
|
+
await fs5.writeFile(paths.historyFile(sessionId), body, {
|
|
6866
|
+
encoding: "utf8",
|
|
6867
|
+
mode: 384
|
|
6868
|
+
});
|
|
6869
|
+
});
|
|
6870
|
+
}
|
|
6871
|
+
async load(sessionId) {
|
|
6872
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6873
|
+
return [];
|
|
6874
|
+
}
|
|
6875
|
+
const pending = this.writeQueues.get(sessionId);
|
|
6876
|
+
if (pending) {
|
|
6877
|
+
await pending;
|
|
6878
|
+
}
|
|
6879
|
+
let raw;
|
|
6880
|
+
try {
|
|
6881
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
6882
|
+
} catch (err) {
|
|
6883
|
+
const e = err;
|
|
6884
|
+
if (e.code === "ENOENT") {
|
|
6885
|
+
return [];
|
|
6886
|
+
}
|
|
6887
|
+
throw err;
|
|
6888
|
+
}
|
|
6889
|
+
const out = [];
|
|
6890
|
+
for (const line of raw.split("\n")) {
|
|
6891
|
+
if (line.length === 0) {
|
|
6892
|
+
continue;
|
|
6893
|
+
}
|
|
6894
|
+
let parsed;
|
|
6895
|
+
try {
|
|
6896
|
+
parsed = JSON.parse(line);
|
|
6897
|
+
} catch {
|
|
6898
|
+
continue;
|
|
6899
|
+
}
|
|
6900
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6901
|
+
continue;
|
|
6902
|
+
}
|
|
6903
|
+
const obj = parsed;
|
|
6904
|
+
if (typeof obj.method !== "string") {
|
|
6905
|
+
continue;
|
|
6906
|
+
}
|
|
6907
|
+
if (typeof obj.recordedAt !== "number") {
|
|
6908
|
+
continue;
|
|
6909
|
+
}
|
|
6910
|
+
out.push({
|
|
6911
|
+
method: obj.method,
|
|
6912
|
+
params: obj.params,
|
|
6913
|
+
recordedAt: obj.recordedAt
|
|
6914
|
+
});
|
|
6915
|
+
}
|
|
6916
|
+
if (out.length > MAX_ENTRIES) {
|
|
6917
|
+
return out.slice(-MAX_ENTRIES);
|
|
6918
|
+
}
|
|
6919
|
+
return out;
|
|
6920
|
+
}
|
|
6921
|
+
async delete(sessionId) {
|
|
6922
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6923
|
+
return;
|
|
6924
|
+
}
|
|
6925
|
+
return this.enqueue(sessionId, async () => {
|
|
6926
|
+
try {
|
|
6927
|
+
await fs5.unlink(paths.historyFile(sessionId));
|
|
6928
|
+
} catch (err) {
|
|
6929
|
+
const e = err;
|
|
6930
|
+
if (e.code !== "ENOENT") {
|
|
6931
|
+
throw err;
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
try {
|
|
6935
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
6936
|
+
} catch (err) {
|
|
6937
|
+
const e = err;
|
|
6938
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
6939
|
+
throw err;
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
});
|
|
6943
|
+
}
|
|
6944
|
+
enqueue(sessionId, task) {
|
|
6945
|
+
const prev = this.writeQueues.get(sessionId) ?? Promise.resolve();
|
|
6946
|
+
const task$ = prev.then(task, task);
|
|
6947
|
+
const settled = task$.catch(() => void 0);
|
|
6948
|
+
this.writeQueues.set(sessionId, settled);
|
|
6949
|
+
void settled.finally(() => {
|
|
6950
|
+
if (this.writeQueues.get(sessionId) === settled) {
|
|
6951
|
+
this.writeQueues.delete(sessionId);
|
|
6952
|
+
}
|
|
6953
|
+
});
|
|
6954
|
+
return task$;
|
|
6955
|
+
}
|
|
6956
|
+
};
|
|
6957
|
+
|
|
5433
6958
|
// src/core/session-manager.ts
|
|
6959
|
+
init_paths();
|
|
5434
6960
|
init_types();
|
|
5435
6961
|
var SessionManager = class {
|
|
5436
6962
|
constructor(registry, spawner, store, options = {}) {
|
|
5437
6963
|
this.registry = registry;
|
|
5438
6964
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
5439
6965
|
this.store = store ?? new SessionStore();
|
|
6966
|
+
this.histories = new HistoryStore();
|
|
5440
6967
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
5441
6968
|
}
|
|
5442
6969
|
registry;
|
|
@@ -5444,7 +6971,12 @@ var SessionManager = class {
|
|
|
5444
6971
|
resurrectionInflight = /* @__PURE__ */ new Map();
|
|
5445
6972
|
spawner;
|
|
5446
6973
|
store;
|
|
6974
|
+
histories;
|
|
5447
6975
|
idleTimeoutMs;
|
|
6976
|
+
// Serialize meta.json read-modify-write operations per session id so
|
|
6977
|
+
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
6978
|
+
// back-to-back) don't lose writes via interleaved reads.
|
|
6979
|
+
metaWriteQueues = /* @__PURE__ */ new Map();
|
|
5448
6980
|
async create(params) {
|
|
5449
6981
|
const fresh = await this.bootstrapAgent({
|
|
5450
6982
|
agentId: params.agentId,
|
|
@@ -5461,7 +6993,8 @@ var SessionManager = class {
|
|
|
5461
6993
|
title: params.title,
|
|
5462
6994
|
agentArgs: params.agentArgs,
|
|
5463
6995
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5464
|
-
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] })
|
|
6996
|
+
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
6997
|
+
historyStore: this.histories
|
|
5465
6998
|
});
|
|
5466
6999
|
await this.attachManagerHooks(session);
|
|
5467
7000
|
return session;
|
|
@@ -5537,7 +7070,13 @@ var SessionManager = class {
|
|
|
5537
7070
|
title: params.title,
|
|
5538
7071
|
agentArgs: params.agentArgs,
|
|
5539
7072
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5540
|
-
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] })
|
|
7073
|
+
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7074
|
+
historyStore: this.histories,
|
|
7075
|
+
seedHistory: params.seedHistory,
|
|
7076
|
+
currentModel: params.currentModel,
|
|
7077
|
+
currentMode: params.currentMode,
|
|
7078
|
+
agentCommands: params.agentCommands,
|
|
7079
|
+
firstPromptSeeded: true
|
|
5541
7080
|
});
|
|
5542
7081
|
await this.attachManagerHooks(session);
|
|
5543
7082
|
return session;
|
|
@@ -5591,6 +7130,7 @@ var SessionManager = class {
|
|
|
5591
7130
|
this.sessions.delete(session.sessionId);
|
|
5592
7131
|
if (deleteRecord) {
|
|
5593
7132
|
void this.store.delete(session.sessionId).catch(() => void 0);
|
|
7133
|
+
void this.histories.delete(session.sessionId).catch(() => void 0);
|
|
5594
7134
|
}
|
|
5595
7135
|
});
|
|
5596
7136
|
session.onTitleChange((title) => {
|
|
@@ -5601,6 +7141,24 @@ var SessionManager = class {
|
|
|
5601
7141
|
() => void 0
|
|
5602
7142
|
);
|
|
5603
7143
|
});
|
|
7144
|
+
session.onModelChange((model) => {
|
|
7145
|
+
void this.persistSnapshot(session.sessionId, { currentModel: model }).catch(
|
|
7146
|
+
() => void 0
|
|
7147
|
+
);
|
|
7148
|
+
});
|
|
7149
|
+
session.onModeChange((mode) => {
|
|
7150
|
+
void this.persistSnapshot(session.sessionId, { currentMode: mode }).catch(
|
|
7151
|
+
() => void 0
|
|
7152
|
+
);
|
|
7153
|
+
});
|
|
7154
|
+
session.onAgentCommandsChange((commands) => {
|
|
7155
|
+
void this.persistSnapshot(session.sessionId, {
|
|
7156
|
+
agentCommands: commands.map((c) => ({
|
|
7157
|
+
name: c.name,
|
|
7158
|
+
...c.description !== void 0 ? { description: c.description } : {}
|
|
7159
|
+
}))
|
|
7160
|
+
}).catch(() => void 0);
|
|
7161
|
+
});
|
|
5604
7162
|
this.sessions.set(session.sessionId, session);
|
|
5605
7163
|
await this.store.write(
|
|
5606
7164
|
recordFromMemorySession({
|
|
@@ -5609,22 +7167,45 @@ var SessionManager = class {
|
|
|
5609
7167
|
agentId: session.agentId,
|
|
5610
7168
|
cwd: session.cwd,
|
|
5611
7169
|
title: session.title,
|
|
5612
|
-
agentArgs: session.agentArgs
|
|
7170
|
+
agentArgs: session.agentArgs,
|
|
7171
|
+
currentModel: session.currentModel,
|
|
7172
|
+
currentMode: session.currentMode
|
|
5613
7173
|
})
|
|
5614
7174
|
).catch(() => void 0);
|
|
5615
7175
|
}
|
|
7176
|
+
// Resolve a session's recorded history without forcing a resurrect.
|
|
7177
|
+
// Returns the in-memory snapshot if the session is hot, falls back
|
|
7178
|
+
// to the on-disk history file otherwise. Returns undefined if the
|
|
7179
|
+
// session id is unknown to both the live map and disk store, so the
|
|
7180
|
+
// caller can distinguish "no history yet" (empty array) from "404".
|
|
7181
|
+
async getHistory(sessionId) {
|
|
7182
|
+
const live = this.sessions.get(sessionId);
|
|
7183
|
+
if (live) {
|
|
7184
|
+
return live.getHistorySnapshot();
|
|
7185
|
+
}
|
|
7186
|
+
const record = await this.store.read(sessionId);
|
|
7187
|
+
if (!record) {
|
|
7188
|
+
return void 0;
|
|
7189
|
+
}
|
|
7190
|
+
return this.histories.load(sessionId).catch(() => []);
|
|
7191
|
+
}
|
|
5616
7192
|
async loadFromDisk(sessionId) {
|
|
5617
7193
|
const record = await this.store.read(sessionId);
|
|
5618
7194
|
if (!record) {
|
|
5619
7195
|
return void 0;
|
|
5620
7196
|
}
|
|
7197
|
+
const seedHistory = await this.histories.load(sessionId).catch(() => []);
|
|
5621
7198
|
return {
|
|
5622
7199
|
hydraSessionId: record.sessionId,
|
|
5623
7200
|
upstreamSessionId: record.upstreamSessionId,
|
|
5624
7201
|
agentId: record.agentId,
|
|
5625
7202
|
cwd: record.cwd,
|
|
5626
7203
|
title: record.title,
|
|
5627
|
-
agentArgs: record.agentArgs
|
|
7204
|
+
agentArgs: record.agentArgs,
|
|
7205
|
+
seedHistory: seedHistory.length > 0 ? seedHistory : void 0,
|
|
7206
|
+
currentModel: record.currentModel,
|
|
7207
|
+
currentMode: record.currentMode,
|
|
7208
|
+
agentCommands: record.agentCommands
|
|
5628
7209
|
};
|
|
5629
7210
|
}
|
|
5630
7211
|
get(sessionId) {
|
|
@@ -5666,13 +7247,14 @@ var SessionManager = class {
|
|
|
5666
7247
|
continue;
|
|
5667
7248
|
}
|
|
5668
7249
|
liveIds.add(session.sessionId);
|
|
7250
|
+
const used = await historyMtimeIso(session.sessionId) ?? new Date(session.updatedAt).toISOString();
|
|
5669
7251
|
entries.push({
|
|
5670
7252
|
sessionId: session.sessionId,
|
|
5671
7253
|
upstreamSessionId: session.upstreamSessionId,
|
|
5672
7254
|
cwd: session.cwd,
|
|
5673
7255
|
title: session.title,
|
|
5674
7256
|
agentId: session.agentId,
|
|
5675
|
-
updatedAt:
|
|
7257
|
+
updatedAt: used,
|
|
5676
7258
|
attachedClients: session.attachedCount,
|
|
5677
7259
|
status: "live"
|
|
5678
7260
|
});
|
|
@@ -5685,13 +7267,14 @@ var SessionManager = class {
|
|
|
5685
7267
|
if (filter.cwd && r.cwd !== filter.cwd) {
|
|
5686
7268
|
continue;
|
|
5687
7269
|
}
|
|
7270
|
+
const used = await historyMtimeIso(r.sessionId) ?? r.updatedAt;
|
|
5688
7271
|
entries.push({
|
|
5689
7272
|
sessionId: r.sessionId,
|
|
5690
7273
|
upstreamSessionId: r.upstreamSessionId,
|
|
5691
7274
|
cwd: r.cwd,
|
|
5692
7275
|
title: r.title,
|
|
5693
7276
|
agentId: r.agentId,
|
|
5694
|
-
updatedAt:
|
|
7277
|
+
updatedAt: used,
|
|
5695
7278
|
attachedClients: 0,
|
|
5696
7279
|
status: "cold"
|
|
5697
7280
|
});
|
|
@@ -5707,19 +7290,25 @@ var SessionManager = class {
|
|
|
5707
7290
|
await this.store.delete(sessionId).catch(() => void 0);
|
|
5708
7291
|
return true;
|
|
5709
7292
|
}
|
|
7293
|
+
async hasRecord(sessionId) {
|
|
7294
|
+
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
7295
|
+
return record !== void 0;
|
|
7296
|
+
}
|
|
5710
7297
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
5711
7298
|
// was written at create time; updating it here keeps the session
|
|
5712
7299
|
// record's title in sync with what was broadcast to clients so a
|
|
5713
7300
|
// daemon restart (and later resurrect) restores the same title.
|
|
5714
7301
|
async persistTitle(sessionId, title) {
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
7302
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
7303
|
+
const record = await this.store.read(sessionId);
|
|
7304
|
+
if (!record) {
|
|
7305
|
+
return;
|
|
7306
|
+
}
|
|
7307
|
+
await this.store.write({
|
|
7308
|
+
...record,
|
|
7309
|
+
title,
|
|
7310
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7311
|
+
});
|
|
5723
7312
|
});
|
|
5724
7313
|
}
|
|
5725
7314
|
// Persist an agent swap from /hydra switch. The on-disk record's
|
|
@@ -5727,28 +7316,71 @@ var SessionManager = class {
|
|
|
5727
7316
|
// later resurrect) brings the session back up on the agent the user
|
|
5728
7317
|
// most recently switched to, not the one it was originally created on.
|
|
5729
7318
|
async persistAgentChange(sessionId, agentId, upstreamSessionId) {
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
7319
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
7320
|
+
const record = await this.store.read(sessionId);
|
|
7321
|
+
if (!record) {
|
|
7322
|
+
return;
|
|
7323
|
+
}
|
|
7324
|
+
await this.store.write({
|
|
7325
|
+
...record,
|
|
7326
|
+
agentId,
|
|
7327
|
+
upstreamSessionId,
|
|
7328
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7329
|
+
});
|
|
7330
|
+
});
|
|
7331
|
+
}
|
|
7332
|
+
// Update one or more snapshot fields (model, mode, commands) in
|
|
7333
|
+
// meta.json. Used so cold-resurrect can deliver the latest snapshot
|
|
7334
|
+
// to attaching clients via the attach response _meta. No-op if the
|
|
7335
|
+
// session record has gone away (race with deleteRecord).
|
|
7336
|
+
async persistSnapshot(sessionId, update) {
|
|
7337
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
7338
|
+
const record = await this.store.read(sessionId);
|
|
7339
|
+
if (!record) {
|
|
7340
|
+
return;
|
|
7341
|
+
}
|
|
7342
|
+
await this.store.write({
|
|
7343
|
+
...record,
|
|
7344
|
+
...update.currentModel !== void 0 ? { currentModel: update.currentModel } : {},
|
|
7345
|
+
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
7346
|
+
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
7347
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7348
|
+
});
|
|
5739
7349
|
});
|
|
5740
7350
|
}
|
|
7351
|
+
// Serialize meta.json writes per session id so concurrent
|
|
7352
|
+
// read-modify-write operations don't interleave reads.
|
|
7353
|
+
enqueueMetaWrite(sessionId, task) {
|
|
7354
|
+
const prev = this.metaWriteQueues.get(sessionId) ?? Promise.resolve();
|
|
7355
|
+
const next = prev.then(task, task);
|
|
7356
|
+
const settled = next.catch(() => void 0);
|
|
7357
|
+
this.metaWriteQueues.set(sessionId, settled);
|
|
7358
|
+
void settled.finally(() => {
|
|
7359
|
+
if (this.metaWriteQueues.get(sessionId) === settled) {
|
|
7360
|
+
this.metaWriteQueues.delete(sessionId);
|
|
7361
|
+
}
|
|
7362
|
+
});
|
|
7363
|
+
return next;
|
|
7364
|
+
}
|
|
5741
7365
|
async closeAll() {
|
|
5742
7366
|
const sessions = [...this.sessions.values()];
|
|
5743
7367
|
await Promise.allSettled(sessions.map((s) => s.close()));
|
|
5744
7368
|
this.sessions.clear();
|
|
5745
7369
|
}
|
|
5746
7370
|
};
|
|
7371
|
+
async function historyMtimeIso(sessionId) {
|
|
7372
|
+
try {
|
|
7373
|
+
const st = await fs6.stat(paths.historyFile(sessionId));
|
|
7374
|
+
return new Date(st.mtimeMs).toISOString();
|
|
7375
|
+
} catch {
|
|
7376
|
+
return void 0;
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
5747
7379
|
|
|
5748
7380
|
// src/core/extensions.ts
|
|
5749
7381
|
init_paths();
|
|
5750
7382
|
import { spawn as spawn2 } from "child_process";
|
|
5751
|
-
import * as
|
|
7383
|
+
import * as fs7 from "fs";
|
|
5752
7384
|
import * as fsp from "fs/promises";
|
|
5753
7385
|
import * as path3 from "path";
|
|
5754
7386
|
var RESTART_BASE_MS = 1e3;
|
|
@@ -6031,7 +7663,7 @@ var ExtensionManager = class {
|
|
|
6031
7663
|
}
|
|
6032
7664
|
const ext = entry.config;
|
|
6033
7665
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
6034
|
-
const logStream =
|
|
7666
|
+
const logStream = fs7.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
6035
7667
|
flags: "a"
|
|
6036
7668
|
});
|
|
6037
7669
|
logStream.write(
|
|
@@ -6081,7 +7713,7 @@ var ExtensionManager = class {
|
|
|
6081
7713
|
}
|
|
6082
7714
|
if (typeof child.pid === "number") {
|
|
6083
7715
|
try {
|
|
6084
|
-
|
|
7716
|
+
fs7.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
6085
7717
|
`, {
|
|
6086
7718
|
encoding: "utf8",
|
|
6087
7719
|
mode: 384
|
|
@@ -6106,7 +7738,7 @@ var ExtensionManager = class {
|
|
|
6106
7738
|
});
|
|
6107
7739
|
child.on("exit", (code, signal) => {
|
|
6108
7740
|
try {
|
|
6109
|
-
|
|
7741
|
+
fs7.unlinkSync(paths.extensionPidFile(ext.name));
|
|
6110
7742
|
} catch {
|
|
6111
7743
|
}
|
|
6112
7744
|
logStream.write(
|
|
@@ -6222,8 +7854,7 @@ init_config();
|
|
|
6222
7854
|
function registerSessionRoutes(app, manager, defaults) {
|
|
6223
7855
|
app.get("/v1/sessions", async (request) => {
|
|
6224
7856
|
const query = request.query;
|
|
6225
|
-
const
|
|
6226
|
-
const sessions = await manager.list({ cwd: query?.cwd, all });
|
|
7857
|
+
const sessions = await manager.list({ cwd: query?.cwd });
|
|
6227
7858
|
return { sessions };
|
|
6228
7859
|
});
|
|
6229
7860
|
app.post("/v1/sessions", async (request, reply) => {
|
|
@@ -6245,6 +7876,22 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
6245
7876
|
reply.code(500).send({ error: err.message });
|
|
6246
7877
|
}
|
|
6247
7878
|
});
|
|
7879
|
+
app.post("/v1/sessions/:id/kill", async (request, reply) => {
|
|
7880
|
+
const raw = request.params.id;
|
|
7881
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
7882
|
+
const session = manager.get(id);
|
|
7883
|
+
if (session) {
|
|
7884
|
+
await session.close({ deleteRecord: false });
|
|
7885
|
+
reply.code(204).send();
|
|
7886
|
+
return;
|
|
7887
|
+
}
|
|
7888
|
+
const exists = await manager.hasRecord(id);
|
|
7889
|
+
if (!exists) {
|
|
7890
|
+
reply.code(404).send({ error: "session not found" });
|
|
7891
|
+
return;
|
|
7892
|
+
}
|
|
7893
|
+
reply.code(204).send();
|
|
7894
|
+
});
|
|
6248
7895
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
6249
7896
|
const raw = request.params.id;
|
|
6250
7897
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -6261,6 +7908,50 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
6261
7908
|
}
|
|
6262
7909
|
reply.code(204).send();
|
|
6263
7910
|
});
|
|
7911
|
+
app.get("/v1/sessions/:id/history", async (request, reply) => {
|
|
7912
|
+
const raw = request.params.id;
|
|
7913
|
+
const query = request.query;
|
|
7914
|
+
const follow = query?.follow === "1" || query?.follow === "true";
|
|
7915
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
7916
|
+
const live = manager.get(id);
|
|
7917
|
+
let snapshot;
|
|
7918
|
+
let unsubscribe;
|
|
7919
|
+
if (live) {
|
|
7920
|
+
snapshot = live.getHistorySnapshot();
|
|
7921
|
+
if (follow) {
|
|
7922
|
+
unsubscribe = live.onBroadcast((entry) => {
|
|
7923
|
+
if (reply.raw.writableEnded) {
|
|
7924
|
+
return;
|
|
7925
|
+
}
|
|
7926
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
7927
|
+
});
|
|
7928
|
+
}
|
|
7929
|
+
} else {
|
|
7930
|
+
const cold = await manager.getHistory(id);
|
|
7931
|
+
if (cold === void 0) {
|
|
7932
|
+
reply.code(404).send({ error: "session not found" });
|
|
7933
|
+
return reply;
|
|
7934
|
+
}
|
|
7935
|
+
snapshot = cold;
|
|
7936
|
+
}
|
|
7937
|
+
reply.raw.setHeader("Content-Type", "application/x-ndjson");
|
|
7938
|
+
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
7939
|
+
reply.raw.statusCode = 200;
|
|
7940
|
+
for (const entry of snapshot ?? []) {
|
|
7941
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
7942
|
+
}
|
|
7943
|
+
if (!unsubscribe) {
|
|
7944
|
+
reply.raw.end();
|
|
7945
|
+
return reply;
|
|
7946
|
+
}
|
|
7947
|
+
request.raw.on("close", () => {
|
|
7948
|
+
unsubscribe?.();
|
|
7949
|
+
if (!reply.raw.writableEnded) {
|
|
7950
|
+
reply.raw.end();
|
|
7951
|
+
}
|
|
7952
|
+
});
|
|
7953
|
+
return reply;
|
|
7954
|
+
});
|
|
6264
7955
|
}
|
|
6265
7956
|
|
|
6266
7957
|
// src/daemon/routes/agents.ts
|
|
@@ -6408,6 +8099,16 @@ function parseRegisterBody(body) {
|
|
|
6408
8099
|
};
|
|
6409
8100
|
}
|
|
6410
8101
|
|
|
8102
|
+
// src/daemon/routes/config.ts
|
|
8103
|
+
function registerConfigRoutes(app, defaults) {
|
|
8104
|
+
app.get("/v1/config", async () => {
|
|
8105
|
+
return {
|
|
8106
|
+
defaultAgent: defaults.defaultAgent,
|
|
8107
|
+
defaultCwd: defaults.defaultCwd
|
|
8108
|
+
};
|
|
8109
|
+
});
|
|
8110
|
+
}
|
|
8111
|
+
|
|
6411
8112
|
// src/daemon/acp-ws.ts
|
|
6412
8113
|
init_connection();
|
|
6413
8114
|
init_ws_stream();
|
|
@@ -6651,6 +8352,19 @@ function buildResponseMeta(session) {
|
|
|
6651
8352
|
if (session.agentArgs && session.agentArgs.length > 0) {
|
|
6652
8353
|
ours.agentArgs = session.agentArgs;
|
|
6653
8354
|
}
|
|
8355
|
+
if (session.currentModel !== void 0) {
|
|
8356
|
+
ours.currentModel = session.currentModel;
|
|
8357
|
+
}
|
|
8358
|
+
if (session.currentMode !== void 0) {
|
|
8359
|
+
ours.currentMode = session.currentMode;
|
|
8360
|
+
}
|
|
8361
|
+
const commands = session.mergedAvailableCommands();
|
|
8362
|
+
if (commands.length > 0) {
|
|
8363
|
+
ours.availableCommands = commands;
|
|
8364
|
+
}
|
|
8365
|
+
if (session.turnStartedAt !== void 0) {
|
|
8366
|
+
ours.turnStartedAt = session.turnStartedAt;
|
|
8367
|
+
}
|
|
6654
8368
|
return mergeMeta(session.agentMeta, ours);
|
|
6655
8369
|
}
|
|
6656
8370
|
function buildInitializeResult() {
|
|
@@ -6736,6 +8450,10 @@ async function startDaemon(config) {
|
|
|
6736
8450
|
});
|
|
6737
8451
|
registerAgentRoutes(app, registry);
|
|
6738
8452
|
registerExtensionRoutes(app, extensions);
|
|
8453
|
+
registerConfigRoutes(app, {
|
|
8454
|
+
defaultAgent: config.defaultAgent,
|
|
8455
|
+
defaultCwd: config.defaultCwd
|
|
8456
|
+
});
|
|
6739
8457
|
registerAcpWsEndpoint(app, {
|
|
6740
8458
|
config,
|
|
6741
8459
|
manager,
|
|
@@ -6771,7 +8489,7 @@ async function startDaemon(config) {
|
|
|
6771
8489
|
await manager.closeAll();
|
|
6772
8490
|
await app.close();
|
|
6773
8491
|
try {
|
|
6774
|
-
|
|
8492
|
+
fs8.unlinkSync(paths.pidFile());
|
|
6775
8493
|
} catch {
|
|
6776
8494
|
}
|
|
6777
8495
|
try {
|
|
@@ -6810,13 +8528,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
6810
8528
|
init_daemon_bootstrap();
|
|
6811
8529
|
|
|
6812
8530
|
// src/cli/commands/log-tail.ts
|
|
6813
|
-
import * as
|
|
8531
|
+
import * as fs9 from "fs";
|
|
6814
8532
|
import * as fsp3 from "fs/promises";
|
|
6815
8533
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
6816
8534
|
const opts = parseLogTailFlags(argv);
|
|
6817
|
-
let
|
|
8535
|
+
let stat3;
|
|
6818
8536
|
try {
|
|
6819
|
-
|
|
8537
|
+
stat3 = await fsp3.stat(logPath);
|
|
6820
8538
|
} catch (err) {
|
|
6821
8539
|
const e = err;
|
|
6822
8540
|
if (e.code === "ENOENT") {
|
|
@@ -6827,14 +8545,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
6827
8545
|
}
|
|
6828
8546
|
throw err;
|
|
6829
8547
|
}
|
|
6830
|
-
let position = await printTail(logPath,
|
|
8548
|
+
let position = await printTail(logPath, stat3.size, opts.tail);
|
|
6831
8549
|
if (!opts.follow) {
|
|
6832
8550
|
return;
|
|
6833
8551
|
}
|
|
6834
8552
|
process.stdout.write(`-- following ${logPath} --
|
|
6835
8553
|
`);
|
|
6836
8554
|
let pending = false;
|
|
6837
|
-
const watcher =
|
|
8555
|
+
const watcher = fs9.watch(logPath, () => {
|
|
6838
8556
|
if (pending) {
|
|
6839
8557
|
return;
|
|
6840
8558
|
}
|
|
@@ -7592,226 +9310,7 @@ function maxLen3(headerCell, values) {
|
|
|
7592
9310
|
// src/shim/proxy.ts
|
|
7593
9311
|
init_config();
|
|
7594
9312
|
init_daemon_bootstrap();
|
|
7595
|
-
|
|
7596
|
-
// src/shim/resilient-ws.ts
|
|
7597
|
-
init_ws_stream();
|
|
7598
|
-
init_types();
|
|
7599
|
-
import { setTimeout as sleep3 } from "timers/promises";
|
|
7600
|
-
import { WebSocket } from "ws";
|
|
7601
|
-
var BACKOFF_INITIAL_MS = 200;
|
|
7602
|
-
var BACKOFF_MAX_MS = 5e3;
|
|
7603
|
-
var BACKOFF_MULTIPLIER = 2;
|
|
7604
|
-
var MAX_RECONNECT_ATTEMPTS = 60;
|
|
7605
|
-
var ResilientWsStream = class {
|
|
7606
|
-
constructor(opts) {
|
|
7607
|
-
this.opts = opts;
|
|
7608
|
-
}
|
|
7609
|
-
opts;
|
|
7610
|
-
current;
|
|
7611
|
-
outboundQueue = [];
|
|
7612
|
-
messageHandlers = [];
|
|
7613
|
-
closeHandlers = [];
|
|
7614
|
-
destroyed = false;
|
|
7615
|
-
firstConnect = true;
|
|
7616
|
-
reconnectInFlight;
|
|
7617
|
-
connectGate;
|
|
7618
|
-
releaseConnectGate;
|
|
7619
|
-
pendingRequests = /* @__PURE__ */ new Map();
|
|
7620
|
-
async start() {
|
|
7621
|
-
await this.connectWithRetry();
|
|
7622
|
-
}
|
|
7623
|
-
onMessage(handler) {
|
|
7624
|
-
this.messageHandlers.push(handler);
|
|
7625
|
-
}
|
|
7626
|
-
onClose(handler) {
|
|
7627
|
-
this.closeHandlers.push(handler);
|
|
7628
|
-
}
|
|
7629
|
-
async send(message) {
|
|
7630
|
-
if (this.destroyed) {
|
|
7631
|
-
throw new Error("resilient ws stream is destroyed");
|
|
7632
|
-
}
|
|
7633
|
-
if (this.connectGate || !this.current) {
|
|
7634
|
-
this.outboundQueue.push(message);
|
|
7635
|
-
return;
|
|
7636
|
-
}
|
|
7637
|
-
try {
|
|
7638
|
-
await this.current.send(message);
|
|
7639
|
-
} catch (err) {
|
|
7640
|
-
this.outboundQueue.push(message);
|
|
7641
|
-
this.scheduleReconnect(err);
|
|
7642
|
-
}
|
|
7643
|
-
}
|
|
7644
|
-
// Send a request directly and resolve when the matching response arrives
|
|
7645
|
-
// on the same connection. Used by onConnect handlers to await replay-attach
|
|
7646
|
-
// responses before letting the outbound queue drain. Bypasses the
|
|
7647
|
-
// connectGate intentionally.
|
|
7648
|
-
async request(message) {
|
|
7649
|
-
if (this.destroyed) {
|
|
7650
|
-
throw new Error("resilient ws stream is destroyed");
|
|
7651
|
-
}
|
|
7652
|
-
if (!this.current) {
|
|
7653
|
-
throw new Error("resilient ws stream not connected");
|
|
7654
|
-
}
|
|
7655
|
-
const id = message.id;
|
|
7656
|
-
const promise = new Promise((resolve2, reject) => {
|
|
7657
|
-
this.pendingRequests.set(id, { resolve: resolve2, reject });
|
|
7658
|
-
});
|
|
7659
|
-
try {
|
|
7660
|
-
await this.current.send(message);
|
|
7661
|
-
} catch (err) {
|
|
7662
|
-
this.pendingRequests.delete(id);
|
|
7663
|
-
throw err;
|
|
7664
|
-
}
|
|
7665
|
-
return promise;
|
|
7666
|
-
}
|
|
7667
|
-
async close() {
|
|
7668
|
-
this.destroyed = true;
|
|
7669
|
-
if (this.current) {
|
|
7670
|
-
await this.current.close().catch(() => void 0);
|
|
7671
|
-
}
|
|
7672
|
-
for (const handler of this.closeHandlers) {
|
|
7673
|
-
handler();
|
|
7674
|
-
}
|
|
7675
|
-
}
|
|
7676
|
-
async connectWithRetry() {
|
|
7677
|
-
let attempt = 0;
|
|
7678
|
-
let backoff = BACKOFF_INITIAL_MS;
|
|
7679
|
-
while (!this.destroyed) {
|
|
7680
|
-
try {
|
|
7681
|
-
const stream = await openWs(this.opts.url, this.opts.subprotocols);
|
|
7682
|
-
this.bindStream(stream);
|
|
7683
|
-
const wasFirst = this.firstConnect;
|
|
7684
|
-
this.firstConnect = false;
|
|
7685
|
-
this.connectGate = new Promise((resolve2) => {
|
|
7686
|
-
this.releaseConnectGate = resolve2;
|
|
7687
|
-
});
|
|
7688
|
-
try {
|
|
7689
|
-
if (this.opts.onConnect) {
|
|
7690
|
-
try {
|
|
7691
|
-
await this.opts.onConnect(wasFirst);
|
|
7692
|
-
} catch (err) {
|
|
7693
|
-
this.log(
|
|
7694
|
-
`hydra-acp: post-connect handler failed: ${err.message}`
|
|
7695
|
-
);
|
|
7696
|
-
}
|
|
7697
|
-
}
|
|
7698
|
-
} finally {
|
|
7699
|
-
this.releaseConnectGate?.();
|
|
7700
|
-
this.releaseConnectGate = void 0;
|
|
7701
|
-
this.connectGate = void 0;
|
|
7702
|
-
}
|
|
7703
|
-
await this.flushQueue();
|
|
7704
|
-
return;
|
|
7705
|
-
} catch (err) {
|
|
7706
|
-
attempt += 1;
|
|
7707
|
-
if (this.opts.onConnectFailure) {
|
|
7708
|
-
this.opts.onConnectFailure(err);
|
|
7709
|
-
}
|
|
7710
|
-
if (attempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
7711
|
-
throw new Error(
|
|
7712
|
-
`hydra-acp: gave up reconnecting after ${attempt} attempts: ${err.message}`
|
|
7713
|
-
);
|
|
7714
|
-
}
|
|
7715
|
-
this.log(
|
|
7716
|
-
`hydra-acp: connect attempt ${attempt} failed (${err.message}); retrying in ${backoff}ms`
|
|
7717
|
-
);
|
|
7718
|
-
await sleep3(backoff);
|
|
7719
|
-
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
|
|
7720
|
-
}
|
|
7721
|
-
}
|
|
7722
|
-
}
|
|
7723
|
-
bindStream(stream) {
|
|
7724
|
-
this.current = stream;
|
|
7725
|
-
stream.onMessage((msg) => {
|
|
7726
|
-
if (isResponse(msg)) {
|
|
7727
|
-
const pending = this.pendingRequests.get(msg.id);
|
|
7728
|
-
if (pending) {
|
|
7729
|
-
this.pendingRequests.delete(msg.id);
|
|
7730
|
-
pending.resolve(msg);
|
|
7731
|
-
}
|
|
7732
|
-
}
|
|
7733
|
-
for (const handler of this.messageHandlers) {
|
|
7734
|
-
handler(msg);
|
|
7735
|
-
}
|
|
7736
|
-
});
|
|
7737
|
-
stream.onClose((err) => {
|
|
7738
|
-
if (this.destroyed) {
|
|
7739
|
-
return;
|
|
7740
|
-
}
|
|
7741
|
-
this.current = void 0;
|
|
7742
|
-
if (this.pendingRequests.size > 0) {
|
|
7743
|
-
const reason = err ?? new Error("ws closed before response");
|
|
7744
|
-
for (const { reject } of this.pendingRequests.values()) {
|
|
7745
|
-
reject(reason);
|
|
7746
|
-
}
|
|
7747
|
-
this.pendingRequests.clear();
|
|
7748
|
-
}
|
|
7749
|
-
this.scheduleReconnect(err);
|
|
7750
|
-
});
|
|
7751
|
-
}
|
|
7752
|
-
async flushQueue() {
|
|
7753
|
-
if (!this.current) {
|
|
7754
|
-
return;
|
|
7755
|
-
}
|
|
7756
|
-
const queue = this.outboundQueue;
|
|
7757
|
-
this.outboundQueue = [];
|
|
7758
|
-
for (const msg of queue) {
|
|
7759
|
-
try {
|
|
7760
|
-
await this.current.send(msg);
|
|
7761
|
-
} catch (err) {
|
|
7762
|
-
this.outboundQueue.unshift(msg);
|
|
7763
|
-
this.scheduleReconnect(err);
|
|
7764
|
-
return;
|
|
7765
|
-
}
|
|
7766
|
-
}
|
|
7767
|
-
}
|
|
7768
|
-
scheduleReconnect(err) {
|
|
7769
|
-
if (this.destroyed || this.reconnectInFlight) {
|
|
7770
|
-
return;
|
|
7771
|
-
}
|
|
7772
|
-
this.log(
|
|
7773
|
-
`hydra-acp: connection lost (${err?.message ?? "no error"}); reconnecting...`
|
|
7774
|
-
);
|
|
7775
|
-
this.reconnectInFlight = (async () => {
|
|
7776
|
-
try {
|
|
7777
|
-
await this.connectWithRetry();
|
|
7778
|
-
} catch (final) {
|
|
7779
|
-
for (const handler of this.closeHandlers) {
|
|
7780
|
-
handler(final);
|
|
7781
|
-
}
|
|
7782
|
-
this.destroyed = true;
|
|
7783
|
-
} finally {
|
|
7784
|
-
this.reconnectInFlight = void 0;
|
|
7785
|
-
}
|
|
7786
|
-
})();
|
|
7787
|
-
}
|
|
7788
|
-
log(line) {
|
|
7789
|
-
if (this.opts.log) {
|
|
7790
|
-
this.opts.log(line);
|
|
7791
|
-
return;
|
|
7792
|
-
}
|
|
7793
|
-
process.stderr.write(`${line}
|
|
7794
|
-
`);
|
|
7795
|
-
}
|
|
7796
|
-
};
|
|
7797
|
-
function isResponse(msg) {
|
|
7798
|
-
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
7799
|
-
}
|
|
7800
|
-
async function openWs(url, subprotocols) {
|
|
7801
|
-
return new Promise((resolve2, reject) => {
|
|
7802
|
-
const ws = new WebSocket(url, subprotocols);
|
|
7803
|
-
const onOpen = () => {
|
|
7804
|
-
ws.off("error", onError);
|
|
7805
|
-
resolve2(wsToMessageStream(ws));
|
|
7806
|
-
};
|
|
7807
|
-
const onError = (err) => {
|
|
7808
|
-
ws.off("open", onOpen);
|
|
7809
|
-
reject(err);
|
|
7810
|
-
};
|
|
7811
|
-
ws.once("open", onOpen);
|
|
7812
|
-
ws.once("error", onError);
|
|
7813
|
-
});
|
|
7814
|
-
}
|
|
9313
|
+
init_resilient_ws();
|
|
7815
9314
|
|
|
7816
9315
|
// src/shim/session-tracker.ts
|
|
7817
9316
|
init_types();
|
|
@@ -8232,6 +9731,10 @@ async function main() {
|
|
|
8232
9731
|
await runSessionsKill(positional[2]);
|
|
8233
9732
|
return;
|
|
8234
9733
|
}
|
|
9734
|
+
if (sub === "rm") {
|
|
9735
|
+
await runSessionsRm(positional[2]);
|
|
9736
|
+
return;
|
|
9737
|
+
}
|
|
8235
9738
|
process.stderr.write(`Unknown sessions subcommand: ${sub}
|
|
8236
9739
|
`);
|
|
8237
9740
|
process.exit(2);
|
|
@@ -8343,7 +9846,8 @@ function printHelp() {
|
|
|
8343
9846
|
" hydra-acp daemon start|stop|restart|status",
|
|
8344
9847
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
8345
9848
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
8346
|
-
" hydra-acp sessions kill <id>
|
|
9849
|
+
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
9850
|
+
" hydra-acp sessions rm <id> Remove a session entirely (live or cold)",
|
|
8347
9851
|
" hydra-acp extensions list List configured extensions and live state",
|
|
8348
9852
|
" hydra-acp extensions add <name> [opts] Add an extension to config",
|
|
8349
9853
|
" hydra-acp extensions remove <name> Remove an extension from config",
|