@hydra-acp/cli 0.1.2 → 0.1.4
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 +29 -3
- package/dist/cli.js +2234 -601
- package/dist/index.d.ts +188 -8
- package/dist/index.js +754 -119
- 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;
|
|
@@ -43,7 +48,8 @@ var init_paths = __esm({
|
|
|
43
48
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
44
49
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
45
50
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
46
|
-
tuiHistoryFile: () => path.join(hydraHome(), "
|
|
51
|
+
tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
|
|
52
|
+
tuiLogFile: () => path.join(hydraHome(), "tui.log")
|
|
47
53
|
};
|
|
48
54
|
}
|
|
49
55
|
});
|
|
@@ -83,8 +89,7 @@ async function ensureConfig() {
|
|
|
83
89
|
if (e.code !== "ENOENT") {
|
|
84
90
|
throw err;
|
|
85
91
|
}
|
|
86
|
-
const config =
|
|
87
|
-
await writeConfig(config);
|
|
92
|
+
const config = await writeMinimalInitConfig();
|
|
88
93
|
process.stderr.write(
|
|
89
94
|
`hydra-acp: initialized ${paths.config()} with a fresh auth token.
|
|
90
95
|
`
|
|
@@ -93,9 +98,23 @@ async function ensureConfig() {
|
|
|
93
98
|
}
|
|
94
99
|
return loadConfig();
|
|
95
100
|
}
|
|
96
|
-
async function
|
|
101
|
+
async function writeMinimalInitConfig(authToken) {
|
|
102
|
+
const token = authToken ?? generateAuthToken();
|
|
103
|
+
const minimal = { daemon: { authToken: token } };
|
|
97
104
|
await fs.mkdir(paths.home(), { recursive: true });
|
|
98
|
-
await fs.writeFile(paths.config(), JSON.stringify(
|
|
105
|
+
await fs.writeFile(paths.config(), JSON.stringify(minimal, null, 2) + "\n", {
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
mode: 384
|
|
108
|
+
});
|
|
109
|
+
return HydraConfig.parse(minimal);
|
|
110
|
+
}
|
|
111
|
+
async function updateConfigField(mutate) {
|
|
112
|
+
const path6 = paths.config();
|
|
113
|
+
const text = await fs.readFile(path6, "utf8");
|
|
114
|
+
const raw = JSON.parse(text);
|
|
115
|
+
mutate(raw);
|
|
116
|
+
HydraConfig.parse(raw);
|
|
117
|
+
await fs.writeFile(path6, JSON.stringify(raw, null, 2) + "\n", {
|
|
99
118
|
encoding: "utf8",
|
|
100
119
|
mode: 384
|
|
101
120
|
});
|
|
@@ -109,13 +128,6 @@ function generateAuthToken() {
|
|
|
109
128
|
}
|
|
110
129
|
return `hydra_token_${hex}`;
|
|
111
130
|
}
|
|
112
|
-
function defaultConfig() {
|
|
113
|
-
return HydraConfig.parse({
|
|
114
|
-
daemon: {
|
|
115
|
-
authToken: generateAuthToken()
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
131
|
function expandHome(p) {
|
|
120
132
|
if (p === "~" || p === "$HOME") {
|
|
121
133
|
return homedir2();
|
|
@@ -144,7 +156,7 @@ var init_config = __esm({
|
|
|
144
156
|
authToken: z.string().min(16),
|
|
145
157
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
146
158
|
tls: TlsConfig.optional(),
|
|
147
|
-
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(
|
|
159
|
+
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
|
|
148
160
|
});
|
|
149
161
|
RegistryConfig = z.object({
|
|
150
162
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -157,7 +169,11 @@ var init_config = __esm({
|
|
|
157
169
|
// /clear, ^L, resize — bypass this throttle. Default 1000 (1 Hz) keeps
|
|
158
170
|
// CPU low during heavy streaming; bump to 250 for 4 Hz, 100 for ~10 Hz,
|
|
159
171
|
// or 0 to disable throttling entirely.
|
|
160
|
-
repaintThrottleMs: z.number().int().nonnegative().default(1e3)
|
|
172
|
+
repaintThrottleMs: z.number().int().nonnegative().default(1e3),
|
|
173
|
+
// Cap on logical lines retained in the in-memory scrollback render
|
|
174
|
+
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
175
|
+
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
176
|
+
maxScrollbackLines: z.number().int().positive().default(1e4)
|
|
161
177
|
});
|
|
162
178
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
163
179
|
ExtensionBody = z.object({
|
|
@@ -182,7 +198,7 @@ var init_config = __esm({
|
|
|
182
198
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
183
199
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
184
200
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
185
|
-
tui: TuiConfig.default({ repaintThrottleMs: 1e3 })
|
|
201
|
+
tui: TuiConfig.default({ repaintThrottleMs: 1e3, maxScrollbackLines: 1e4 })
|
|
186
202
|
});
|
|
187
203
|
}
|
|
188
204
|
});
|
|
@@ -226,6 +242,9 @@ function extractHydraMeta(meta) {
|
|
|
226
242
|
if (typeof obj.currentMode === "string") {
|
|
227
243
|
out.currentMode = obj.currentMode;
|
|
228
244
|
}
|
|
245
|
+
if (typeof obj.turnStartedAt === "number" && obj.turnStartedAt > 0) {
|
|
246
|
+
out.turnStartedAt = obj.turnStartedAt;
|
|
247
|
+
}
|
|
229
248
|
if (Array.isArray(obj.availableCommands)) {
|
|
230
249
|
const cmds = [];
|
|
231
250
|
for (const raw of obj.availableCommands) {
|
|
@@ -264,7 +283,8 @@ var init_types = __esm({
|
|
|
264
283
|
SessionNotFound: -32001,
|
|
265
284
|
PermissionDenied: -32002,
|
|
266
285
|
AlreadyAttached: -32003,
|
|
267
|
-
AgentNotInstalled: -32005
|
|
286
|
+
AgentNotInstalled: -32005,
|
|
287
|
+
BundleAlreadyImported: -32010
|
|
268
288
|
};
|
|
269
289
|
InitializeParams = z3.object({
|
|
270
290
|
protocolVersion: z3.number().optional(),
|
|
@@ -388,9 +408,9 @@ var init_connection = __esm({
|
|
|
388
408
|
}
|
|
389
409
|
const id = nanoid();
|
|
390
410
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
391
|
-
const response = new Promise((
|
|
411
|
+
const response = new Promise((resolve4, reject) => {
|
|
392
412
|
this.pending.set(id, {
|
|
393
|
-
resolve: (result) =>
|
|
413
|
+
resolve: (result) => resolve4(result),
|
|
394
414
|
reject
|
|
395
415
|
});
|
|
396
416
|
this.stream.send(message).catch((err) => {
|
|
@@ -614,7 +634,7 @@ function firstLine(text, max) {
|
|
|
614
634
|
}
|
|
615
635
|
return void 0;
|
|
616
636
|
}
|
|
617
|
-
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, Session, STATE_UPDATE_KINDS;
|
|
637
|
+
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, MAX_HISTORY_ENTRIES, COMPACT_EVERY, Session, STATE_UPDATE_KINDS;
|
|
618
638
|
var init_session = __esm({
|
|
619
639
|
"src/core/session.ts"() {
|
|
620
640
|
"use strict";
|
|
@@ -623,6 +643,8 @@ var init_session = __esm({
|
|
|
623
643
|
HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
624
644
|
generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
625
645
|
HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
646
|
+
MAX_HISTORY_ENTRIES = 1e3;
|
|
647
|
+
COMPACT_EVERY = 200;
|
|
626
648
|
Session = class {
|
|
627
649
|
sessionId;
|
|
628
650
|
cwd;
|
|
@@ -642,8 +664,8 @@ var init_session = __esm({
|
|
|
642
664
|
currentModel;
|
|
643
665
|
currentMode;
|
|
644
666
|
updatedAt;
|
|
667
|
+
createdAt;
|
|
645
668
|
clients = /* @__PURE__ */ new Map();
|
|
646
|
-
history = [];
|
|
647
669
|
historyStore;
|
|
648
670
|
promptQueue = [];
|
|
649
671
|
promptInFlight = false;
|
|
@@ -659,6 +681,15 @@ var init_session = __esm({
|
|
|
659
681
|
// True once we've observed our first session/prompt; gates the
|
|
660
682
|
// first-prompt-seeded title so subsequent prompts don't churn it.
|
|
661
683
|
firstPromptSeeded = false;
|
|
684
|
+
// Wall-clock when the active prompt started, undefined when idle.
|
|
685
|
+
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
686
|
+
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
687
|
+
promptStartedAt;
|
|
688
|
+
// Counts appends since the last compaction. When it hits COMPACT_EVERY
|
|
689
|
+
// we ask the history store to trim the file to the most recent
|
|
690
|
+
// MAX_HISTORY_ENTRIES. Keeps file growth bounded without per-append
|
|
691
|
+
// file-size checks.
|
|
692
|
+
appendCount = 0;
|
|
662
693
|
// Permission requests that have been broadcast to one or more
|
|
663
694
|
// clients but have not yet resolved. Replayed to clients that
|
|
664
695
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -673,6 +704,12 @@ var init_session = __esm({
|
|
|
673
704
|
internalPromptCapture;
|
|
674
705
|
idleTimeoutMs;
|
|
675
706
|
idleTimer;
|
|
707
|
+
// Time of the last recordable broadcast (or session creation, if
|
|
708
|
+
// none yet). Drives the inactivity-based idle close; deliberately
|
|
709
|
+
// does NOT include snapshot state pings (model/mode/title/commands)
|
|
710
|
+
// or attach/detach, which would otherwise let passive observers
|
|
711
|
+
// and noisy state churn keep a quiet session alive forever.
|
|
712
|
+
lastRecordedAt;
|
|
676
713
|
spawnReplacementAgent;
|
|
677
714
|
agentChangeHandlers = [];
|
|
678
715
|
// Last available_commands_update we observed from the agent. Stored
|
|
@@ -703,12 +740,15 @@ var init_session = __esm({
|
|
|
703
740
|
}
|
|
704
741
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
705
742
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
this.history = [...init.seedHistory];
|
|
743
|
+
if (init.firstPromptSeeded) {
|
|
744
|
+
this.firstPromptSeeded = true;
|
|
709
745
|
}
|
|
746
|
+
this.historyStore = init.historyStore;
|
|
710
747
|
this.updatedAt = Date.now();
|
|
748
|
+
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
749
|
+
this.lastRecordedAt = this.updatedAt;
|
|
711
750
|
this.wireAgent(this.agent);
|
|
751
|
+
this.scheduleIdleCheck();
|
|
712
752
|
}
|
|
713
753
|
broadcastMergedCommands() {
|
|
714
754
|
const merged = [
|
|
@@ -767,12 +807,21 @@ var init_session = __esm({
|
|
|
767
807
|
get attachedCount() {
|
|
768
808
|
return this.clients.size;
|
|
769
809
|
}
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
|
|
775
|
-
return
|
|
810
|
+
// Wall-clock when the in-flight agent turn began, or undefined when
|
|
811
|
+
// idle. Tracked in-memory by broadcastPromptReceived/broadcastTurnComplete
|
|
812
|
+
// so the daemon can hand a fresh attacher mid-turn the right elapsed
|
|
813
|
+
// time without scanning history.
|
|
814
|
+
get turnStartedAt() {
|
|
815
|
+
return this.promptStartedAt;
|
|
816
|
+
}
|
|
817
|
+
// Read the persisted history from disk. Returns [] if no history
|
|
818
|
+
// file exists (fresh session, never prompted). Used by attach() and
|
|
819
|
+
// the HTTP /history endpoint.
|
|
820
|
+
async getHistorySnapshot() {
|
|
821
|
+
if (!this.historyStore) {
|
|
822
|
+
return [];
|
|
823
|
+
}
|
|
824
|
+
return this.historyStore.load(this.sessionId).catch(() => []);
|
|
776
825
|
}
|
|
777
826
|
// Subscribe to recordable broadcast entries — fires once per entry
|
|
778
827
|
// that lands in history (so snapshot-shaped session_info/model/mode/
|
|
@@ -788,6 +837,10 @@ var init_session = __esm({
|
|
|
788
837
|
}
|
|
789
838
|
};
|
|
790
839
|
}
|
|
840
|
+
// Register a client and (asynchronously) load the replay slice it
|
|
841
|
+
// should receive. Validation errors throw synchronously so callers
|
|
842
|
+
// can rely on either the registration being in effect or having
|
|
843
|
+
// thrown; the disk-load is the only async work.
|
|
791
844
|
attach(client, historyPolicy) {
|
|
792
845
|
if (this.closed) {
|
|
793
846
|
throw withCode(
|
|
@@ -803,14 +856,10 @@ var init_session = __esm({
|
|
|
803
856
|
}
|
|
804
857
|
this.clients.set(client.clientId, client);
|
|
805
858
|
this.updatedAt = Date.now();
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
return [];
|
|
859
|
+
if (historyPolicy === "none" || historyPolicy === "pending_only") {
|
|
860
|
+
return Promise.resolve([]);
|
|
809
861
|
}
|
|
810
|
-
|
|
811
|
-
return [];
|
|
812
|
-
}
|
|
813
|
-
return [...this.history];
|
|
862
|
+
return this.getHistorySnapshot();
|
|
814
863
|
}
|
|
815
864
|
// Dispatch in-flight permission requests to a freshly-attached
|
|
816
865
|
// client. Called by the daemon's WS handler *after* it finishes
|
|
@@ -824,7 +873,6 @@ var init_session = __esm({
|
|
|
824
873
|
detach(clientId) {
|
|
825
874
|
if (this.clients.delete(clientId)) {
|
|
826
875
|
this.updatedAt = Date.now();
|
|
827
|
-
this.maybeStartIdleTimer();
|
|
828
876
|
}
|
|
829
877
|
}
|
|
830
878
|
async prompt(clientId, params) {
|
|
@@ -844,13 +892,19 @@ var init_session = __esm({
|
|
|
844
892
|
this.broadcastPromptReceived(client, params);
|
|
845
893
|
this.maybeSeedTitleFromPrompt(params);
|
|
846
894
|
return this.enqueuePrompt(async () => {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
895
|
+
let response;
|
|
896
|
+
try {
|
|
897
|
+
response = await this.agent.connection.request(
|
|
898
|
+
"session/prompt",
|
|
899
|
+
{
|
|
900
|
+
...params,
|
|
901
|
+
sessionId: this.upstreamSessionId
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
this.broadcastTurnComplete(client.clientId, { stopReason: "error" });
|
|
906
|
+
throw err;
|
|
907
|
+
}
|
|
854
908
|
this.broadcastTurnComplete(client.clientId, response);
|
|
855
909
|
return response;
|
|
856
910
|
});
|
|
@@ -864,6 +918,7 @@ var init_session = __esm({
|
|
|
864
918
|
if (client.clientInfo?.version) {
|
|
865
919
|
sentBy.version = client.clientInfo.version;
|
|
866
920
|
}
|
|
921
|
+
this.promptStartedAt = Date.now();
|
|
867
922
|
this.recordAndBroadcast(
|
|
868
923
|
"session/update",
|
|
869
924
|
{
|
|
@@ -900,6 +955,7 @@ var init_session = __esm({
|
|
|
900
955
|
if (stopReason !== void 0) {
|
|
901
956
|
update.stopReason = stopReason;
|
|
902
957
|
}
|
|
958
|
+
this.promptStartedAt = void 0;
|
|
903
959
|
this.recordAndBroadcast(
|
|
904
960
|
"session/update",
|
|
905
961
|
{
|
|
@@ -938,6 +994,13 @@ var init_session = __esm({
|
|
|
938
994
|
return;
|
|
939
995
|
}
|
|
940
996
|
this.cancelIdleTimer();
|
|
997
|
+
if (opts.regenTitle && this.firstPromptSeeded) {
|
|
998
|
+
const timeoutMs = opts.regenTitleTimeoutMs ?? 5e3;
|
|
999
|
+
await Promise.race([
|
|
1000
|
+
this.runTitleRegen().catch(() => void 0),
|
|
1001
|
+
new Promise((r) => setTimeout(r, timeoutMs).unref?.())
|
|
1002
|
+
]);
|
|
1003
|
+
}
|
|
941
1004
|
await this.agent.kill().catch(() => void 0);
|
|
942
1005
|
this.markClosed({ deleteRecord: opts.deleteRecord ?? false });
|
|
943
1006
|
}
|
|
@@ -986,7 +1049,7 @@ var init_session = __esm({
|
|
|
986
1049
|
}
|
|
987
1050
|
const promptParams = params ?? {};
|
|
988
1051
|
const text = extractPromptText(promptParams.prompt);
|
|
989
|
-
const seed = firstLine(text,
|
|
1052
|
+
const seed = firstLine(text, 200);
|
|
990
1053
|
if (!seed) {
|
|
991
1054
|
return;
|
|
992
1055
|
}
|
|
@@ -1078,6 +1141,12 @@ var init_session = __esm({
|
|
|
1078
1141
|
mergedAvailableCommands() {
|
|
1079
1142
|
return [...hydraCommandsAsAdvertised(), ...this.agentAdvertisedCommands];
|
|
1080
1143
|
}
|
|
1144
|
+
// The agent's own advertised commands (not merged with hydra verbs).
|
|
1145
|
+
// Used by SessionManager to persist into meta.json so cold resurrect
|
|
1146
|
+
// can re-deliver via the attach response _meta.
|
|
1147
|
+
agentOnlyAdvertisedCommands() {
|
|
1148
|
+
return [...this.agentAdvertisedCommands];
|
|
1149
|
+
}
|
|
1081
1150
|
// Pick up an agent-emitted session_info_update and store its title
|
|
1082
1151
|
// as our canonical record. The notification is also forwarded to
|
|
1083
1152
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -1209,7 +1278,7 @@ var init_session = __esm({
|
|
|
1209
1278
|
const spawnAgent = this.spawnReplacementAgent;
|
|
1210
1279
|
return this.enqueuePrompt(async () => {
|
|
1211
1280
|
const oldAgentId = this.agentId;
|
|
1212
|
-
const transcript = this.buildSwitchTranscript(oldAgentId);
|
|
1281
|
+
const transcript = await this.buildSwitchTranscript(oldAgentId);
|
|
1213
1282
|
const fresh = await spawnAgent({
|
|
1214
1283
|
agentId: newAgentId,
|
|
1215
1284
|
cwd: this.cwd,
|
|
@@ -1241,15 +1310,20 @@ var init_session = __esm({
|
|
|
1241
1310
|
return { stopReason: "end_turn" };
|
|
1242
1311
|
});
|
|
1243
1312
|
}
|
|
1244
|
-
// Walk
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1313
|
+
// Walk the persisted history and produce a labeled transcript suitable
|
|
1314
|
+
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
1315
|
+
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
1316
|
+
// switches don't accumulate banners) and other update kinds we don't
|
|
1317
|
+
// think the next agent benefits from re-seeing (plans, thoughts,
|
|
1318
|
+
// mode/model/usage).
|
|
1319
|
+
//
|
|
1320
|
+
// The header text defaults to the agent-swap framing; callers like
|
|
1321
|
+
// seedFromImport pass a custom header when the new agent is taking
|
|
1322
|
+
// over an imported session rather than swapping mid-conversation.
|
|
1323
|
+
async buildSwitchTranscript(prevAgentId, headerOverride) {
|
|
1251
1324
|
const lines = [];
|
|
1252
|
-
|
|
1325
|
+
const history = await this.getHistorySnapshot();
|
|
1326
|
+
for (const note of history) {
|
|
1253
1327
|
if (note.method !== "session/update") {
|
|
1254
1328
|
continue;
|
|
1255
1329
|
}
|
|
@@ -1303,29 +1377,53 @@ var init_session = __esm({
|
|
|
1303
1377
|
if (current) {
|
|
1304
1378
|
coalesced.push(`<${current.speaker}>: ${current.text.trim()}`);
|
|
1305
1379
|
}
|
|
1380
|
+
const intro = headerOverride?.intro ?? `You are taking over this conversation from ${prevAgentId}. Below is the transcript so far.`;
|
|
1381
|
+
const followup = headerOverride?.followup ?? `Each line is prefixed with its speaker. Continue from where ${prevAgentId} left off, responding to the user's most recent message.`;
|
|
1306
1382
|
return [
|
|
1307
|
-
|
|
1308
|
-
|
|
1383
|
+
intro,
|
|
1384
|
+
followup,
|
|
1309
1385
|
"",
|
|
1310
1386
|
"--- begin transcript ---",
|
|
1311
1387
|
...coalesced,
|
|
1312
1388
|
"--- end transcript ---"
|
|
1313
1389
|
].join("\n");
|
|
1314
1390
|
}
|
|
1391
|
+
// Replay the persisted history into a freshly-spawned agent so an
|
|
1392
|
+
// imported session has context. Called by SessionManager.doResurrect
|
|
1393
|
+
// on the first wake-up of a session whose meta.json has an empty
|
|
1394
|
+
// upstreamSessionId (the import marker). Wrapped in enqueuePrompt so
|
|
1395
|
+
// any user prompts arriving mid-seed queue behind it (mirrors the
|
|
1396
|
+
// /hydra switch path so the agent isn't asked to respond to a user
|
|
1397
|
+
// turn before it has absorbed the imported transcript). Best-effort:
|
|
1398
|
+
// if the agent fails to absorb the transcript we still leave the
|
|
1399
|
+
// session usable — the user just continues without context.
|
|
1400
|
+
async seedFromImport() {
|
|
1401
|
+
await this.enqueuePrompt(async () => {
|
|
1402
|
+
const transcript = await this.buildSwitchTranscript(this.agentId, {
|
|
1403
|
+
intro: "You are continuing a conversation that was imported from another hydra. Below is the transcript so far.",
|
|
1404
|
+
followup: "Each line is prefixed with its speaker. Treat this as context for the next user message; do not re-respond to earlier turns."
|
|
1405
|
+
});
|
|
1406
|
+
if (!transcript) {
|
|
1407
|
+
return void 0;
|
|
1408
|
+
}
|
|
1409
|
+
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
1410
|
+
return void 0;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1315
1413
|
// Tell every attached client (a) the agent identity has changed
|
|
1316
|
-
// (session_info_update
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1414
|
+
// (session_info_update carrying agentId inside _meta["hydra-acp"] —
|
|
1415
|
+
// the ACP schema for session_info_update is just title/updatedAt/_meta,
|
|
1416
|
+
// so non-hydra clients harmlessly ignore the extension; hydra-aware
|
|
1417
|
+
// ones read it and relabel) and (b) drop a visible banner into the
|
|
1418
|
+
// transcript so users see the switch rather than just suddenly getting
|
|
1419
|
+
// answers from a different agent. Both updates carry synthetic=true
|
|
1321
1420
|
// so a future /hydra switch's transcript builder filters them out.
|
|
1322
1421
|
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
1323
1422
|
this.recordAndBroadcast("session/update", {
|
|
1324
1423
|
sessionId: this.sessionId,
|
|
1325
1424
|
update: {
|
|
1326
1425
|
sessionUpdate: "session_info_update",
|
|
1327
|
-
agentId: newAgentId
|
|
1328
|
-
_meta: { "hydra-acp": { synthetic: true } }
|
|
1426
|
+
_meta: { "hydra-acp": { synthetic: true, agentId: newAgentId } }
|
|
1329
1427
|
}
|
|
1330
1428
|
});
|
|
1331
1429
|
this.recordAndBroadcast("session/update", {
|
|
@@ -1356,21 +1454,55 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1356
1454
|
handler(opts);
|
|
1357
1455
|
}
|
|
1358
1456
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1457
|
+
// Last meaningful activity timestamp. Bumped only by recordable
|
|
1458
|
+
// broadcasts in recordAndBroadcast — the same signal historyMtimeIso
|
|
1459
|
+
// uses for the picker. Initialized at construction (and seeded from
|
|
1460
|
+
// the newest entry on resurrect) so the inactivity window starts
|
|
1461
|
+
// ticking from a sensible floor when there's no history yet.
|
|
1462
|
+
get lastActivityAt() {
|
|
1463
|
+
return this.lastRecordedAt;
|
|
1464
|
+
}
|
|
1465
|
+
// (Re-)arm the idle timer to fire when the inactivity window
|
|
1466
|
+
// elapses past lastActivityAt. Called once at construction and after
|
|
1467
|
+
// every recorded broadcast. The previous design gated on
|
|
1468
|
+
// clients.size === 0; we drop that gate because extensions
|
|
1469
|
+
// (slack/notifier/approver/browser) hold persistent attaches that
|
|
1470
|
+
// would otherwise keep a quiet session alive forever.
|
|
1471
|
+
scheduleIdleCheck() {
|
|
1472
|
+
if (this.closed || this.idleTimeoutMs <= 0) {
|
|
1361
1473
|
return;
|
|
1362
1474
|
}
|
|
1475
|
+
const dueAt = this.lastActivityAt + this.idleTimeoutMs;
|
|
1476
|
+
this.armIdleTimer(Math.max(0, dueAt - Date.now()));
|
|
1477
|
+
}
|
|
1478
|
+
armIdleTimer(delay) {
|
|
1363
1479
|
if (this.idleTimer) {
|
|
1364
|
-
|
|
1480
|
+
clearTimeout(this.idleTimer);
|
|
1365
1481
|
}
|
|
1366
1482
|
this.idleTimer = setTimeout(() => {
|
|
1367
1483
|
this.idleTimer = void 0;
|
|
1368
|
-
|
|
1369
|
-
},
|
|
1484
|
+
this.checkIdle();
|
|
1485
|
+
}, delay);
|
|
1370
1486
|
if (typeof this.idleTimer.unref === "function") {
|
|
1371
1487
|
this.idleTimer.unref();
|
|
1372
1488
|
}
|
|
1373
1489
|
}
|
|
1490
|
+
checkIdle() {
|
|
1491
|
+
if (this.closed || this.idleTimeoutMs <= 0) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (this.turnStartedAt !== void 0 || this.inFlightPermissions.size > 0) {
|
|
1495
|
+
this.armIdleTimer(this.idleTimeoutMs);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const idle = Date.now() - this.lastActivityAt;
|
|
1499
|
+
if (idle < this.idleTimeoutMs) {
|
|
1500
|
+
this.armIdleTimer(this.idleTimeoutMs - idle);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
|
|
1504
|
+
void this.close(opts).catch(() => void 0);
|
|
1505
|
+
}
|
|
1374
1506
|
cancelIdleTimer() {
|
|
1375
1507
|
if (this.idleTimer) {
|
|
1376
1508
|
clearTimeout(this.idleTimer);
|
|
@@ -1395,17 +1527,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1395
1527
|
params: rewritten,
|
|
1396
1528
|
recordedAt: Date.now()
|
|
1397
1529
|
};
|
|
1398
|
-
this.
|
|
1399
|
-
|
|
1400
|
-
if (this.history.length > 1e3) {
|
|
1401
|
-
this.history = this.history.slice(-500);
|
|
1402
|
-
trimmed = true;
|
|
1403
|
-
}
|
|
1530
|
+
this.lastRecordedAt = entry.recordedAt;
|
|
1531
|
+
this.appendCount += 1;
|
|
1404
1532
|
if (this.historyStore) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1533
|
+
const store = this.historyStore;
|
|
1534
|
+
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
1535
|
+
if (this.appendCount >= COMPACT_EVERY) {
|
|
1536
|
+
this.appendCount = 0;
|
|
1537
|
+
void store.compact(this.sessionId, MAX_HISTORY_ENTRIES).catch(
|
|
1409
1538
|
() => void 0
|
|
1410
1539
|
);
|
|
1411
1540
|
}
|
|
@@ -1416,6 +1545,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1416
1545
|
} catch {
|
|
1417
1546
|
}
|
|
1418
1547
|
}
|
|
1548
|
+
this.scheduleIdleCheck();
|
|
1419
1549
|
}
|
|
1420
1550
|
this.updatedAt = Date.now();
|
|
1421
1551
|
for (const client of this.clients.values()) {
|
|
@@ -1434,7 +1564,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1434
1564
|
);
|
|
1435
1565
|
}
|
|
1436
1566
|
const clientParams = this.rewriteForClient(params);
|
|
1437
|
-
return new Promise((
|
|
1567
|
+
return new Promise((resolve4, reject) => {
|
|
1438
1568
|
let settled = false;
|
|
1439
1569
|
const outbound = [];
|
|
1440
1570
|
const entry = { addClient: sendTo };
|
|
@@ -1469,7 +1599,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1469
1599
|
result
|
|
1470
1600
|
}).catch(() => void 0);
|
|
1471
1601
|
}
|
|
1472
|
-
|
|
1602
|
+
resolve4(result);
|
|
1473
1603
|
});
|
|
1474
1604
|
}).catch((err) => {
|
|
1475
1605
|
settle(() => reject(err));
|
|
@@ -1481,11 +1611,11 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1481
1611
|
});
|
|
1482
1612
|
}
|
|
1483
1613
|
async enqueuePrompt(task) {
|
|
1484
|
-
return new Promise((
|
|
1614
|
+
return new Promise((resolve4, reject) => {
|
|
1485
1615
|
const run = async () => {
|
|
1486
1616
|
try {
|
|
1487
1617
|
const result = await task();
|
|
1488
|
-
|
|
1618
|
+
resolve4(result);
|
|
1489
1619
|
} catch (err) {
|
|
1490
1620
|
reject(err);
|
|
1491
1621
|
}
|
|
@@ -1520,6 +1650,64 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1520
1650
|
}
|
|
1521
1651
|
});
|
|
1522
1652
|
|
|
1653
|
+
// src/tui/history.ts
|
|
1654
|
+
import { promises as fs6 } from "fs";
|
|
1655
|
+
import * as path3 from "path";
|
|
1656
|
+
async function loadHistory(file) {
|
|
1657
|
+
let text;
|
|
1658
|
+
try {
|
|
1659
|
+
text = await fs6.readFile(file, "utf8");
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
if (err.code === "ENOENT") {
|
|
1662
|
+
return [];
|
|
1663
|
+
}
|
|
1664
|
+
throw err;
|
|
1665
|
+
}
|
|
1666
|
+
return parseHistory(text);
|
|
1667
|
+
}
|
|
1668
|
+
function parseHistory(text) {
|
|
1669
|
+
const out = [];
|
|
1670
|
+
for (const rawLine of text.split("\n")) {
|
|
1671
|
+
if (rawLine.length === 0) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const decoded = JSON.parse(rawLine);
|
|
1676
|
+
if (typeof decoded === "string") {
|
|
1677
|
+
out.push(decoded);
|
|
1678
|
+
}
|
|
1679
|
+
} catch {
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return out;
|
|
1683
|
+
}
|
|
1684
|
+
function appendEntry(history, entry) {
|
|
1685
|
+
const trimmed = entry.replace(/\n+$/, "");
|
|
1686
|
+
if (trimmed.length === 0) {
|
|
1687
|
+
return history;
|
|
1688
|
+
}
|
|
1689
|
+
if (history.length > 0 && history[history.length - 1] === trimmed) {
|
|
1690
|
+
return history;
|
|
1691
|
+
}
|
|
1692
|
+
const out = history.concat(trimmed);
|
|
1693
|
+
if (out.length > HISTORY_CAP) {
|
|
1694
|
+
return out.slice(out.length - HISTORY_CAP);
|
|
1695
|
+
}
|
|
1696
|
+
return out;
|
|
1697
|
+
}
|
|
1698
|
+
async function saveHistory(file, history) {
|
|
1699
|
+
await fs6.mkdir(path3.dirname(file), { recursive: true });
|
|
1700
|
+
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1701
|
+
await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1702
|
+
}
|
|
1703
|
+
var HISTORY_CAP;
|
|
1704
|
+
var init_history = __esm({
|
|
1705
|
+
"src/tui/history.ts"() {
|
|
1706
|
+
"use strict";
|
|
1707
|
+
HISTORY_CAP = 500;
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1523
1711
|
// src/acp/ws-stream.ts
|
|
1524
1712
|
function wsToMessageStream(ws) {
|
|
1525
1713
|
const messageHandlers = [];
|
|
@@ -1565,13 +1753,13 @@ function wsToMessageStream(ws) {
|
|
|
1565
1753
|
throw new Error("ws is closed");
|
|
1566
1754
|
}
|
|
1567
1755
|
const text = JSON.stringify(message);
|
|
1568
|
-
await new Promise((
|
|
1756
|
+
await new Promise((resolve4, reject) => {
|
|
1569
1757
|
ws.send(text, (err) => {
|
|
1570
1758
|
if (err) {
|
|
1571
1759
|
reject(err);
|
|
1572
1760
|
return;
|
|
1573
1761
|
}
|
|
1574
|
-
|
|
1762
|
+
resolve4();
|
|
1575
1763
|
});
|
|
1576
1764
|
});
|
|
1577
1765
|
},
|
|
@@ -1650,7 +1838,150 @@ var init_daemon_bootstrap = __esm({
|
|
|
1650
1838
|
}
|
|
1651
1839
|
});
|
|
1652
1840
|
|
|
1841
|
+
// src/cli/session-row.ts
|
|
1842
|
+
function toRow(s, now = Date.now()) {
|
|
1843
|
+
return {
|
|
1844
|
+
session: stripHydraSessionPrefix(s.sessionId),
|
|
1845
|
+
upstream: s.upstreamSessionId ?? "-",
|
|
1846
|
+
status: (s.status ?? "live").toUpperCase(),
|
|
1847
|
+
clients: s.status === "cold" ? "-" : String(s.attachedClients),
|
|
1848
|
+
agent: s.agentId ?? "?",
|
|
1849
|
+
age: formatRelativeAge(s.updatedAt, now),
|
|
1850
|
+
title: s.title ?? "-",
|
|
1851
|
+
cwd: s.cwd
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function computeWidths(rows) {
|
|
1855
|
+
return {
|
|
1856
|
+
session: maxLen(HEADER.session, rows.map((r) => r.session)),
|
|
1857
|
+
upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
1858
|
+
status: maxLen(HEADER.status, rows.map((r) => r.status)),
|
|
1859
|
+
clients: maxLen(HEADER.clients, rows.map((r) => r.clients)),
|
|
1860
|
+
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
1861
|
+
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
1862
|
+
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
function formatRelativeAge(iso, now) {
|
|
1866
|
+
if (!iso) {
|
|
1867
|
+
return "?";
|
|
1868
|
+
}
|
|
1869
|
+
const t = Date.parse(iso);
|
|
1870
|
+
if (Number.isNaN(t)) {
|
|
1871
|
+
return "?";
|
|
1872
|
+
}
|
|
1873
|
+
const diff = Math.max(0, now - t);
|
|
1874
|
+
const sec = Math.floor(diff / 1e3);
|
|
1875
|
+
if (sec < 60) {
|
|
1876
|
+
return "<1m";
|
|
1877
|
+
}
|
|
1878
|
+
const min = Math.floor(sec / 60);
|
|
1879
|
+
if (min < 60) {
|
|
1880
|
+
return `${min}m`;
|
|
1881
|
+
}
|
|
1882
|
+
const hr = Math.floor(min / 60);
|
|
1883
|
+
if (hr < 24) {
|
|
1884
|
+
return `${hr}h`;
|
|
1885
|
+
}
|
|
1886
|
+
const day = Math.floor(hr / 24);
|
|
1887
|
+
if (day < 14) {
|
|
1888
|
+
return `${day}d`;
|
|
1889
|
+
}
|
|
1890
|
+
const week = Math.floor(day / 7);
|
|
1891
|
+
if (week < 9) {
|
|
1892
|
+
return `${week}w`;
|
|
1893
|
+
}
|
|
1894
|
+
const month = Math.floor(day / 30);
|
|
1895
|
+
if (month < 12) {
|
|
1896
|
+
return `${month}mo`;
|
|
1897
|
+
}
|
|
1898
|
+
const year = Math.floor(day / 365);
|
|
1899
|
+
return `${year}y`;
|
|
1900
|
+
}
|
|
1901
|
+
function maxLen(headerCell, values) {
|
|
1902
|
+
let max = headerCell.length;
|
|
1903
|
+
for (const v of values) {
|
|
1904
|
+
if (v.length > max) {
|
|
1905
|
+
max = v.length;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return max;
|
|
1909
|
+
}
|
|
1910
|
+
function formatRow(r, w, maxWidth) {
|
|
1911
|
+
const fixed = [
|
|
1912
|
+
r.session.padEnd(w.session),
|
|
1913
|
+
r.upstream.padEnd(w.upstream),
|
|
1914
|
+
r.status.padEnd(w.status),
|
|
1915
|
+
r.clients.padStart(w.clients),
|
|
1916
|
+
r.agent.padEnd(w.agent),
|
|
1917
|
+
r.age.padStart(w.age)
|
|
1918
|
+
].join(SEP);
|
|
1919
|
+
if (maxWidth === void 0) {
|
|
1920
|
+
return [fixed, r.title.padEnd(w.title), r.cwd].join(SEP);
|
|
1921
|
+
}
|
|
1922
|
+
const titleCap = Math.min(w.title, TITLE_MAX_WIDTH);
|
|
1923
|
+
const budget = maxWidth - fixed.length - SEP.length;
|
|
1924
|
+
if (budget <= 0) {
|
|
1925
|
+
return fixed.slice(0, maxWidth);
|
|
1926
|
+
}
|
|
1927
|
+
const titleNatural = Math.min(r.title.length, titleCap);
|
|
1928
|
+
let titleAlloc = titleNatural + SEP.length + MIN_CWD <= budget ? titleCap : Math.max(0, budget - SEP.length - MIN_CWD);
|
|
1929
|
+
titleAlloc = Math.min(titleAlloc, Math.max(0, budget - SEP.length - 1));
|
|
1930
|
+
const titleCell = truncateRight(r.title, titleAlloc).padEnd(titleAlloc);
|
|
1931
|
+
const cwdBudget = Math.max(0, budget - titleAlloc - SEP.length);
|
|
1932
|
+
const cwdCell = truncateMiddle(r.cwd, cwdBudget);
|
|
1933
|
+
return [fixed, titleCell, cwdCell].join(SEP);
|
|
1934
|
+
}
|
|
1935
|
+
function truncateRight(s, max) {
|
|
1936
|
+
if (max <= 0) {
|
|
1937
|
+
return "";
|
|
1938
|
+
}
|
|
1939
|
+
if (s.length <= max) {
|
|
1940
|
+
return s;
|
|
1941
|
+
}
|
|
1942
|
+
if (max === 1) {
|
|
1943
|
+
return "\u2026";
|
|
1944
|
+
}
|
|
1945
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
1946
|
+
}
|
|
1947
|
+
function truncateMiddle(s, max) {
|
|
1948
|
+
if (max <= 0) {
|
|
1949
|
+
return "";
|
|
1950
|
+
}
|
|
1951
|
+
if (s.length <= max) {
|
|
1952
|
+
return s;
|
|
1953
|
+
}
|
|
1954
|
+
if (max === 1) {
|
|
1955
|
+
return "\u2026";
|
|
1956
|
+
}
|
|
1957
|
+
const head = Math.ceil((max - 1) / 2);
|
|
1958
|
+
const tail = max - 1 - head;
|
|
1959
|
+
return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
|
|
1960
|
+
}
|
|
1961
|
+
var HEADER, SEP, MIN_CWD, TITLE_MAX_WIDTH;
|
|
1962
|
+
var init_session_row = __esm({
|
|
1963
|
+
"src/cli/session-row.ts"() {
|
|
1964
|
+
"use strict";
|
|
1965
|
+
init_session();
|
|
1966
|
+
HEADER = {
|
|
1967
|
+
session: "SESSION",
|
|
1968
|
+
upstream: "UPSTREAM",
|
|
1969
|
+
status: "STATUS",
|
|
1970
|
+
clients: "CLIENTS",
|
|
1971
|
+
agent: "AGENT",
|
|
1972
|
+
age: "AGE",
|
|
1973
|
+
title: "TITLE",
|
|
1974
|
+
cwd: "CWD"
|
|
1975
|
+
};
|
|
1976
|
+
SEP = " ";
|
|
1977
|
+
MIN_CWD = 8;
|
|
1978
|
+
TITLE_MAX_WIDTH = 40;
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1653
1982
|
// src/cli/commands/sessions.ts
|
|
1983
|
+
import * as fs11 from "fs/promises";
|
|
1984
|
+
import * as path5 from "path";
|
|
1654
1985
|
async function runSessionsList(opts = {}) {
|
|
1655
1986
|
const config = await loadConfig();
|
|
1656
1987
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -1685,44 +2016,13 @@ async function runSessionsList(opts = {}) {
|
|
|
1685
2016
|
visible = [...sorted.slice(0, liveCount), ...coldSlice];
|
|
1686
2017
|
truncated = hiddenCold;
|
|
1687
2018
|
}
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
agent: s.agentId ?? "?",
|
|
1694
|
-
title: s.title ?? "-",
|
|
1695
|
-
cwd: s.cwd
|
|
1696
|
-
}));
|
|
1697
|
-
const header = {
|
|
1698
|
-
session: "SESSION",
|
|
1699
|
-
upstream: "UPSTREAM",
|
|
1700
|
-
status: "STATUS",
|
|
1701
|
-
clients: "CLIENTS",
|
|
1702
|
-
agent: "AGENT",
|
|
1703
|
-
title: "TITLE",
|
|
1704
|
-
cwd: "CWD"
|
|
1705
|
-
};
|
|
1706
|
-
const widths = {
|
|
1707
|
-
session: maxLen(header.session, rows.map((r) => r.session)),
|
|
1708
|
-
upstream: maxLen(header.upstream, rows.map((r) => r.upstream)),
|
|
1709
|
-
status: maxLen(header.status, rows.map((r) => r.status)),
|
|
1710
|
-
clients: maxLen(header.clients, rows.map((r) => r.clients)),
|
|
1711
|
-
agent: maxLen(header.agent, rows.map((r) => r.agent)),
|
|
1712
|
-
title: maxLen(header.title, rows.map((r) => r.title))
|
|
1713
|
-
};
|
|
1714
|
-
const formatRow2 = (r) => [
|
|
1715
|
-
r.session.padEnd(widths.session),
|
|
1716
|
-
r.upstream.padEnd(widths.upstream),
|
|
1717
|
-
r.status.padEnd(widths.status),
|
|
1718
|
-
r.clients.padStart(widths.clients),
|
|
1719
|
-
r.agent.padEnd(widths.agent),
|
|
1720
|
-
r.title.padEnd(widths.title),
|
|
1721
|
-
r.cwd
|
|
1722
|
-
].join(" ");
|
|
1723
|
-
process.stdout.write(formatRow2(header) + "\n");
|
|
2019
|
+
const now = Date.now();
|
|
2020
|
+
const rows = visible.map((s) => toRow(s, now));
|
|
2021
|
+
const widths = computeWidths(rows);
|
|
2022
|
+
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2023
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth) + "\n");
|
|
1724
2024
|
for (const r of rows) {
|
|
1725
|
-
process.stdout.write(
|
|
2025
|
+
process.stdout.write(formatRow(r, widths, maxWidth) + "\n");
|
|
1726
2026
|
}
|
|
1727
2027
|
if (truncated > 0) {
|
|
1728
2028
|
process.stdout.write(
|
|
@@ -1732,15 +2032,6 @@ async function runSessionsList(opts = {}) {
|
|
|
1732
2032
|
);
|
|
1733
2033
|
}
|
|
1734
2034
|
}
|
|
1735
|
-
function maxLen(headerCell, values) {
|
|
1736
|
-
let max = headerCell.length;
|
|
1737
|
-
for (const v of values) {
|
|
1738
|
-
if (v.length > max) {
|
|
1739
|
-
max = v.length;
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return max;
|
|
1743
|
-
}
|
|
1744
2035
|
async function runSessionsKill(id) {
|
|
1745
2036
|
if (!id) {
|
|
1746
2037
|
process.stderr.write("Usage: hydra-acp sessions kill <session-id>\n");
|
|
@@ -1748,6 +2039,25 @@ async function runSessionsKill(id) {
|
|
|
1748
2039
|
}
|
|
1749
2040
|
const config = await loadConfig();
|
|
1750
2041
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2042
|
+
const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
|
|
2043
|
+
method: "POST",
|
|
2044
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2045
|
+
});
|
|
2046
|
+
if (!response.ok && response.status !== 204) {
|
|
2047
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
2048
|
+
`);
|
|
2049
|
+
process.exit(1);
|
|
2050
|
+
}
|
|
2051
|
+
process.stdout.write(`Killed ${id}
|
|
2052
|
+
`);
|
|
2053
|
+
}
|
|
2054
|
+
async function runSessionsRm(id) {
|
|
2055
|
+
if (!id) {
|
|
2056
|
+
process.stderr.write("Usage: hydra-acp sessions rm <session-id>\n");
|
|
2057
|
+
process.exit(2);
|
|
2058
|
+
}
|
|
2059
|
+
const config = await loadConfig();
|
|
2060
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
1751
2061
|
const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
|
|
1752
2062
|
method: "DELETE",
|
|
1753
2063
|
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
@@ -1757,9 +2067,114 @@ async function runSessionsKill(id) {
|
|
|
1757
2067
|
`);
|
|
1758
2068
|
process.exit(1);
|
|
1759
2069
|
}
|
|
1760
|
-
process.stdout.write(`
|
|
2070
|
+
process.stdout.write(`Removed ${id}
|
|
1761
2071
|
`);
|
|
1762
2072
|
}
|
|
2073
|
+
async function runSessionsExport(id, outPath) {
|
|
2074
|
+
if (!id) {
|
|
2075
|
+
process.stderr.write(
|
|
2076
|
+
"Usage: hydra-acp sessions export <session-id> [--out <file>]\n"
|
|
2077
|
+
);
|
|
2078
|
+
process.exit(2);
|
|
2079
|
+
}
|
|
2080
|
+
const config = await loadConfig();
|
|
2081
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2082
|
+
const response = await fetch(
|
|
2083
|
+
`${baseUrl}/v1/sessions/${encodeURIComponent(id)}/export`,
|
|
2084
|
+
{
|
|
2085
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2086
|
+
}
|
|
2087
|
+
);
|
|
2088
|
+
if (!response.ok) {
|
|
2089
|
+
const text = await response.text().catch(() => "");
|
|
2090
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
2091
|
+
`);
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
}
|
|
2094
|
+
const body = await response.text();
|
|
2095
|
+
if (!outPath) {
|
|
2096
|
+
process.stdout.write(body);
|
|
2097
|
+
if (!body.endsWith("\n")) {
|
|
2098
|
+
process.stdout.write("\n");
|
|
2099
|
+
}
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
2103
|
+
await fs11.mkdir(path5.dirname(path5.resolve(resolved)), { recursive: true });
|
|
2104
|
+
await fs11.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2105
|
+
process.stdout.write(`Wrote ${resolved}
|
|
2106
|
+
`);
|
|
2107
|
+
}
|
|
2108
|
+
async function runSessionsImport(file, opts = {}) {
|
|
2109
|
+
if (!file) {
|
|
2110
|
+
process.stderr.write(
|
|
2111
|
+
"Usage: hydra-acp sessions import <file>|- [--replace]\n"
|
|
2112
|
+
);
|
|
2113
|
+
process.exit(2);
|
|
2114
|
+
}
|
|
2115
|
+
let body;
|
|
2116
|
+
if (file === "-") {
|
|
2117
|
+
body = await readStdin();
|
|
2118
|
+
} else {
|
|
2119
|
+
body = await fs11.readFile(file, "utf8");
|
|
2120
|
+
}
|
|
2121
|
+
let bundle;
|
|
2122
|
+
try {
|
|
2123
|
+
bundle = JSON.parse(body);
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
process.stderr.write(`Failed to parse bundle: ${err.message}
|
|
2126
|
+
`);
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
2129
|
+
const config = await loadConfig();
|
|
2130
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2131
|
+
const response = await fetch(`${baseUrl}/v1/sessions/import`, {
|
|
2132
|
+
method: "POST",
|
|
2133
|
+
headers: {
|
|
2134
|
+
"Content-Type": "application/json",
|
|
2135
|
+
Authorization: `Bearer ${config.daemon.authToken}`
|
|
2136
|
+
},
|
|
2137
|
+
body: JSON.stringify({ bundle, replace: opts.replace === true })
|
|
2138
|
+
});
|
|
2139
|
+
if (response.status === 409) {
|
|
2140
|
+
const detail = await response.json().catch(() => ({}));
|
|
2141
|
+
process.stderr.write(
|
|
2142
|
+
`Bundle already imported as ${detail.existingSessionId ?? "unknown"}. Use --replace to overwrite.
|
|
2143
|
+
`
|
|
2144
|
+
);
|
|
2145
|
+
process.exit(1);
|
|
2146
|
+
}
|
|
2147
|
+
if (!response.ok) {
|
|
2148
|
+
const text = await response.text().catch(() => "");
|
|
2149
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
2150
|
+
`);
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
const result = await response.json();
|
|
2154
|
+
process.stdout.write(
|
|
2155
|
+
result.replaced ? `Replaced ${result.sessionId} (from ${result.importedFromSessionId})
|
|
2156
|
+
` : `Imported as ${result.sessionId} (from ${result.importedFromSessionId})
|
|
2157
|
+
`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
async function readStdin() {
|
|
2161
|
+
const chunks = [];
|
|
2162
|
+
for await (const chunk of process.stdin) {
|
|
2163
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
2164
|
+
}
|
|
2165
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2166
|
+
}
|
|
2167
|
+
function deriveFilenameFrom(response, id) {
|
|
2168
|
+
const cd = response.headers.get("content-disposition");
|
|
2169
|
+
if (cd) {
|
|
2170
|
+
const match = cd.match(/filename="([^"]+)"/);
|
|
2171
|
+
if (match) {
|
|
2172
|
+
return match[1];
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2176
|
+
return `hydra-${id}-${stamp}.hydra`;
|
|
2177
|
+
}
|
|
1763
2178
|
function httpBase(host, port, tls) {
|
|
1764
2179
|
const protocol = tls ? "https" : "http";
|
|
1765
2180
|
return `${protocol}://${host}:${port}`;
|
|
@@ -1768,7 +2183,7 @@ var init_sessions = __esm({
|
|
|
1768
2183
|
"src/cli/commands/sessions.ts"() {
|
|
1769
2184
|
"use strict";
|
|
1770
2185
|
init_config();
|
|
1771
|
-
|
|
2186
|
+
init_session_row();
|
|
1772
2187
|
}
|
|
1773
2188
|
});
|
|
1774
2189
|
|
|
@@ -1779,11 +2194,11 @@ function isResponse(msg) {
|
|
|
1779
2194
|
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
1780
2195
|
}
|
|
1781
2196
|
async function openWs(url, subprotocols) {
|
|
1782
|
-
return new Promise((
|
|
2197
|
+
return new Promise((resolve4, reject) => {
|
|
1783
2198
|
const ws = new WebSocket(url, subprotocols);
|
|
1784
2199
|
const onOpen = () => {
|
|
1785
2200
|
ws.off("error", onError);
|
|
1786
|
-
|
|
2201
|
+
resolve4(wsToMessageStream(ws));
|
|
1787
2202
|
};
|
|
1788
2203
|
const onError = (err) => {
|
|
1789
2204
|
ws.off("open", onOpen);
|
|
@@ -1854,8 +2269,8 @@ var init_resilient_ws = __esm({
|
|
|
1854
2269
|
throw new Error("resilient ws stream not connected");
|
|
1855
2270
|
}
|
|
1856
2271
|
const id = message.id;
|
|
1857
|
-
const promise = new Promise((
|
|
1858
|
-
this.pendingRequests.set(id, { resolve:
|
|
2272
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2273
|
+
this.pendingRequests.set(id, { resolve: resolve4, reject });
|
|
1859
2274
|
});
|
|
1860
2275
|
try {
|
|
1861
2276
|
await this.current.send(message);
|
|
@@ -1883,8 +2298,8 @@ var init_resilient_ws = __esm({
|
|
|
1883
2298
|
this.bindStream(stream);
|
|
1884
2299
|
const wasFirst = this.firstConnect;
|
|
1885
2300
|
this.firstConnect = false;
|
|
1886
|
-
this.connectGate = new Promise((
|
|
1887
|
-
this.releaseConnectGate =
|
|
2301
|
+
this.connectGate = new Promise((resolve4) => {
|
|
2302
|
+
this.releaseConnectGate = resolve4;
|
|
1888
2303
|
});
|
|
1889
2304
|
try {
|
|
1890
2305
|
if (this.opts.onConnect) {
|
|
@@ -2007,64 +2422,6 @@ var init_resilient_ws = __esm({
|
|
|
2007
2422
|
}
|
|
2008
2423
|
});
|
|
2009
2424
|
|
|
2010
|
-
// src/tui/history.ts
|
|
2011
|
-
import { promises as fs9 } from "fs";
|
|
2012
|
-
import * as path4 from "path";
|
|
2013
|
-
async function loadHistory(file) {
|
|
2014
|
-
let text;
|
|
2015
|
-
try {
|
|
2016
|
-
text = await fs9.readFile(file, "utf8");
|
|
2017
|
-
} catch (err) {
|
|
2018
|
-
if (err.code === "ENOENT") {
|
|
2019
|
-
return [];
|
|
2020
|
-
}
|
|
2021
|
-
throw err;
|
|
2022
|
-
}
|
|
2023
|
-
return parseHistory(text);
|
|
2024
|
-
}
|
|
2025
|
-
function parseHistory(text) {
|
|
2026
|
-
const out = [];
|
|
2027
|
-
for (const rawLine of text.split("\n")) {
|
|
2028
|
-
if (rawLine.length === 0) {
|
|
2029
|
-
continue;
|
|
2030
|
-
}
|
|
2031
|
-
try {
|
|
2032
|
-
const decoded = JSON.parse(rawLine);
|
|
2033
|
-
if (typeof decoded === "string") {
|
|
2034
|
-
out.push(decoded);
|
|
2035
|
-
}
|
|
2036
|
-
} catch {
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
return out;
|
|
2040
|
-
}
|
|
2041
|
-
function appendEntry(history, entry) {
|
|
2042
|
-
const trimmed = entry.replace(/\n+$/, "");
|
|
2043
|
-
if (trimmed.length === 0) {
|
|
2044
|
-
return history;
|
|
2045
|
-
}
|
|
2046
|
-
if (history.length > 0 && history[history.length - 1] === trimmed) {
|
|
2047
|
-
return history;
|
|
2048
|
-
}
|
|
2049
|
-
const out = history.concat(trimmed);
|
|
2050
|
-
if (out.length > HISTORY_CAP) {
|
|
2051
|
-
return out.slice(out.length - HISTORY_CAP);
|
|
2052
|
-
}
|
|
2053
|
-
return out;
|
|
2054
|
-
}
|
|
2055
|
-
async function saveHistory(file, history) {
|
|
2056
|
-
await fs9.mkdir(path4.dirname(file), { recursive: true });
|
|
2057
|
-
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2058
|
-
await fs9.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2059
|
-
}
|
|
2060
|
-
var HISTORY_CAP;
|
|
2061
|
-
var init_history = __esm({
|
|
2062
|
-
"src/tui/history.ts"() {
|
|
2063
|
-
"use strict";
|
|
2064
|
-
HISTORY_CAP = 500;
|
|
2065
|
-
}
|
|
2066
|
-
});
|
|
2067
|
-
|
|
2068
2425
|
// src/tui/discovery.ts
|
|
2069
2426
|
async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
2070
2427
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -2096,6 +2453,26 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
2096
2453
|
title: s.title
|
|
2097
2454
|
}));
|
|
2098
2455
|
}
|
|
2456
|
+
async function killSession(config, id, fetchImpl = fetch) {
|
|
2457
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2458
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}/kill`, {
|
|
2459
|
+
method: "POST",
|
|
2460
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2461
|
+
});
|
|
2462
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
2463
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
async function deleteSession(config, id, fetchImpl = fetch) {
|
|
2467
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2468
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
2469
|
+
method: "DELETE",
|
|
2470
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2471
|
+
});
|
|
2472
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
2473
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2099
2476
|
function pickMostRecent(sessions, cwd) {
|
|
2100
2477
|
const matching = sessions.filter((s) => s.cwd === cwd);
|
|
2101
2478
|
if (matching.length === 0) {
|
|
@@ -2123,147 +2500,406 @@ async function pickSession(term, opts) {
|
|
|
2123
2500
|
if (opts.sessions.length === 0) {
|
|
2124
2501
|
return { kind: "new" };
|
|
2125
2502
|
}
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
|
|
2503
|
+
const sortSessions = (sessions) => {
|
|
2504
|
+
const score = (s) => {
|
|
2505
|
+
if (s.status !== "live") {
|
|
2506
|
+
return 0;
|
|
2507
|
+
}
|
|
2508
|
+
return s.cwd === opts.cwd ? 2 : 1;
|
|
2509
|
+
};
|
|
2510
|
+
return [...sessions].sort((a, b) => {
|
|
2511
|
+
const tier = score(b) - score(a);
|
|
2512
|
+
if (tier !== 0) {
|
|
2513
|
+
return tier;
|
|
2514
|
+
}
|
|
2515
|
+
return b.updatedAt.localeCompare(a.updatedAt);
|
|
2516
|
+
});
|
|
2517
|
+
};
|
|
2518
|
+
let visible = sortSessions(opts.sessions);
|
|
2519
|
+
let rows = visible.map((s) => toRow(s, Date.now()));
|
|
2520
|
+
let widths = computeWidths(rows);
|
|
2521
|
+
let total = 1 + visible.length;
|
|
2522
|
+
let selectedIdx = 0;
|
|
2523
|
+
let scrollOffset = 0;
|
|
2524
|
+
let mode = "normal";
|
|
2525
|
+
let pendingAction = null;
|
|
2526
|
+
let transientStatus = null;
|
|
2527
|
+
let termHeight = readTermHeight(term);
|
|
2528
|
+
let termWidth = readTermWidth(term);
|
|
2529
|
+
let viewportSize = 0;
|
|
2530
|
+
let newSessionLabel = "";
|
|
2531
|
+
let headerLine = "";
|
|
2532
|
+
let sessionLines = [];
|
|
2533
|
+
let startRow = 1;
|
|
2534
|
+
const computeLayout = () => {
|
|
2535
|
+
termHeight = readTermHeight(term);
|
|
2536
|
+
termWidth = readTermWidth(term);
|
|
2537
|
+
const maxViewportRows = Math.max(3, termHeight - 6);
|
|
2538
|
+
viewportSize = Math.min(visible.length, maxViewportRows);
|
|
2539
|
+
const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
|
|
2540
|
+
newSessionLabel = formatNewSessionLabel(opts.cwd, rowMaxWidth);
|
|
2541
|
+
headerLine = formatRow(HEADER, widths, rowMaxWidth);
|
|
2542
|
+
sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth));
|
|
2543
|
+
};
|
|
2544
|
+
const rebuildRows = () => {
|
|
2545
|
+
rows = visible.map((s) => toRow(s, Date.now()));
|
|
2546
|
+
widths = computeWidths(rows);
|
|
2547
|
+
total = 1 + visible.length;
|
|
2548
|
+
computeLayout();
|
|
2549
|
+
};
|
|
2550
|
+
const adjustScroll = () => {
|
|
2551
|
+
if (selectedIdx === 0) {
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
const sessionIdx = selectedIdx - 1;
|
|
2555
|
+
if (sessionIdx < scrollOffset) {
|
|
2556
|
+
scrollOffset = sessionIdx;
|
|
2557
|
+
} else if (sessionIdx >= scrollOffset + viewportSize) {
|
|
2558
|
+
scrollOffset = sessionIdx - viewportSize + 1;
|
|
2559
|
+
} else if (scrollOffset + viewportSize > visible.length) {
|
|
2560
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
2129
2561
|
}
|
|
2130
|
-
return s.cwd === opts.cwd ? 2 : 1;
|
|
2131
2562
|
};
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2563
|
+
const paintNewItem = () => {
|
|
2564
|
+
if (selectedIdx === 0) {
|
|
2565
|
+
term.brightWhite.bgBlue.noFormat(`\u276F ${newSessionLabel}`);
|
|
2566
|
+
} else {
|
|
2567
|
+
term.noFormat(` ${newSessionLabel}`);
|
|
2136
2568
|
}
|
|
2137
|
-
|
|
2569
|
+
};
|
|
2570
|
+
const paintSessionRow = (sessionIdx) => {
|
|
2571
|
+
const label = sessionLines[sessionIdx] ?? "";
|
|
2572
|
+
if (selectedIdx === sessionIdx + 1) {
|
|
2573
|
+
term.brightWhite.bgBlue.noFormat(`\u276F ${label}`);
|
|
2574
|
+
} else {
|
|
2575
|
+
term.noFormat(` ${label}`);
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
const formatIndicator = () => {
|
|
2579
|
+
const above = scrollOffset;
|
|
2580
|
+
const below = Math.max(0, visible.length - scrollOffset - viewportSize);
|
|
2581
|
+
if (above === 0 && below === 0) {
|
|
2582
|
+
return "";
|
|
2583
|
+
}
|
|
2584
|
+
const parts = [];
|
|
2585
|
+
if (above > 0) {
|
|
2586
|
+
parts.push(`\u2191 ${above} above`);
|
|
2587
|
+
}
|
|
2588
|
+
if (below > 0) {
|
|
2589
|
+
parts.push(`\u2193 ${below} below`);
|
|
2590
|
+
}
|
|
2591
|
+
return ` ${parts.join(" \xB7 ")}`;
|
|
2592
|
+
};
|
|
2593
|
+
const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
|
|
2594
|
+
const paintIndicator = () => {
|
|
2595
|
+
term.moveTo(1, indicatorRow()).eraseLineAfter();
|
|
2596
|
+
if (mode === "confirm-kill" && pendingAction) {
|
|
2597
|
+
term.brightYellow.noFormat(` kill ${shortId2(pendingAction.sessionId)}? [y/N]`);
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
if (mode === "confirm-delete" && pendingAction) {
|
|
2601
|
+
term.brightRed.noFormat(` delete ${shortId2(pendingAction.sessionId)}? [y/N]`);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
if (mode === "busy" && pendingAction) {
|
|
2605
|
+
term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (transientStatus !== null) {
|
|
2609
|
+
term.dim.noFormat(` ${transientStatus}`);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
term.dim.noFormat(formatIndicator());
|
|
2613
|
+
};
|
|
2614
|
+
const indicatorRow = () => startRow + 3 + viewportSize;
|
|
2615
|
+
const sessionRow = (sessionIdx) => startRow + 3 + (sessionIdx - scrollOffset);
|
|
2616
|
+
const renderFromScratch = () => {
|
|
2617
|
+
computeLayout();
|
|
2618
|
+
adjustScroll();
|
|
2619
|
+
startRow = 1;
|
|
2620
|
+
term.moveTo(1, 1).eraseDisplayBelow();
|
|
2621
|
+
paintNewItem();
|
|
2622
|
+
term("\n\n");
|
|
2623
|
+
term.dim.noFormat(` ${headerLine}`)("\n");
|
|
2624
|
+
for (let v = 0; v < viewportSize; v++) {
|
|
2625
|
+
paintSessionRow(scrollOffset + v);
|
|
2626
|
+
term("\n");
|
|
2627
|
+
}
|
|
2628
|
+
paintIndicator();
|
|
2629
|
+
term("\n");
|
|
2630
|
+
};
|
|
2631
|
+
const repaintNewItem = () => {
|
|
2632
|
+
term.moveTo(1, startRow).eraseLineAfter();
|
|
2633
|
+
paintNewItem();
|
|
2634
|
+
};
|
|
2635
|
+
const repaintSessionRow = (sessionIdx) => {
|
|
2636
|
+
if (sessionIdx < scrollOffset || sessionIdx >= scrollOffset + viewportSize) {
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
term.moveTo(1, sessionRow(sessionIdx)).eraseLineAfter();
|
|
2640
|
+
paintSessionRow(sessionIdx);
|
|
2641
|
+
};
|
|
2642
|
+
const repaintViewport = () => {
|
|
2643
|
+
for (let v = 0; v < viewportSize; v++) {
|
|
2644
|
+
const row = startRow + 3 + v;
|
|
2645
|
+
term.moveTo(1, row).eraseLineAfter();
|
|
2646
|
+
const sessionIdx = scrollOffset + v;
|
|
2647
|
+
if (sessionIdx < visible.length) {
|
|
2648
|
+
paintSessionRow(sessionIdx);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
paintIndicator();
|
|
2652
|
+
};
|
|
2653
|
+
renderFromScratch();
|
|
2654
|
+
term.hideCursor();
|
|
2655
|
+
return await new Promise((resolve4) => {
|
|
2656
|
+
let resolved = false;
|
|
2657
|
+
const onResize = () => {
|
|
2658
|
+
if (resolved) {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
renderFromScratch();
|
|
2662
|
+
};
|
|
2663
|
+
const cleanup = () => {
|
|
2664
|
+
if (resolved) {
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
resolved = true;
|
|
2668
|
+
term.off("key", onKey);
|
|
2669
|
+
term.off("resize", onResize);
|
|
2670
|
+
term.grabInput(false);
|
|
2671
|
+
term.hideCursor(false);
|
|
2672
|
+
term.moveTo(1, indicatorRow() + 1);
|
|
2673
|
+
term("\n");
|
|
2674
|
+
};
|
|
2675
|
+
const refresh = async (preferredId) => {
|
|
2676
|
+
try {
|
|
2677
|
+
const next = await listSessions(opts.config);
|
|
2678
|
+
visible = sortSessions(next);
|
|
2679
|
+
rebuildRows();
|
|
2680
|
+
if (preferredId !== void 0) {
|
|
2681
|
+
const idx = visible.findIndex((s) => s.sessionId === preferredId);
|
|
2682
|
+
if (idx >= 0) {
|
|
2683
|
+
selectedIdx = idx + 1;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
if (selectedIdx > total - 1) {
|
|
2687
|
+
selectedIdx = Math.max(0, total - 1);
|
|
2688
|
+
}
|
|
2689
|
+
if (scrollOffset + viewportSize > visible.length) {
|
|
2690
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
2691
|
+
}
|
|
2692
|
+
adjustScroll();
|
|
2693
|
+
renderFromScratch();
|
|
2694
|
+
} catch (err) {
|
|
2695
|
+
transientStatus = `refresh failed: ${err.message}`;
|
|
2696
|
+
renderFromScratch();
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
const performAction = async (kind) => {
|
|
2700
|
+
if (!pendingAction) {
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
const target = pendingAction;
|
|
2704
|
+
mode = "busy";
|
|
2705
|
+
paintIndicator();
|
|
2706
|
+
try {
|
|
2707
|
+
if (kind === "kill") {
|
|
2708
|
+
await killSession(opts.config, target.sessionId);
|
|
2709
|
+
} else {
|
|
2710
|
+
await deleteSession(opts.config, target.sessionId);
|
|
2711
|
+
}
|
|
2712
|
+
mode = "normal";
|
|
2713
|
+
pendingAction = null;
|
|
2714
|
+
await refresh(kind === "kill" ? target.sessionId : void 0);
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
mode = "normal";
|
|
2717
|
+
pendingAction = null;
|
|
2718
|
+
transientStatus = `${kind} failed: ${err.message}`;
|
|
2719
|
+
paintIndicator();
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
const move = (delta) => {
|
|
2723
|
+
const next = Math.min(total - 1, Math.max(0, selectedIdx + delta));
|
|
2724
|
+
if (next === selectedIdx) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
const old = selectedIdx;
|
|
2728
|
+
const oldScroll = scrollOffset;
|
|
2729
|
+
selectedIdx = next;
|
|
2730
|
+
adjustScroll();
|
|
2731
|
+
if (scrollOffset !== oldScroll) {
|
|
2732
|
+
repaintViewport();
|
|
2733
|
+
if (old === 0 || selectedIdx === 0) {
|
|
2734
|
+
repaintNewItem();
|
|
2735
|
+
}
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (old === 0) {
|
|
2739
|
+
repaintNewItem();
|
|
2740
|
+
} else {
|
|
2741
|
+
repaintSessionRow(old - 1);
|
|
2742
|
+
}
|
|
2743
|
+
if (selectedIdx === 0) {
|
|
2744
|
+
repaintNewItem();
|
|
2745
|
+
} else {
|
|
2746
|
+
repaintSessionRow(selectedIdx - 1);
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
const clearTransient = () => {
|
|
2750
|
+
if (transientStatus === null) {
|
|
2751
|
+
return false;
|
|
2752
|
+
}
|
|
2753
|
+
transientStatus = null;
|
|
2754
|
+
paintIndicator();
|
|
2755
|
+
return true;
|
|
2756
|
+
};
|
|
2757
|
+
const onKey = (name, _matches, data) => {
|
|
2758
|
+
if (mode === "busy") {
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (mode === "confirm-kill" || mode === "confirm-delete") {
|
|
2762
|
+
if (data?.isCharacter && (name === "y" || name === "Y")) {
|
|
2763
|
+
const kind = mode === "confirm-kill" ? "kill" : "delete";
|
|
2764
|
+
void performAction(kind);
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
if (name === "ESCAPE" || name === "CTRL_C" || name === "ENTER" || name === "KP_ENTER" || data?.isCharacter && (name === "n" || name === "N")) {
|
|
2768
|
+
mode = "normal";
|
|
2769
|
+
pendingAction = null;
|
|
2770
|
+
paintIndicator();
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
clearTransient();
|
|
2776
|
+
if (data?.isCharacter) {
|
|
2777
|
+
if (name === "r" || name === "R") {
|
|
2778
|
+
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
2779
|
+
void refresh(currentId);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
if ((name === "k" || name === "K") && selectedIdx > 0) {
|
|
2783
|
+
const session = visible[selectedIdx - 1];
|
|
2784
|
+
if (!session) {
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
pendingAction = {
|
|
2788
|
+
sessionId: session.sessionId,
|
|
2789
|
+
cwd: session.cwd,
|
|
2790
|
+
status: session.status
|
|
2791
|
+
};
|
|
2792
|
+
mode = "confirm-kill";
|
|
2793
|
+
paintIndicator();
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
if ((name === "d" || name === "D") && selectedIdx > 0) {
|
|
2797
|
+
const session = visible[selectedIdx - 1];
|
|
2798
|
+
if (!session) {
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
if (session.status === "live") {
|
|
2802
|
+
transientStatus = "session is live \u2014 press k to kill it first";
|
|
2803
|
+
paintIndicator();
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
pendingAction = {
|
|
2807
|
+
sessionId: session.sessionId,
|
|
2808
|
+
cwd: session.cwd,
|
|
2809
|
+
status: session.status
|
|
2810
|
+
};
|
|
2811
|
+
mode = "confirm-delete";
|
|
2812
|
+
paintIndicator();
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
switch (name) {
|
|
2818
|
+
case "UP":
|
|
2819
|
+
case "SHIFT_TAB":
|
|
2820
|
+
move(-1);
|
|
2821
|
+
return;
|
|
2822
|
+
case "DOWN":
|
|
2823
|
+
case "TAB":
|
|
2824
|
+
move(1);
|
|
2825
|
+
return;
|
|
2826
|
+
case "PAGE_UP":
|
|
2827
|
+
move(-viewportSize);
|
|
2828
|
+
return;
|
|
2829
|
+
case "PAGE_DOWN":
|
|
2830
|
+
move(viewportSize);
|
|
2831
|
+
return;
|
|
2832
|
+
case "HOME":
|
|
2833
|
+
move(-total);
|
|
2834
|
+
return;
|
|
2835
|
+
case "END":
|
|
2836
|
+
move(total);
|
|
2837
|
+
return;
|
|
2838
|
+
case "ENTER":
|
|
2839
|
+
case "KP_ENTER": {
|
|
2840
|
+
cleanup();
|
|
2841
|
+
if (selectedIdx === 0) {
|
|
2842
|
+
resolve4({ kind: "new" });
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
const session = visible[selectedIdx - 1];
|
|
2846
|
+
if (!session) {
|
|
2847
|
+
resolve4({ kind: "abort" });
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const result = {
|
|
2851
|
+
kind: "attach",
|
|
2852
|
+
sessionId: session.sessionId
|
|
2853
|
+
};
|
|
2854
|
+
if (session.agentId !== void 0) {
|
|
2855
|
+
result.agentId = session.agentId;
|
|
2856
|
+
}
|
|
2857
|
+
resolve4(result);
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
case "ESCAPE":
|
|
2861
|
+
case "CTRL_C":
|
|
2862
|
+
cleanup();
|
|
2863
|
+
resolve4({ kind: "abort" });
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
term.grabInput({});
|
|
2868
|
+
term.on("key", onKey);
|
|
2869
|
+
term.on("resize", onResize);
|
|
2138
2870
|
});
|
|
2139
|
-
const liveCount = sorted.filter((s) => s.status !== "cold").length;
|
|
2140
|
-
const coldSlice = sorted.slice(liveCount, liveCount + opts.coldLimit);
|
|
2141
|
-
const hiddenCold = sorted.length - liveCount - coldSlice.length;
|
|
2142
|
-
const visible = [...sorted.slice(0, liveCount), ...coldSlice];
|
|
2143
|
-
const rows = visible.map(toRow);
|
|
2144
|
-
const widths = computeWidths(rows);
|
|
2145
|
-
const newSessionLabel = `+ New session in ${opts.cwd}`;
|
|
2146
|
-
const items = [newSessionLabel, ...rows.map((r) => formatRow(r, widths))];
|
|
2147
|
-
term("\n");
|
|
2148
|
-
term.bold("Select a session")("\n");
|
|
2149
|
-
if (hiddenCold > 0) {
|
|
2150
|
-
term.dim(`(${hiddenCold} older cold session${hiddenCold === 1 ? "" : "s"} hidden; use \`hydra-acp sessions --all\` to view)
|
|
2151
|
-
`);
|
|
2152
|
-
}
|
|
2153
|
-
term.dim(formatRow(HEADER, widths))("\n");
|
|
2154
|
-
const onCtrlC = (name) => {
|
|
2155
|
-
if (name === "CTRL_C") {
|
|
2156
|
-
term.grabInput(false);
|
|
2157
|
-
term("\n");
|
|
2158
|
-
process.exit(130);
|
|
2159
|
-
}
|
|
2160
|
-
};
|
|
2161
|
-
term.on("key", onCtrlC);
|
|
2162
|
-
let response;
|
|
2163
|
-
try {
|
|
2164
|
-
response = await term.singleColumnMenu(items, {
|
|
2165
|
-
cancelable: true,
|
|
2166
|
-
exitOnUnexpectedKey: false,
|
|
2167
|
-
selectedIndex: 0,
|
|
2168
|
-
style: term.brightWhite,
|
|
2169
|
-
selectedStyle: term.brightWhite.bgBlue,
|
|
2170
|
-
keyBindings: {
|
|
2171
|
-
ENTER: "submit",
|
|
2172
|
-
KP_ENTER: "submit",
|
|
2173
|
-
UP: "previous",
|
|
2174
|
-
DOWN: "next",
|
|
2175
|
-
TAB: "next",
|
|
2176
|
-
SHIFT_TAB: "previous",
|
|
2177
|
-
HOME: "first",
|
|
2178
|
-
END: "last",
|
|
2179
|
-
ESCAPE: "cancel",
|
|
2180
|
-
CTRL_C: "cancel"
|
|
2181
|
-
}
|
|
2182
|
-
}).promise;
|
|
2183
|
-
} finally {
|
|
2184
|
-
term.off("key", onCtrlC);
|
|
2185
|
-
}
|
|
2186
|
-
term("\n");
|
|
2187
|
-
if (response.canceled || response.selectedIndex === void 0) {
|
|
2188
|
-
return { kind: "abort" };
|
|
2189
|
-
}
|
|
2190
|
-
if (response.selectedIndex === 0) {
|
|
2191
|
-
return { kind: "new" };
|
|
2192
|
-
}
|
|
2193
|
-
const session = visible[response.selectedIndex - 1];
|
|
2194
|
-
if (!session) {
|
|
2195
|
-
return { kind: "abort" };
|
|
2196
|
-
}
|
|
2197
|
-
const result = {
|
|
2198
|
-
kind: "attach",
|
|
2199
|
-
sessionId: session.sessionId
|
|
2200
|
-
};
|
|
2201
|
-
if (session.agentId !== void 0) {
|
|
2202
|
-
result.agentId = session.agentId;
|
|
2203
|
-
}
|
|
2204
|
-
return result;
|
|
2205
|
-
}
|
|
2206
|
-
function toRow(s) {
|
|
2207
|
-
return {
|
|
2208
|
-
session: stripHydraSessionPrefix(s.sessionId),
|
|
2209
|
-
upstream: s.upstreamSessionId ?? "-",
|
|
2210
|
-
status: s.status.toUpperCase(),
|
|
2211
|
-
clients: s.status === "cold" ? "-" : String(s.attachedClients),
|
|
2212
|
-
agent: s.agentId ?? "?",
|
|
2213
|
-
title: s.title ?? "-",
|
|
2214
|
-
cwd: s.cwd
|
|
2215
|
-
};
|
|
2216
2871
|
}
|
|
2217
|
-
function
|
|
2218
|
-
return
|
|
2219
|
-
session: maxLen4(HEADER.session, rows.map((r) => r.session)),
|
|
2220
|
-
upstream: maxLen4(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
2221
|
-
status: maxLen4(HEADER.status, rows.map((r) => r.status)),
|
|
2222
|
-
clients: maxLen4(HEADER.clients, rows.map((r) => r.clients)),
|
|
2223
|
-
agent: maxLen4(HEADER.agent, rows.map((r) => r.agent)),
|
|
2224
|
-
title: maxLen4(HEADER.title, rows.map((r) => r.title))
|
|
2225
|
-
};
|
|
2872
|
+
function readTermHeight(term) {
|
|
2873
|
+
return term.height ?? 24;
|
|
2226
2874
|
}
|
|
2227
|
-
function
|
|
2228
|
-
|
|
2229
|
-
for (const v of values) {
|
|
2230
|
-
if (v.length > max) {
|
|
2231
|
-
max = v.length;
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
return max;
|
|
2875
|
+
function readTermWidth(term) {
|
|
2876
|
+
return term.width ?? 80;
|
|
2235
2877
|
}
|
|
2236
|
-
function
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
r.status.padEnd(w.status),
|
|
2241
|
-
r.clients.padStart(w.clients),
|
|
2242
|
-
r.agent.padEnd(w.agent),
|
|
2243
|
-
r.title.padEnd(w.title),
|
|
2244
|
-
r.cwd
|
|
2245
|
-
].join(" ");
|
|
2878
|
+
function formatNewSessionLabel(cwd, maxWidth) {
|
|
2879
|
+
const prefix = "+ New session in ";
|
|
2880
|
+
const budget = Math.max(1, maxWidth - prefix.length);
|
|
2881
|
+
return prefix + truncateMiddle(cwd, budget);
|
|
2246
2882
|
}
|
|
2247
|
-
var
|
|
2883
|
+
var ROW_PREFIX_WIDTH;
|
|
2248
2884
|
var init_picker = __esm({
|
|
2249
2885
|
"src/tui/picker.ts"() {
|
|
2250
2886
|
"use strict";
|
|
2887
|
+
init_session_row();
|
|
2251
2888
|
init_session();
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
upstream: "UPSTREAM",
|
|
2255
|
-
status: "STATUS",
|
|
2256
|
-
clients: "CLIENTS",
|
|
2257
|
-
agent: "AGENT",
|
|
2258
|
-
title: "TITLE",
|
|
2259
|
-
cwd: "CWD"
|
|
2260
|
-
};
|
|
2889
|
+
init_discovery();
|
|
2890
|
+
ROW_PREFIX_WIDTH = 2;
|
|
2261
2891
|
}
|
|
2262
2892
|
});
|
|
2263
2893
|
|
|
2264
2894
|
// src/tui/screen.ts
|
|
2265
2895
|
import stringWidth from "string-width";
|
|
2266
2896
|
import wrapAnsi from "wrap-ansi";
|
|
2897
|
+
function formattedLineSig(zone, width, line) {
|
|
2898
|
+
if (!line) {
|
|
2899
|
+
return `${zone}|${width}|empty`;
|
|
2900
|
+
}
|
|
2901
|
+
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}`;
|
|
2902
|
+
}
|
|
2267
2903
|
function computePromptVisualRows(buffer, room) {
|
|
2268
2904
|
const rows = [];
|
|
2269
2905
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -2348,16 +2984,16 @@ function writeStyled(term, text, style) {
|
|
|
2348
2984
|
term.bold.red.noFormat(text);
|
|
2349
2985
|
return;
|
|
2350
2986
|
case "tool-status-pending":
|
|
2351
|
-
term.dim.
|
|
2987
|
+
term.dim.noFormat(text);
|
|
2352
2988
|
return;
|
|
2353
2989
|
case "tool-status-running":
|
|
2354
|
-
term.
|
|
2990
|
+
term.brightYellow.noFormat(text);
|
|
2355
2991
|
return;
|
|
2356
2992
|
case "tool-status-cancelled":
|
|
2357
2993
|
term.dim.noFormat(text);
|
|
2358
2994
|
return;
|
|
2359
2995
|
case "plan":
|
|
2360
|
-
term.
|
|
2996
|
+
term.brightYellow.noFormat(text);
|
|
2361
2997
|
return;
|
|
2362
2998
|
case "plan-done":
|
|
2363
2999
|
term.green.noFormat(text);
|
|
@@ -2406,6 +3042,12 @@ function wrap(text, width) {
|
|
|
2406
3042
|
if (text.length === 0) {
|
|
2407
3043
|
return [""];
|
|
2408
3044
|
}
|
|
3045
|
+
if (!NON_ASCII.test(text)) {
|
|
3046
|
+
return wrapAscii(text, width);
|
|
3047
|
+
}
|
|
3048
|
+
return wrapVisible(text, width);
|
|
3049
|
+
}
|
|
3050
|
+
function wrapAscii(text, width) {
|
|
2409
3051
|
const out = [];
|
|
2410
3052
|
let remaining = text;
|
|
2411
3053
|
while (remaining.length > width) {
|
|
@@ -2428,14 +3070,78 @@ function wrap(text, width) {
|
|
|
2428
3070
|
out.push(remaining);
|
|
2429
3071
|
return out;
|
|
2430
3072
|
}
|
|
3073
|
+
function wrapVisible(text, width) {
|
|
3074
|
+
const out = [];
|
|
3075
|
+
const graphemes = [];
|
|
3076
|
+
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3077
|
+
graphemes.push({ seg: segment, w: stringWidth(segment) });
|
|
3078
|
+
}
|
|
3079
|
+
let i = 0;
|
|
3080
|
+
while (i < graphemes.length) {
|
|
3081
|
+
let chunk = "";
|
|
3082
|
+
let chunkW = 0;
|
|
3083
|
+
let lastSpaceI = -1;
|
|
3084
|
+
let chunkAtLastSpace = "";
|
|
3085
|
+
while (i < graphemes.length) {
|
|
3086
|
+
const g = graphemes[i];
|
|
3087
|
+
if (chunkW + g.w > width) {
|
|
3088
|
+
break;
|
|
3089
|
+
}
|
|
3090
|
+
if (g.seg === " ") {
|
|
3091
|
+
lastSpaceI = i;
|
|
3092
|
+
chunkAtLastSpace = chunk;
|
|
3093
|
+
}
|
|
3094
|
+
chunk += g.seg;
|
|
3095
|
+
chunkW += g.w;
|
|
3096
|
+
i += 1;
|
|
3097
|
+
}
|
|
3098
|
+
if (i >= graphemes.length) {
|
|
3099
|
+
out.push(chunk);
|
|
3100
|
+
break;
|
|
3101
|
+
}
|
|
3102
|
+
if (lastSpaceI >= 0) {
|
|
3103
|
+
out.push(chunkAtLastSpace);
|
|
3104
|
+
i = lastSpaceI + 1;
|
|
3105
|
+
} else if (chunk.length === 0) {
|
|
3106
|
+
out.push(graphemes[i].seg);
|
|
3107
|
+
i += 1;
|
|
3108
|
+
} else {
|
|
3109
|
+
out.push(chunk);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
return out;
|
|
3113
|
+
}
|
|
2431
3114
|
function truncate(text, max) {
|
|
2432
|
-
if (
|
|
3115
|
+
if (max <= 0) {
|
|
3116
|
+
return "";
|
|
3117
|
+
}
|
|
3118
|
+
if (text.length <= max && !NON_ASCII.test(text)) {
|
|
3119
|
+
return text;
|
|
3120
|
+
}
|
|
3121
|
+
const visible = stringWidth(text);
|
|
3122
|
+
if (visible <= max) {
|
|
2433
3123
|
return text;
|
|
2434
3124
|
}
|
|
2435
3125
|
if (max <= 1) {
|
|
2436
|
-
return text
|
|
3126
|
+
return takeByWidth(text, max);
|
|
2437
3127
|
}
|
|
2438
|
-
return text
|
|
3128
|
+
return takeByWidth(text, max - 1) + "\u2026";
|
|
3129
|
+
}
|
|
3130
|
+
function takeByWidth(text, budget) {
|
|
3131
|
+
if (budget <= 0) {
|
|
3132
|
+
return "";
|
|
3133
|
+
}
|
|
3134
|
+
let out = "";
|
|
3135
|
+
let used = 0;
|
|
3136
|
+
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3137
|
+
const w = stringWidth(segment);
|
|
3138
|
+
if (used + w > budget) {
|
|
3139
|
+
break;
|
|
3140
|
+
}
|
|
3141
|
+
out += segment;
|
|
3142
|
+
used += w;
|
|
3143
|
+
}
|
|
3144
|
+
return out;
|
|
2439
3145
|
}
|
|
2440
3146
|
function firstLine2(text) {
|
|
2441
3147
|
const idx = text.indexOf("\n");
|
|
@@ -2542,13 +3248,15 @@ function mapKeyName(name) {
|
|
|
2542
3248
|
return "ctrl-u";
|
|
2543
3249
|
case "CTRL_W":
|
|
2544
3250
|
return "ctrl-w";
|
|
3251
|
+
case "CTRL_Y":
|
|
3252
|
+
return "ctrl-y";
|
|
2545
3253
|
case "ESCAPE":
|
|
2546
3254
|
return "escape";
|
|
2547
3255
|
default:
|
|
2548
3256
|
return null;
|
|
2549
3257
|
}
|
|
2550
3258
|
}
|
|
2551
|
-
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;
|
|
3259
|
+
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, shortId;
|
|
2552
3260
|
var init_screen = __esm({
|
|
2553
3261
|
"src/tui/screen.ts"() {
|
|
2554
3262
|
"use strict";
|
|
@@ -2562,6 +3270,7 @@ var init_screen = __esm({
|
|
|
2562
3270
|
MAX_COMPLETION_ROWS = 6;
|
|
2563
3271
|
CONFIRM_PROMPT_ROWS = 2;
|
|
2564
3272
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
3273
|
+
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
2565
3274
|
Screen = class {
|
|
2566
3275
|
term;
|
|
2567
3276
|
dispatcher;
|
|
@@ -2582,6 +3291,27 @@ var init_screen = __esm({
|
|
|
2582
3291
|
lastRepaintAt = 0;
|
|
2583
3292
|
throttledRepaintTimer = null;
|
|
2584
3293
|
contentRepaintThrottleMs;
|
|
3294
|
+
maxScrollbackLines;
|
|
3295
|
+
// Wrap memoization: each FormattedLine that lands in this.lines gets a
|
|
3296
|
+
// monotonic id assigned via trackLine(); wrapCache holds the pre-wrapped
|
|
3297
|
+
// FormattedLine[] for that id at wrapCacheWidth. Width changes flush the
|
|
3298
|
+
// whole cache; in-place body mutation (streaming) and splices invalidate
|
|
3299
|
+
// affected ids. Result: steady-state repaints only wrap newly-appended
|
|
3300
|
+
// lines, not the entire history.
|
|
3301
|
+
nextLineId = 1;
|
|
3302
|
+
lineIds = /* @__PURE__ */ new WeakMap();
|
|
3303
|
+
wrapCache = /* @__PURE__ */ new Map();
|
|
3304
|
+
wrapCacheWidth = 0;
|
|
3305
|
+
// Per-row signature of what was painted to each terminal row on the
|
|
3306
|
+
// previous repaint. drawX methods funnel through paintRow(), which
|
|
3307
|
+
// skips the moveTo+eraseLineAfter+write sequence when the new
|
|
3308
|
+
// signature matches the previous frame. Eliminates flicker during
|
|
3309
|
+
// the 1Hz busy-tick: only rows whose content actually changed
|
|
3310
|
+
// (banner elapsed, tools-block summary) get re-emitted instead of
|
|
3311
|
+
// every visible row. Cleared on dimension change.
|
|
3312
|
+
lastFrameRows = /* @__PURE__ */ new Map();
|
|
3313
|
+
lastFrameW = 0;
|
|
3314
|
+
lastFrameH = 0;
|
|
2585
3315
|
permissionPrompt = null;
|
|
2586
3316
|
confirmPrompt = null;
|
|
2587
3317
|
completions = [];
|
|
@@ -2596,6 +3326,7 @@ var init_screen = __esm({
|
|
|
2596
3326
|
queued: 0
|
|
2597
3327
|
};
|
|
2598
3328
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
3329
|
+
lastWindowTitle = null;
|
|
2599
3330
|
resizeHandler;
|
|
2600
3331
|
keyHandler;
|
|
2601
3332
|
mouseHandler;
|
|
@@ -2616,6 +3347,7 @@ var init_screen = __esm({
|
|
|
2616
3347
|
this.dispatcher = opts.dispatcher;
|
|
2617
3348
|
this.onKey = opts.onKey;
|
|
2618
3349
|
this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
|
|
3350
|
+
this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
|
|
2619
3351
|
this.resizeHandler = () => this.repaint();
|
|
2620
3352
|
this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
|
|
2621
3353
|
this.mouseHandler = (name) => this.handleMouse(name);
|
|
@@ -2627,6 +3359,11 @@ var init_screen = __esm({
|
|
|
2627
3359
|
}
|
|
2628
3360
|
this.started = true;
|
|
2629
3361
|
this.term.fullscreen(true);
|
|
3362
|
+
this.lastFrameRows.clear();
|
|
3363
|
+
this.lastFrameW = 0;
|
|
3364
|
+
this.lastFrameH = 0;
|
|
3365
|
+
this.lastWindowTitle = null;
|
|
3366
|
+
process.stdout.write("\x1B[?7l");
|
|
2630
3367
|
this.term.grabInput({ mouse: "button" });
|
|
2631
3368
|
this.term.hideCursor(false);
|
|
2632
3369
|
this.term.on("key", this.keyHandler);
|
|
@@ -2646,6 +3383,7 @@ var init_screen = __esm({
|
|
|
2646
3383
|
this.term.off("resize", this.resizeHandler);
|
|
2647
3384
|
this.term.grabInput(false);
|
|
2648
3385
|
this.term.hideCursor(false);
|
|
3386
|
+
process.stdout.write("\x1B[?7h");
|
|
2649
3387
|
this.term.fullscreen(false);
|
|
2650
3388
|
this.term("\n");
|
|
2651
3389
|
}
|
|
@@ -2716,13 +3454,17 @@ var init_screen = __esm({
|
|
|
2716
3454
|
}
|
|
2717
3455
|
this.streamingActive = false;
|
|
2718
3456
|
this.lines.push(...lines);
|
|
3457
|
+
this.trackLines(lines);
|
|
2719
3458
|
this.adjustScrollForLineChange(lines.length);
|
|
3459
|
+
this.trimScrollback();
|
|
2720
3460
|
this.scheduleRepaint();
|
|
2721
3461
|
}
|
|
2722
3462
|
appendLine(line) {
|
|
2723
3463
|
this.streamingActive = false;
|
|
2724
3464
|
this.lines.push(line);
|
|
3465
|
+
this.trackLine(line);
|
|
2725
3466
|
this.adjustScrollForLineChange(1);
|
|
3467
|
+
this.trimScrollback();
|
|
2726
3468
|
this.scheduleRepaint();
|
|
2727
3469
|
}
|
|
2728
3470
|
// When scrolled away from the bottom, shift scrollOffset to keep the
|
|
@@ -2734,6 +3476,40 @@ var init_screen = __esm({
|
|
|
2734
3476
|
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
2735
3477
|
}
|
|
2736
3478
|
}
|
|
3479
|
+
trackLine(line) {
|
|
3480
|
+
this.lineIds.set(line, this.nextLineId++);
|
|
3481
|
+
}
|
|
3482
|
+
trackLines(lines) {
|
|
3483
|
+
for (const line of lines) {
|
|
3484
|
+
this.trackLine(line);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
forgetLine(line) {
|
|
3488
|
+
const id = this.lineIds.get(line);
|
|
3489
|
+
if (id !== void 0) {
|
|
3490
|
+
this.wrapCache.delete(id);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
// Drop oldest lines once scrollback exceeds the configured cap. Removes
|
|
3494
|
+
// their wrap-cache entries and shifts keyedBlocks indices in sync;
|
|
3495
|
+
// blocks whose lines fully fell off the head are dropped (a later
|
|
3496
|
+
// upsert for that key will start a fresh block at the bottom).
|
|
3497
|
+
trimScrollback() {
|
|
3498
|
+
const overflow = this.lines.length - this.maxScrollbackLines;
|
|
3499
|
+
if (overflow <= 0) {
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
const removed = this.lines.splice(0, overflow);
|
|
3503
|
+
for (const line of removed) {
|
|
3504
|
+
this.forgetLine(line);
|
|
3505
|
+
}
|
|
3506
|
+
for (const [key, range] of [...this.keyedBlocks.entries()]) {
|
|
3507
|
+
range.start -= overflow;
|
|
3508
|
+
if (range.start < 0) {
|
|
3509
|
+
this.keyedBlocks.delete(key);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
2737
3513
|
// Append-or-replace a single-line block keyed by `key`. Thin wrapper
|
|
2738
3514
|
// around upsertLines for the common one-row case (tool calls).
|
|
2739
3515
|
upsertLine(key, line) {
|
|
@@ -2755,7 +3531,15 @@ var init_screen = __esm({
|
|
|
2755
3531
|
touchesEnd = oldEnd >= this.lines.length;
|
|
2756
3532
|
const delta = newLines.length - existing.count;
|
|
2757
3533
|
scrollDelta = delta;
|
|
2758
|
-
this.lines.splice(
|
|
3534
|
+
const removed = this.lines.splice(
|
|
3535
|
+
existing.start,
|
|
3536
|
+
existing.count,
|
|
3537
|
+
...newLines
|
|
3538
|
+
);
|
|
3539
|
+
for (const line of removed) {
|
|
3540
|
+
this.forgetLine(line);
|
|
3541
|
+
}
|
|
3542
|
+
this.trackLines(newLines);
|
|
2759
3543
|
existing.count = newLines.length;
|
|
2760
3544
|
if (delta !== 0) {
|
|
2761
3545
|
for (const [k, range] of this.keyedBlocks) {
|
|
@@ -2772,11 +3556,13 @@ var init_screen = __esm({
|
|
|
2772
3556
|
count: newLines.length
|
|
2773
3557
|
});
|
|
2774
3558
|
this.lines.push(...newLines);
|
|
3559
|
+
this.trackLines(newLines);
|
|
2775
3560
|
}
|
|
2776
3561
|
if (touchesEnd) {
|
|
2777
3562
|
this.streamingActive = false;
|
|
2778
3563
|
}
|
|
2779
3564
|
this.adjustScrollForLineChange(scrollDelta);
|
|
3565
|
+
this.trimScrollback();
|
|
2780
3566
|
this.scheduleRepaint();
|
|
2781
3567
|
}
|
|
2782
3568
|
// Append fragments of a streaming message (e.g. agent_message_chunk). The
|
|
@@ -2793,6 +3579,7 @@ var init_screen = __esm({
|
|
|
2793
3579
|
if (this.streamingActive && this.lines.length > 0) {
|
|
2794
3580
|
const last = this.lines[this.lines.length - 1];
|
|
2795
3581
|
if (last) {
|
|
3582
|
+
this.forgetLine(last);
|
|
2796
3583
|
last.body += first ?? "";
|
|
2797
3584
|
}
|
|
2798
3585
|
} else {
|
|
@@ -2800,7 +3587,9 @@ var init_screen = __esm({
|
|
|
2800
3587
|
const last = this.lines[this.lines.length - 1];
|
|
2801
3588
|
const isBlank = last && last.body === "" && (!last.prefix || last.prefix === "");
|
|
2802
3589
|
if (!isBlank) {
|
|
2803
|
-
|
|
3590
|
+
const sep = { body: "" };
|
|
3591
|
+
this.lines.push(sep);
|
|
3592
|
+
this.trackLine(sep);
|
|
2804
3593
|
added += 1;
|
|
2805
3594
|
}
|
|
2806
3595
|
}
|
|
@@ -2813,25 +3602,48 @@ var init_screen = __esm({
|
|
|
2813
3602
|
initial.prefixStyle = prefixStyle;
|
|
2814
3603
|
}
|
|
2815
3604
|
this.lines.push(initial);
|
|
3605
|
+
this.trackLine(initial);
|
|
2816
3606
|
added += 1;
|
|
2817
3607
|
}
|
|
2818
3608
|
const continuationPrefix = " ".repeat(prefix.length);
|
|
2819
3609
|
for (const piece of rest) {
|
|
2820
|
-
|
|
3610
|
+
const cont = {
|
|
2821
3611
|
prefix: continuationPrefix,
|
|
2822
3612
|
body: piece,
|
|
2823
3613
|
bodyStyle
|
|
2824
|
-
}
|
|
3614
|
+
};
|
|
3615
|
+
this.lines.push(cont);
|
|
3616
|
+
this.trackLine(cont);
|
|
2825
3617
|
added += 1;
|
|
2826
3618
|
}
|
|
2827
3619
|
this.streamingActive = true;
|
|
2828
3620
|
this.adjustScrollForLineChange(added);
|
|
3621
|
+
this.trimScrollback();
|
|
2829
3622
|
this.scheduleRepaint();
|
|
2830
3623
|
}
|
|
2831
3624
|
setHeader(header) {
|
|
2832
3625
|
this.header = { ...this.header, ...header };
|
|
3626
|
+
this.syncWindowTitle();
|
|
2833
3627
|
this.repaint();
|
|
2834
3628
|
}
|
|
3629
|
+
// Push the current session title (or short session id, as fallback) to
|
|
3630
|
+
// the host terminal via OSC 2. Supported by xterm/foot/iTerm2/Alacritty/
|
|
3631
|
+
// most modern emulators; ignored harmlessly elsewhere.
|
|
3632
|
+
syncWindowTitle() {
|
|
3633
|
+
const title = this.header.title?.trim();
|
|
3634
|
+
const fallback = shortId(this.header.sessionId) || "hydra";
|
|
3635
|
+
const raw = title && title.length > 0 ? title : fallback;
|
|
3636
|
+
const clean = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
3637
|
+
if (clean === this.lastWindowTitle) {
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
this.lastWindowTitle = clean;
|
|
3641
|
+
process.stdout.write(`\x1B]2;${clean}\x1B\\`);
|
|
3642
|
+
}
|
|
3643
|
+
clearWindowTitle() {
|
|
3644
|
+
this.lastWindowTitle = null;
|
|
3645
|
+
process.stdout.write("\x1B]2;\x1B\\");
|
|
3646
|
+
}
|
|
2835
3647
|
setBanner(banner) {
|
|
2836
3648
|
this.banner = { ...this.banner, ...banner };
|
|
2837
3649
|
this.drawBanner();
|
|
@@ -2840,6 +3652,8 @@ var init_screen = __esm({
|
|
|
2840
3652
|
clearScrollback() {
|
|
2841
3653
|
this.lines = [];
|
|
2842
3654
|
this.keyedBlocks.clear();
|
|
3655
|
+
this.wrapCache.clear();
|
|
3656
|
+
this.wrapCacheWidth = 0;
|
|
2843
3657
|
this.streamingActive = false;
|
|
2844
3658
|
this.scrollOffset = 0;
|
|
2845
3659
|
this.repaint();
|
|
@@ -2862,7 +3676,10 @@ var init_screen = __esm({
|
|
|
2862
3676
|
return;
|
|
2863
3677
|
}
|
|
2864
3678
|
const touchesEnd = existing.start + existing.count >= this.lines.length;
|
|
2865
|
-
this.lines.splice(existing.start, existing.count);
|
|
3679
|
+
const removed = this.lines.splice(existing.start, existing.count);
|
|
3680
|
+
for (const line of removed) {
|
|
3681
|
+
this.forgetLine(line);
|
|
3682
|
+
}
|
|
2866
3683
|
this.keyedBlocks.delete(key);
|
|
2867
3684
|
for (const [, range] of this.keyedBlocks) {
|
|
2868
3685
|
if (range.start > existing.start) {
|
|
@@ -2878,6 +3695,22 @@ var init_screen = __esm({
|
|
|
2878
3695
|
redraw() {
|
|
2879
3696
|
this.repaint();
|
|
2880
3697
|
}
|
|
3698
|
+
// Forced clean-slate repaint. Drops the per-row signature cache, the
|
|
3699
|
+
// window-title cache, the wrap cache, and clears the terminal before
|
|
3700
|
+
// painting. Wired to ^L so the user can recover when something has
|
|
3701
|
+
// corrupted the visible state and the per-row sig check otherwise
|
|
3702
|
+
// short-circuits the re-emit.
|
|
3703
|
+
fullRedraw() {
|
|
3704
|
+
this.lastFrameRows.clear();
|
|
3705
|
+
this.lastFrameW = 0;
|
|
3706
|
+
this.lastFrameH = 0;
|
|
3707
|
+
this.lastWindowTitle = null;
|
|
3708
|
+
this.wrapCache.clear();
|
|
3709
|
+
this.wrapCacheWidth = 0;
|
|
3710
|
+
process.stdout.write("\x1B[?7l");
|
|
3711
|
+
this.term.clear();
|
|
3712
|
+
this.repaint();
|
|
3713
|
+
}
|
|
2881
3714
|
// While paused, append* methods buffer state but don't repaint. Calls are
|
|
2882
3715
|
// counter-based so they nest safely. Resume triggers exactly one repaint
|
|
2883
3716
|
// if any was requested while paused.
|
|
@@ -2936,9 +3769,12 @@ var init_screen = __esm({
|
|
|
2936
3769
|
if (last && last.body === "" && (last.prefix === void 0 || last.prefix === "")) {
|
|
2937
3770
|
return;
|
|
2938
3771
|
}
|
|
2939
|
-
|
|
3772
|
+
const sep = { body: "" };
|
|
3773
|
+
this.lines.push(sep);
|
|
3774
|
+
this.trackLine(sep);
|
|
2940
3775
|
this.streamingActive = false;
|
|
2941
3776
|
this.adjustScrollForLineChange(1);
|
|
3777
|
+
this.trimScrollback();
|
|
2942
3778
|
this.scheduleRepaint();
|
|
2943
3779
|
}
|
|
2944
3780
|
// The dispatcher is the source of truth for prompt state. If the prompt
|
|
@@ -3010,9 +3846,11 @@ var init_screen = __esm({
|
|
|
3010
3846
|
return Math.max(0, bottom - top + 1);
|
|
3011
3847
|
}
|
|
3012
3848
|
maxScrollOffset() {
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3015
|
-
|
|
3849
|
+
const { rows } = this.wrapTail(
|
|
3850
|
+
this.term.width,
|
|
3851
|
+
Number.POSITIVE_INFINITY
|
|
3852
|
+
);
|
|
3853
|
+
return Math.max(0, rows.length - this.scrollbackVisibleRows());
|
|
3016
3854
|
}
|
|
3017
3855
|
// Used by content mutators to coalesce rapid updates. Repaints fire
|
|
3018
3856
|
// at most once per contentRepaintThrottleMs; if a paint happened
|
|
@@ -3045,6 +3883,22 @@ var init_screen = __esm({
|
|
|
3045
3883
|
this.repaint();
|
|
3046
3884
|
}, this.contentRepaintThrottleMs - elapsed);
|
|
3047
3885
|
}
|
|
3886
|
+
// Funnel for every row that any drawX method renders. Skips emitting
|
|
3887
|
+
// moveTo+eraseLineAfter+paint when the row's signature matches the
|
|
3888
|
+
// previous frame's. The signature must capture everything that affects
|
|
3889
|
+
// visible output for that row (width, FormattedLine fields, banner
|
|
3890
|
+
// state, etc.) so identical sigs guarantee identical bytes.
|
|
3891
|
+
paintRow(row, signature, paint) {
|
|
3892
|
+
if (row < 1 || row > this.term.height) {
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
if (this.lastFrameRows.get(row) === signature) {
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
this.lastFrameRows.set(row, signature);
|
|
3899
|
+
this.term.moveTo(1, row).eraseLineAfter();
|
|
3900
|
+
paint();
|
|
3901
|
+
}
|
|
3048
3902
|
repaint() {
|
|
3049
3903
|
if (this.repaintPaused > 0) {
|
|
3050
3904
|
this.repaintPending = true;
|
|
@@ -3060,6 +3914,11 @@ var init_screen = __esm({
|
|
|
3060
3914
|
if (w < 20 || h < 8) {
|
|
3061
3915
|
return;
|
|
3062
3916
|
}
|
|
3917
|
+
if (w !== this.lastFrameW || h !== this.lastFrameH) {
|
|
3918
|
+
this.lastFrameRows.clear();
|
|
3919
|
+
this.lastFrameW = w;
|
|
3920
|
+
this.lastFrameH = h;
|
|
3921
|
+
}
|
|
3063
3922
|
this.drawHeader();
|
|
3064
3923
|
this.drawSeparator(HEADER_ROWS);
|
|
3065
3924
|
this.drawScrollback();
|
|
@@ -3075,38 +3934,40 @@ var init_screen = __esm({
|
|
|
3075
3934
|
}
|
|
3076
3935
|
drawHeader() {
|
|
3077
3936
|
const w = this.term.width;
|
|
3078
|
-
this.term.moveTo(1, 1).eraseLineAfter();
|
|
3079
3937
|
const usage = formatUsage(this.header.usage);
|
|
3080
3938
|
const sid = shortId(this.header.sessionId);
|
|
3081
3939
|
const title = this.header.title?.trim();
|
|
3082
|
-
const
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3940
|
+
const sig = `hdr|${w}|${this.header.agent}|${this.header.cwd}|${sid}|${title ?? ""}|${usage ?? ""}`;
|
|
3941
|
+
this.paintRow(1, sig, () => {
|
|
3942
|
+
const fixed = "hydra \xB7 ".length + this.header.agent.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
|
|
3943
|
+
const variableRoom = Math.max(8, w - fixed);
|
|
3944
|
+
let cwdRoom;
|
|
3945
|
+
let titleRoom;
|
|
3946
|
+
if (title) {
|
|
3947
|
+
const cwdMin = Math.min(this.header.cwd.length, 12);
|
|
3948
|
+
const titleCap = Math.max(8, variableRoom - cwdMin);
|
|
3949
|
+
titleRoom = Math.min(title.length, titleCap);
|
|
3950
|
+
cwdRoom = Math.max(8, variableRoom - titleRoom);
|
|
3951
|
+
} else {
|
|
3952
|
+
titleRoom = 0;
|
|
3953
|
+
cwdRoom = variableRoom;
|
|
3954
|
+
}
|
|
3955
|
+
this.term.bold("hydra")(" \xB7 ").cyan.noFormat(this.header.agent)(" \xB7 ").dim.noFormat(truncate(this.header.cwd, cwdRoom))(" \xB7 ").yellow(sid);
|
|
3956
|
+
if (title) {
|
|
3957
|
+
this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
|
|
3958
|
+
}
|
|
3959
|
+
if (usage) {
|
|
3960
|
+
const col = Math.max(1, w - usage.length + 1);
|
|
3961
|
+
this.term.moveTo(col, 1);
|
|
3962
|
+
this.term.dim(usage);
|
|
3963
|
+
}
|
|
3964
|
+
});
|
|
3103
3965
|
}
|
|
3104
3966
|
drawSeparator(row) {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
this.term.dim("\u2500".repeat(this.term.width));
|
|
3967
|
+
const w = this.term.width;
|
|
3968
|
+
this.paintRow(row, `sep|${w}`, () => {
|
|
3969
|
+
this.term.dim("\u2500".repeat(w));
|
|
3970
|
+
});
|
|
3110
3971
|
}
|
|
3111
3972
|
drawScrollback() {
|
|
3112
3973
|
const w = this.term.width;
|
|
@@ -3115,21 +3976,30 @@ var init_screen = __esm({
|
|
|
3115
3976
|
if (visibleRows <= 0) {
|
|
3116
3977
|
return;
|
|
3117
3978
|
}
|
|
3118
|
-
const wrapped = this.
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3979
|
+
const { rows: wrapped, exhausted } = this.wrapTail(
|
|
3980
|
+
w,
|
|
3981
|
+
visibleRows + this.scrollOffset
|
|
3982
|
+
);
|
|
3983
|
+
if (exhausted) {
|
|
3984
|
+
const max = Math.max(0, wrapped.length - visibleRows);
|
|
3985
|
+
if (this.scrollOffset > max) {
|
|
3986
|
+
this.scrollOffset = max;
|
|
3987
|
+
}
|
|
3122
3988
|
}
|
|
3123
3989
|
const end = wrapped.length - this.scrollOffset;
|
|
3124
3990
|
const start = Math.max(0, end - visibleRows);
|
|
3125
3991
|
const slice = wrapped.slice(start, end);
|
|
3992
|
+
const padTop = Math.max(0, visibleRows - slice.length);
|
|
3126
3993
|
for (let i = 0; i < visibleRows; i++) {
|
|
3127
3994
|
const row = top + i;
|
|
3128
|
-
|
|
3129
|
-
const line = slice[
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3995
|
+
const sliceIdx = i - padTop;
|
|
3996
|
+
const line = sliceIdx >= 0 ? slice[sliceIdx] : void 0;
|
|
3997
|
+
const sig = formattedLineSig("sb", w, line);
|
|
3998
|
+
this.paintRow(row, sig, () => {
|
|
3999
|
+
if (line) {
|
|
4000
|
+
this.writeFormattedLine(line, w);
|
|
4001
|
+
}
|
|
4002
|
+
});
|
|
3133
4003
|
}
|
|
3134
4004
|
}
|
|
3135
4005
|
queuedRows() {
|
|
@@ -3160,26 +4030,27 @@ var init_screen = __esm({
|
|
|
3160
4030
|
}
|
|
3161
4031
|
for (let i = 0; i < rows; i++) {
|
|
3162
4032
|
const row = completionTop + i;
|
|
3163
|
-
this.term.moveTo(1, row).eraseLineAfter();
|
|
3164
4033
|
const item = this.completions[i];
|
|
3165
|
-
if (!item) {
|
|
3166
|
-
continue;
|
|
3167
|
-
}
|
|
3168
4034
|
const isLast = i === rows - 1 && this.completions.length > MAX_COMPLETION_ROWS;
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
)
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
4035
|
+
const overflow = this.completions.length - MAX_COMPLETION_ROWS + 1;
|
|
4036
|
+
const sig = item ? isLast ? `comp|${w}|overflow|${overflow}` : `comp|${w}|${nameWidth}|${item.name}|${item.description ?? ""}` : `comp|${w}|empty`;
|
|
4037
|
+
this.paintRow(row, sig, () => {
|
|
4038
|
+
if (!item) {
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
if (isLast) {
|
|
4042
|
+
this.term.dim(` + ${overflow} more match(es)`);
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
const namePadded = item.name.padEnd(nameWidth);
|
|
4046
|
+
const desc = item.description ?? "";
|
|
4047
|
+
const remaining = w - namePadded.length - 4;
|
|
4048
|
+
const truncated = remaining > 0 ? truncate(desc, remaining) : "";
|
|
4049
|
+
this.term(" ").brightCyan(namePadded);
|
|
4050
|
+
if (truncated.length > 0) {
|
|
4051
|
+
this.term(" ").dim(truncated);
|
|
4052
|
+
}
|
|
4053
|
+
});
|
|
3183
4054
|
}
|
|
3184
4055
|
}
|
|
3185
4056
|
drawQueuedZone() {
|
|
@@ -3194,17 +4065,19 @@ var init_screen = __esm({
|
|
|
3194
4065
|
const queuedTop = queuedBottom - rows + 1;
|
|
3195
4066
|
for (let i = 0; i < rows; i++) {
|
|
3196
4067
|
const row = queuedTop + i;
|
|
3197
|
-
this.term.moveTo(1, row).eraseLineAfter();
|
|
3198
4068
|
const text = this.queuedTexts[i];
|
|
3199
|
-
if (text === void 0) {
|
|
3200
|
-
continue;
|
|
3201
|
-
}
|
|
3202
4069
|
const isLast = i === rows - 1 && this.queuedTexts.length > MAX_QUEUED_ROWS;
|
|
3203
4070
|
const overflow = this.queuedTexts.length - MAX_QUEUED_ROWS;
|
|
3204
|
-
const summary = isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
|
|
3205
|
-
const
|
|
3206
|
-
|
|
3207
|
-
|
|
4071
|
+
const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
|
|
4072
|
+
const sig = text === void 0 ? `queued|${w}|empty` : `queued|${w}|${isLast ? "ovf" : "row"}|${summary}`;
|
|
4073
|
+
this.paintRow(row, sig, () => {
|
|
4074
|
+
if (text === void 0) {
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
const display = ` \u23F3 ${summary}`;
|
|
4078
|
+
const padded = display + " ".repeat(Math.max(0, w - display.length));
|
|
4079
|
+
this.term.bgBlue.brightWhite.noFormat(padded);
|
|
4080
|
+
});
|
|
3208
4081
|
}
|
|
3209
4082
|
}
|
|
3210
4083
|
drawPrompt() {
|
|
@@ -3225,19 +4098,30 @@ var init_screen = __esm({
|
|
|
3225
4098
|
for (let i = 0; i < layout.rendered; i++) {
|
|
3226
4099
|
const vr = visualRows[layout.windowStart + i];
|
|
3227
4100
|
const row = top + i;
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
this.term(" ");
|
|
4101
|
+
let gutter = "wrap";
|
|
4102
|
+
let slice = "";
|
|
4103
|
+
if (vr) {
|
|
4104
|
+
if (vr.bufferIdx === 0 && vr.startCol === 0) {
|
|
4105
|
+
gutter = "first";
|
|
4106
|
+
} else if (vr.startCol === 0) {
|
|
4107
|
+
gutter = "newline";
|
|
4108
|
+
}
|
|
4109
|
+
slice = (state.buffer[vr.bufferIdx] ?? "").slice(vr.startCol, vr.endCol);
|
|
3238
4110
|
}
|
|
3239
|
-
const
|
|
3240
|
-
this.
|
|
4111
|
+
const sig = vr ? `prompt|${this.term.width}|${gutter}|${slice}` : `prompt|${this.term.width}|empty`;
|
|
4112
|
+
this.paintRow(row, sig, () => {
|
|
4113
|
+
if (!vr) {
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
if (gutter === "first") {
|
|
4117
|
+
this.term.brightWhite("> ");
|
|
4118
|
+
} else if (gutter === "newline") {
|
|
4119
|
+
this.term.dim("\xB7 ");
|
|
4120
|
+
} else {
|
|
4121
|
+
this.term(" ");
|
|
4122
|
+
}
|
|
4123
|
+
this.term.noFormat(slice);
|
|
4124
|
+
});
|
|
3241
4125
|
}
|
|
3242
4126
|
}
|
|
3243
4127
|
drawConfirmPrompt() {
|
|
@@ -3247,10 +4131,12 @@ var init_screen = __esm({
|
|
|
3247
4131
|
}
|
|
3248
4132
|
const w = this.term.width;
|
|
3249
4133
|
const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
|
|
3250
|
-
this.
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
this.
|
|
4134
|
+
this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
|
|
4135
|
+
this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
|
|
4136
|
+
});
|
|
4137
|
+
this.paintRow(top + 1, `confirm|h|${w}|${spec.hint}`, () => {
|
|
4138
|
+
this.term.dim(` ${truncate(spec.hint, w - 2)}`);
|
|
4139
|
+
});
|
|
3254
4140
|
}
|
|
3255
4141
|
drawPermissionPrompt() {
|
|
3256
4142
|
const spec = this.permissionPrompt;
|
|
@@ -3261,21 +4147,20 @@ var init_screen = __esm({
|
|
|
3261
4147
|
const rows = this.permissionRows();
|
|
3262
4148
|
const top = this.term.height - rows - BANNER_ROWS + 1;
|
|
3263
4149
|
let row = top;
|
|
3264
|
-
const writeRow = (paint) => {
|
|
4150
|
+
const writeRow = (sig, paint) => {
|
|
3265
4151
|
if (row >= top + rows) {
|
|
3266
4152
|
return;
|
|
3267
4153
|
}
|
|
3268
|
-
this.
|
|
3269
|
-
paint();
|
|
4154
|
+
this.paintRow(row, sig, paint);
|
|
3270
4155
|
row += 1;
|
|
3271
4156
|
};
|
|
3272
|
-
writeRow(() => {
|
|
4157
|
+
writeRow(`perm|t|${w}|${spec.title}`, () => {
|
|
3273
4158
|
this.term.brightYellow(` \u{1F512} ${truncate(spec.title, w - 5)}`);
|
|
3274
4159
|
});
|
|
3275
|
-
writeRow(() => {
|
|
4160
|
+
writeRow(`perm|sub|${w}`, () => {
|
|
3276
4161
|
this.term.dim(" This action requires approval");
|
|
3277
4162
|
});
|
|
3278
|
-
writeRow(() => {
|
|
4163
|
+
writeRow(`perm|q|${w}`, () => {
|
|
3279
4164
|
this.term(" Do you want to proceed?");
|
|
3280
4165
|
});
|
|
3281
4166
|
for (let i = 0; i < spec.options.length; i++) {
|
|
@@ -3289,7 +4174,7 @@ var init_screen = __esm({
|
|
|
3289
4174
|
const isSel = i === spec.selectedIndex;
|
|
3290
4175
|
const marker = isSel ? "\u276F" : " ";
|
|
3291
4176
|
const body = ` ${marker} ${i + 1}. ${truncate(opt.label, w - 8)}`;
|
|
3292
|
-
writeRow(() => {
|
|
4177
|
+
writeRow(`perm|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.label}`, () => {
|
|
3293
4178
|
if (isSel) {
|
|
3294
4179
|
this.term.brightCyan(body);
|
|
3295
4180
|
} else {
|
|
@@ -3297,38 +4182,42 @@ var init_screen = __esm({
|
|
|
3297
4182
|
}
|
|
3298
4183
|
});
|
|
3299
4184
|
}
|
|
3300
|
-
writeRow(() => {
|
|
4185
|
+
writeRow(`perm|hint|${w}`, () => {
|
|
3301
4186
|
this.term.dim(" \u2191/\u2193 choose \xB7 Enter submit \xB7 Esc cancel \xB7 1\u20139 quick-pick");
|
|
3302
4187
|
});
|
|
3303
4188
|
}
|
|
3304
4189
|
drawBanner() {
|
|
3305
4190
|
const row = this.term.height;
|
|
3306
|
-
this.term.
|
|
3307
|
-
const
|
|
3308
|
-
const
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
4191
|
+
const w = this.term.width;
|
|
4192
|
+
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
4193
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.planMode ? "1" : "0"}|${this.banner.hint}`;
|
|
4194
|
+
this.paintRow(row, sig, () => {
|
|
4195
|
+
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
4196
|
+
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
4197
|
+
if (this.banner.status === "busy") {
|
|
4198
|
+
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
4199
|
+
if (elapsedStr) {
|
|
4200
|
+
this.term(" ").dim(elapsedStr);
|
|
4201
|
+
}
|
|
4202
|
+
} else if (this.banner.status === "disconnected") {
|
|
4203
|
+
this.term.brightRed(`${dot} ${this.banner.status}`);
|
|
4204
|
+
} else {
|
|
4205
|
+
this.term.brightGreen(`${dot} ${this.banner.status}`);
|
|
3313
4206
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
this.
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
this.term(" \xB7 ")
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
} else {
|
|
3329
|
-
this.term.dim(planLabel);
|
|
3330
|
-
}
|
|
3331
|
-
this.term(" \xB7 ").dim(this.banner.hint);
|
|
4207
|
+
if (this.banner.queued > 0) {
|
|
4208
|
+
this.term(" \xB7 ").brightYellow(`${this.banner.queued} queued`);
|
|
4209
|
+
}
|
|
4210
|
+
if (this.scrollOffset > 0) {
|
|
4211
|
+
this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
|
|
4212
|
+
}
|
|
4213
|
+
this.term(" \xB7 ");
|
|
4214
|
+
if (this.banner.planMode) {
|
|
4215
|
+
this.term.brightYellow(planLabel);
|
|
4216
|
+
} else {
|
|
4217
|
+
this.term.dim(planLabel);
|
|
4218
|
+
}
|
|
4219
|
+
this.term(" \xB7 ").dim(this.banner.hint);
|
|
4220
|
+
});
|
|
3332
4221
|
}
|
|
3333
4222
|
placeCursor() {
|
|
3334
4223
|
if (this.permissionPrompt) {
|
|
@@ -3378,37 +4267,83 @@ var init_screen = __esm({
|
|
|
3378
4267
|
4 + this.permissionPrompt.options.length
|
|
3379
4268
|
);
|
|
3380
4269
|
}
|
|
3381
|
-
|
|
4270
|
+
// Walk this.lines from the tail, accumulating wrapped rows via the
|
|
4271
|
+
// wrap cache, until we have at least `needed` rows or run out. Returns
|
|
4272
|
+
// the collected rows in original (top-down) order plus an `exhausted`
|
|
4273
|
+
// flag that's true iff we reached the head of this.lines. The hot path
|
|
4274
|
+
// (drawScrollback) only ever asks for `visibleRows + scrollOffset`
|
|
4275
|
+
// rows, so a 10k-line scrollback costs ~50 cache hits per repaint
|
|
4276
|
+
// instead of 10k. With `needed = Infinity` this walks everything and
|
|
4277
|
+
// doubles as a total-row counter for maxScrollOffset.
|
|
4278
|
+
wrapTail(width, needed) {
|
|
3382
4279
|
if (width <= 4) {
|
|
3383
|
-
|
|
4280
|
+
const take = Math.min(needed, this.lines.length);
|
|
4281
|
+
return {
|
|
4282
|
+
rows: this.lines.slice(this.lines.length - take),
|
|
4283
|
+
exhausted: needed >= this.lines.length
|
|
4284
|
+
};
|
|
3384
4285
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
4286
|
+
if (this.wrapCacheWidth !== width) {
|
|
4287
|
+
this.wrapCache.clear();
|
|
4288
|
+
this.wrapCacheWidth = width;
|
|
4289
|
+
}
|
|
4290
|
+
if (needed <= 0 || this.lines.length === 0) {
|
|
4291
|
+
return { rows: [], exhausted: true };
|
|
4292
|
+
}
|
|
4293
|
+
const batches = [];
|
|
4294
|
+
let total = 0;
|
|
4295
|
+
let stoppedAt = 0;
|
|
4296
|
+
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
4297
|
+
const wrapped = this.wrapOne(this.lines[i], width);
|
|
4298
|
+
batches.push(wrapped);
|
|
4299
|
+
total += wrapped.length;
|
|
4300
|
+
stoppedAt = i;
|
|
4301
|
+
if (total >= needed) {
|
|
4302
|
+
break;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
const rows = [];
|
|
4306
|
+
for (let i = batches.length - 1; i >= 0; i--) {
|
|
4307
|
+
rows.push(...batches[i]);
|
|
4308
|
+
}
|
|
4309
|
+
return { rows, exhausted: stoppedAt === 0 };
|
|
4310
|
+
}
|
|
4311
|
+
wrapOne(line, width) {
|
|
4312
|
+
const id = this.lineIds.get(line);
|
|
4313
|
+
if (id !== void 0) {
|
|
4314
|
+
const cached = this.wrapCache.get(id);
|
|
4315
|
+
if (cached) {
|
|
4316
|
+
return cached;
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
const prefix = line.prefix ?? "";
|
|
4320
|
+
const room = Math.max(1, width - prefix.length);
|
|
4321
|
+
const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room);
|
|
4322
|
+
const wrapped = [];
|
|
4323
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
4324
|
+
const chunk = chunks[i] ?? "";
|
|
4325
|
+
const wrappedLine = {
|
|
4326
|
+
prefix: i === 0 ? line.prefix : " ".repeat(prefix.length),
|
|
4327
|
+
body: chunk
|
|
4328
|
+
};
|
|
4329
|
+
if (line.prefixStyle !== void 0) {
|
|
4330
|
+
wrappedLine.prefixStyle = line.prefixStyle;
|
|
3409
4331
|
}
|
|
4332
|
+
if (line.bodyStyle !== void 0) {
|
|
4333
|
+
wrappedLine.bodyStyle = line.bodyStyle;
|
|
4334
|
+
}
|
|
4335
|
+
if (line.fillRow) {
|
|
4336
|
+
wrappedLine.fillRow = true;
|
|
4337
|
+
}
|
|
4338
|
+
if (line.ansi) {
|
|
4339
|
+
wrappedLine.ansi = true;
|
|
4340
|
+
}
|
|
4341
|
+
wrapped.push(wrappedLine);
|
|
4342
|
+
}
|
|
4343
|
+
if (id !== void 0) {
|
|
4344
|
+
this.wrapCache.set(id, wrapped);
|
|
3410
4345
|
}
|
|
3411
|
-
return
|
|
4346
|
+
return wrapped;
|
|
3412
4347
|
}
|
|
3413
4348
|
writeFormattedLine(line, width) {
|
|
3414
4349
|
if (line.prefix) {
|
|
@@ -3429,6 +4364,8 @@ var init_screen = __esm({
|
|
|
3429
4364
|
}
|
|
3430
4365
|
}
|
|
3431
4366
|
};
|
|
4367
|
+
NON_ASCII = /[^\x20-\x7e]/;
|
|
4368
|
+
SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3432
4369
|
shortId = stripHydraSessionPrefix;
|
|
3433
4370
|
}
|
|
3434
4371
|
});
|
|
@@ -3447,6 +4384,10 @@ var init_input = __esm({
|
|
|
3447
4384
|
savedDraft = null;
|
|
3448
4385
|
history = [];
|
|
3449
4386
|
turnRunning = false;
|
|
4387
|
+
// Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
|
|
4388
|
+
// here so ^Y can yank it back. Standard readline keeps a stack; we
|
|
4389
|
+
// only keep one slot because that's what 99% of yank uses look like.
|
|
4390
|
+
killBuffer = "";
|
|
3450
4391
|
constructor(opts = {}) {
|
|
3451
4392
|
this.history = [...opts.history ?? []];
|
|
3452
4393
|
this.planMode = opts.planMode ?? false;
|
|
@@ -3555,6 +4496,9 @@ var init_input = __esm({
|
|
|
3555
4496
|
case "ctrl-w":
|
|
3556
4497
|
this.killWord();
|
|
3557
4498
|
return [];
|
|
4499
|
+
case "ctrl-y":
|
|
4500
|
+
this.yank();
|
|
4501
|
+
return [];
|
|
3558
4502
|
case "escape":
|
|
3559
4503
|
return [];
|
|
3560
4504
|
}
|
|
@@ -3648,11 +4592,19 @@ var init_input = __esm({
|
|
|
3648
4592
|
}
|
|
3649
4593
|
killLine() {
|
|
3650
4594
|
const line = this.currentLine();
|
|
4595
|
+
const killed = line.slice(0, this.col);
|
|
4596
|
+
if (killed.length > 0) {
|
|
4597
|
+
this.killBuffer = killed;
|
|
4598
|
+
}
|
|
3651
4599
|
this.setCurrentLine(line.slice(this.col));
|
|
3652
4600
|
this.col = 0;
|
|
3653
4601
|
}
|
|
3654
4602
|
killToEnd() {
|
|
3655
4603
|
const line = this.currentLine();
|
|
4604
|
+
const killed = line.slice(this.col);
|
|
4605
|
+
if (killed.length > 0) {
|
|
4606
|
+
this.killBuffer = killed;
|
|
4607
|
+
}
|
|
3656
4608
|
this.setCurrentLine(line.slice(0, this.col));
|
|
3657
4609
|
}
|
|
3658
4610
|
killWord() {
|
|
@@ -3668,9 +4620,19 @@ var init_input = __esm({
|
|
|
3668
4620
|
while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
|
|
3669
4621
|
i -= 1;
|
|
3670
4622
|
}
|
|
4623
|
+
const killed = line.slice(i, this.col);
|
|
4624
|
+
if (killed.length > 0) {
|
|
4625
|
+
this.killBuffer = killed;
|
|
4626
|
+
}
|
|
3671
4627
|
this.setCurrentLine(line.slice(0, i) + line.slice(this.col));
|
|
3672
4628
|
this.col = i;
|
|
3673
4629
|
}
|
|
4630
|
+
yank() {
|
|
4631
|
+
if (this.killBuffer.length === 0) {
|
|
4632
|
+
return;
|
|
4633
|
+
}
|
|
4634
|
+
this.insertText(this.killBuffer);
|
|
4635
|
+
}
|
|
3674
4636
|
moveLeft() {
|
|
3675
4637
|
if (this.col > 0) {
|
|
3676
4638
|
this.col -= 1;
|
|
@@ -3821,10 +4783,28 @@ function mapUpdate(update) {
|
|
|
3821
4783
|
}
|
|
3822
4784
|
function mapSessionInfo(u) {
|
|
3823
4785
|
const title = readString(u, "title");
|
|
3824
|
-
|
|
4786
|
+
const meta = u._meta;
|
|
4787
|
+
let agentId;
|
|
4788
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
4789
|
+
const ns = meta["hydra-acp"];
|
|
4790
|
+
if (ns && typeof ns === "object" && !Array.isArray(ns)) {
|
|
4791
|
+
const candidate = ns.agentId;
|
|
4792
|
+
if (typeof candidate === "string") {
|
|
4793
|
+
agentId = candidate;
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
if (title === void 0 && agentId === void 0) {
|
|
3825
4798
|
return null;
|
|
3826
4799
|
}
|
|
3827
|
-
|
|
4800
|
+
const event = { kind: "session-info" };
|
|
4801
|
+
if (title !== void 0) {
|
|
4802
|
+
event.title = title;
|
|
4803
|
+
}
|
|
4804
|
+
if (agentId !== void 0) {
|
|
4805
|
+
event.agentId = agentId;
|
|
4806
|
+
}
|
|
4807
|
+
return event;
|
|
3828
4808
|
}
|
|
3829
4809
|
function mapAvailableCommands(u) {
|
|
3830
4810
|
const list = u.availableCommands ?? u.commands;
|
|
@@ -4228,8 +5208,9 @@ function formatToolLine(state) {
|
|
|
4228
5208
|
title = `${initial} \xB7 ${latest}`;
|
|
4229
5209
|
}
|
|
4230
5210
|
return {
|
|
4231
|
-
prefix:
|
|
4232
|
-
|
|
5211
|
+
prefix: ` ${toolStatusIcon(state.status)} `,
|
|
5212
|
+
prefixStyle: toolIconStyle(state.status),
|
|
5213
|
+
body: title,
|
|
4233
5214
|
bodyStyle: toolStatusStyle(state.status)
|
|
4234
5215
|
};
|
|
4235
5216
|
}
|
|
@@ -4254,6 +5235,22 @@ function toolStatusIcon(status) {
|
|
|
4254
5235
|
return "\u25D0";
|
|
4255
5236
|
}
|
|
4256
5237
|
}
|
|
5238
|
+
function toolIconStyle(status) {
|
|
5239
|
+
switch (status) {
|
|
5240
|
+
case "completed":
|
|
5241
|
+
case "succeeded":
|
|
5242
|
+
case "ok":
|
|
5243
|
+
return "tool-status-ok";
|
|
5244
|
+
case "failed":
|
|
5245
|
+
case "error":
|
|
5246
|
+
case "rejected":
|
|
5247
|
+
return "tool-status-fail";
|
|
5248
|
+
case "cancelled":
|
|
5249
|
+
return "tool-status-cancelled";
|
|
5250
|
+
default:
|
|
5251
|
+
return "tool-status-running";
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
4257
5254
|
function formatPlan(event) {
|
|
4258
5255
|
if (event.entries.length === 0) {
|
|
4259
5256
|
return [
|
|
@@ -4333,23 +5330,32 @@ var init_format = __esm({
|
|
|
4333
5330
|
});
|
|
4334
5331
|
|
|
4335
5332
|
// src/tui/app.ts
|
|
5333
|
+
import { appendFileSync, statSync, renameSync } from "fs";
|
|
4336
5334
|
import { nanoid as nanoid3 } from "nanoid";
|
|
4337
5335
|
import termkit from "terminal-kit";
|
|
4338
5336
|
async function runTuiApp(opts) {
|
|
4339
5337
|
const config = await ensureConfig();
|
|
4340
5338
|
await ensureDaemonReachable(config);
|
|
4341
5339
|
const term = termkit.terminal;
|
|
5340
|
+
const exitHint = {};
|
|
4342
5341
|
let nextOpts = opts;
|
|
4343
5342
|
while (nextOpts !== null) {
|
|
4344
|
-
nextOpts = await runSession(term, config, nextOpts);
|
|
5343
|
+
nextOpts = await runSession(term, config, nextOpts, exitHint);
|
|
5344
|
+
}
|
|
5345
|
+
if (exitHint.sessionId) {
|
|
5346
|
+
const short = stripHydraSessionPrefix(exitHint.sessionId);
|
|
5347
|
+
process.stdout.write(`To resume: hydra-acp tui --resume ${short}
|
|
5348
|
+
`);
|
|
4345
5349
|
}
|
|
4346
5350
|
}
|
|
4347
|
-
async function runSession(term, config, opts) {
|
|
5351
|
+
async function runSession(term, config, opts, exitHint) {
|
|
4348
5352
|
const ctx = await resolveSession(term, config, opts);
|
|
4349
5353
|
if (!ctx) {
|
|
4350
5354
|
term.grabInput(false);
|
|
4351
5355
|
process.exit(0);
|
|
4352
5356
|
}
|
|
5357
|
+
const launchLabel = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
5358
|
+
term.cyan(launchLabel)("\n");
|
|
4353
5359
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
4354
5360
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
4355
5361
|
const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
|
|
@@ -4396,8 +5402,9 @@ async function runSession(term, config, opts) {
|
|
|
4396
5402
|
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
4397
5403
|
if (before === 0 && pendingTurns > 0) {
|
|
4398
5404
|
sessionBusySince = Date.now();
|
|
5405
|
+
dispatcherRef?.setTurnRunning(true);
|
|
4399
5406
|
if (screenReady) {
|
|
4400
|
-
screenRef.setBanner({ status: "
|
|
5407
|
+
screenRef.setBanner({ status: "busy", elapsedMs: 0 });
|
|
4401
5408
|
}
|
|
4402
5409
|
if (sessionElapsedTimer === null && screenReady) {
|
|
4403
5410
|
sessionElapsedTimer = setInterval(() => {
|
|
@@ -4406,10 +5413,11 @@ async function runSession(term, config, opts) {
|
|
|
4406
5413
|
}
|
|
4407
5414
|
screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
|
|
4408
5415
|
renderToolsBlock();
|
|
4409
|
-
},
|
|
5416
|
+
}, 1e3);
|
|
4410
5417
|
}
|
|
4411
5418
|
} else if (before > 0 && pendingTurns === 0) {
|
|
4412
5419
|
sessionBusySince = null;
|
|
5420
|
+
dispatcherRef?.setTurnRunning(false);
|
|
4413
5421
|
if (sessionElapsedTimer !== null) {
|
|
4414
5422
|
clearInterval(sessionElapsedTimer);
|
|
4415
5423
|
sessionElapsedTimer = null;
|
|
@@ -4423,9 +5431,11 @@ async function runSession(term, config, opts) {
|
|
|
4423
5431
|
}
|
|
4424
5432
|
};
|
|
4425
5433
|
let screenRef = null;
|
|
5434
|
+
let dispatcherRef = null;
|
|
4426
5435
|
conn.onNotification("session/update", (params) => {
|
|
4427
5436
|
const { update } = params ?? {};
|
|
4428
5437
|
const event = mapUpdate(update);
|
|
5438
|
+
debugLogUpdate(update, event);
|
|
4429
5439
|
if (event?.kind === "user-text") {
|
|
4430
5440
|
adjustPendingTurns(1);
|
|
4431
5441
|
} else if (event?.kind === "turn-complete") {
|
|
@@ -4446,10 +5456,10 @@ async function runSession(term, config, opts) {
|
|
|
4446
5456
|
if (pendingPermission.toolCallId && toolCallId && pendingPermission.toolCallId !== toolCallId) {
|
|
4447
5457
|
return;
|
|
4448
5458
|
}
|
|
4449
|
-
const
|
|
5459
|
+
const resolve4 = pendingPermission.resolve;
|
|
4450
5460
|
pendingPermission = null;
|
|
4451
5461
|
screen.setPermissionPrompt(null);
|
|
4452
|
-
|
|
5462
|
+
resolve4(result ?? { outcome: { outcome: "cancelled" } });
|
|
4453
5463
|
};
|
|
4454
5464
|
const maybeDismissPermissionByToolUpdate = (update) => {
|
|
4455
5465
|
if (!pendingPermission?.toolCallId) {
|
|
@@ -4482,14 +5492,14 @@ async function runSession(term, config, opts) {
|
|
|
4482
5492
|
if (!pendingPermission) {
|
|
4483
5493
|
return;
|
|
4484
5494
|
}
|
|
4485
|
-
const { options, resolve:
|
|
5495
|
+
const { options, resolve: resolve4 } = pendingPermission;
|
|
4486
5496
|
pendingPermission = null;
|
|
4487
5497
|
screen.setPermissionPrompt(null);
|
|
4488
5498
|
if (optionId === null) {
|
|
4489
|
-
|
|
5499
|
+
resolve4({ outcome: { outcome: "cancelled" } });
|
|
4490
5500
|
return;
|
|
4491
5501
|
}
|
|
4492
|
-
|
|
5502
|
+
resolve4({ outcome: { outcome: "selected", optionId } });
|
|
4493
5503
|
void options;
|
|
4494
5504
|
};
|
|
4495
5505
|
conn.onRequest("session/request_permission", async (params) => {
|
|
@@ -4507,12 +5517,12 @@ async function runSession(term, config, opts) {
|
|
|
4507
5517
|
]);
|
|
4508
5518
|
return { outcome: { outcome: "cancelled" } };
|
|
4509
5519
|
}
|
|
4510
|
-
return new Promise((
|
|
5520
|
+
return new Promise((resolve4) => {
|
|
4511
5521
|
pendingPermission = {
|
|
4512
5522
|
title,
|
|
4513
5523
|
options,
|
|
4514
5524
|
selectedIndex: 0,
|
|
4515
|
-
resolve:
|
|
5525
|
+
resolve: resolve4,
|
|
4516
5526
|
toolCallId
|
|
4517
5527
|
};
|
|
4518
5528
|
refreshPermissionPrompt();
|
|
@@ -4542,6 +5552,7 @@ async function runSession(term, config, opts) {
|
|
|
4542
5552
|
let initialModel;
|
|
4543
5553
|
let initialMode;
|
|
4544
5554
|
let initialCommands;
|
|
5555
|
+
let initialTurnStartedAt;
|
|
4545
5556
|
if (ctx.sessionId === "__new__") {
|
|
4546
5557
|
const created = await conn.request("session/new", {
|
|
4547
5558
|
cwd: ctx.cwd,
|
|
@@ -4549,6 +5560,7 @@ async function runSession(term, config, opts) {
|
|
|
4549
5560
|
...opts.name ? { _meta: { [HYDRA_META_KEY]: { name: opts.name } } } : {}
|
|
4550
5561
|
});
|
|
4551
5562
|
resolvedSessionId = created.sessionId;
|
|
5563
|
+
exitHint.sessionId = resolvedSessionId;
|
|
4552
5564
|
const hydraMeta = extractHydraMeta(created._meta ?? void 0);
|
|
4553
5565
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
4554
5566
|
if (hydraMeta.agentId) {
|
|
@@ -4562,6 +5574,7 @@ async function runSession(term, config, opts) {
|
|
|
4562
5574
|
}
|
|
4563
5575
|
initialModel = hydraMeta.currentModel;
|
|
4564
5576
|
initialMode = hydraMeta.currentMode;
|
|
5577
|
+
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
4565
5578
|
if (hydraMeta.availableCommands) {
|
|
4566
5579
|
initialCommands = hydraMeta.availableCommands.map(
|
|
4567
5580
|
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
@@ -4574,6 +5587,7 @@ async function runSession(term, config, opts) {
|
|
|
4574
5587
|
clientInfo: { name: "hydra-acp-tui", version: "0.1.0" }
|
|
4575
5588
|
});
|
|
4576
5589
|
resolvedSessionId = attached.sessionId;
|
|
5590
|
+
exitHint.sessionId = resolvedSessionId;
|
|
4577
5591
|
const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
|
|
4578
5592
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
4579
5593
|
if (hydraMeta.agentId) {
|
|
@@ -4587,20 +5601,26 @@ async function runSession(term, config, opts) {
|
|
|
4587
5601
|
}
|
|
4588
5602
|
initialModel = hydraMeta.currentModel;
|
|
4589
5603
|
initialMode = hydraMeta.currentMode;
|
|
5604
|
+
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
4590
5605
|
if (hydraMeta.availableCommands) {
|
|
4591
5606
|
initialCommands = hydraMeta.availableCommands.map(
|
|
4592
5607
|
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
4593
5608
|
);
|
|
4594
5609
|
}
|
|
4595
5610
|
}
|
|
4596
|
-
const historyFile = paths.tuiHistoryFile();
|
|
5611
|
+
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
4597
5612
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
4598
5613
|
const dispatcher = new InputDispatcher({ history });
|
|
5614
|
+
dispatcherRef = dispatcher;
|
|
5615
|
+
if (pendingTurns > 0) {
|
|
5616
|
+
dispatcher.setTurnRunning(true);
|
|
5617
|
+
}
|
|
4599
5618
|
let turnInFlight = null;
|
|
4600
5619
|
const screen = new Screen({
|
|
4601
5620
|
term,
|
|
4602
5621
|
dispatcher,
|
|
4603
5622
|
repaintThrottleMs: config.tui.repaintThrottleMs,
|
|
5623
|
+
maxScrollbackLines: config.tui.maxScrollbackLines,
|
|
4604
5624
|
onKey: (events) => {
|
|
4605
5625
|
for (const ev of events) {
|
|
4606
5626
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
@@ -4764,14 +5784,21 @@ async function runSession(term, config, opts) {
|
|
|
4764
5784
|
screen.appendLines(formatEvent({ kind: "model-changed", model: initialModel }));
|
|
4765
5785
|
}
|
|
4766
5786
|
let finishSession = null;
|
|
4767
|
-
const sessionDone = new Promise((
|
|
4768
|
-
finishSession =
|
|
5787
|
+
const sessionDone = new Promise((resolve4) => {
|
|
5788
|
+
finishSession = resolve4;
|
|
4769
5789
|
});
|
|
5790
|
+
const cancelRemoteTurn = () => {
|
|
5791
|
+
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
5792
|
+
};
|
|
4770
5793
|
const sigintHandler = () => {
|
|
4771
5794
|
if (turnInFlight) {
|
|
4772
5795
|
turnInFlight.cancel();
|
|
4773
5796
|
return;
|
|
4774
5797
|
}
|
|
5798
|
+
if (pendingTurns > 0) {
|
|
5799
|
+
cancelRemoteTurn();
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
4775
5802
|
void requestExit();
|
|
4776
5803
|
};
|
|
4777
5804
|
let exitConfirmation = null;
|
|
@@ -4846,6 +5873,7 @@ async function runSession(term, config, opts) {
|
|
|
4846
5873
|
};
|
|
4847
5874
|
const teardown = () => {
|
|
4848
5875
|
process.off("SIGINT", sigintHandler);
|
|
5876
|
+
screen.clearWindowTitle();
|
|
4849
5877
|
screen.stop();
|
|
4850
5878
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
4851
5879
|
void stream.close().catch(() => void 0);
|
|
@@ -4861,22 +5889,32 @@ async function runSession(term, config, opts) {
|
|
|
4861
5889
|
}
|
|
4862
5890
|
};
|
|
4863
5891
|
const switchSession = async () => {
|
|
4864
|
-
|
|
4865
|
-
if (!resume) {
|
|
5892
|
+
if (!finishSession) {
|
|
4866
5893
|
return;
|
|
4867
5894
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
5895
|
+
const pendingDraft = dispatcher.state().buffer.join("\n");
|
|
5896
|
+
if (pendingDraft.replace(/\s+$/, "").length > 0) {
|
|
5897
|
+
history = appendEntry(history, pendingDraft);
|
|
5898
|
+
dispatcher.setHistory(history);
|
|
5899
|
+
}
|
|
5900
|
+
screen.pauseRepaint();
|
|
5901
|
+
screen.stop();
|
|
5902
|
+
saveHistory(historyFile, history).catch(() => void 0);
|
|
4870
5903
|
const sessions = await listSessions(config);
|
|
4871
5904
|
const choice = await pickSession(term, {
|
|
4872
5905
|
cwd: resolvedCwd,
|
|
4873
5906
|
sessions,
|
|
4874
|
-
|
|
5907
|
+
config
|
|
4875
5908
|
});
|
|
4876
5909
|
if (choice.kind === "abort") {
|
|
4877
|
-
|
|
5910
|
+
screen.start();
|
|
5911
|
+
screen.resumeRepaint();
|
|
4878
5912
|
return;
|
|
4879
5913
|
}
|
|
5914
|
+
const resume = finishSession;
|
|
5915
|
+
finishSession = null;
|
|
5916
|
+
process.off("SIGINT", sigintHandler);
|
|
5917
|
+
void stream.close().catch(() => void 0);
|
|
4880
5918
|
if (choice.kind === "new") {
|
|
4881
5919
|
const { sessionId: _drop, ...rest } = opts;
|
|
4882
5920
|
void _drop;
|
|
@@ -4901,6 +5939,8 @@ async function runSession(term, config, opts) {
|
|
|
4901
5939
|
case "cancel":
|
|
4902
5940
|
if (turnInFlight) {
|
|
4903
5941
|
turnInFlight.cancel();
|
|
5942
|
+
} else if (pendingTurns > 0) {
|
|
5943
|
+
cancelRemoteTurn();
|
|
4904
5944
|
}
|
|
4905
5945
|
if (promptQueue.length > (workerActive ? 1 : 0)) {
|
|
4906
5946
|
promptQueue.length = workerActive ? 1 : 0;
|
|
@@ -4917,7 +5957,7 @@ async function runSession(term, config, opts) {
|
|
|
4917
5957
|
screen.setBanner({});
|
|
4918
5958
|
return;
|
|
4919
5959
|
case "redraw":
|
|
4920
|
-
screen.
|
|
5960
|
+
screen.fullRedraw();
|
|
4921
5961
|
return;
|
|
4922
5962
|
case "switch-session":
|
|
4923
5963
|
void switchSession();
|
|
@@ -5088,7 +6128,6 @@ async function runSession(term, config, opts) {
|
|
|
5088
6128
|
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
5089
6129
|
adjustPendingTurns(1);
|
|
5090
6130
|
appendRender({ kind: "user-text", text });
|
|
5091
|
-
dispatcher.setTurnRunning(true);
|
|
5092
6131
|
let cancelled = false;
|
|
5093
6132
|
turnInFlight = {
|
|
5094
6133
|
cancel: () => {
|
|
@@ -5118,7 +6157,6 @@ async function runSession(term, config, opts) {
|
|
|
5118
6157
|
});
|
|
5119
6158
|
} finally {
|
|
5120
6159
|
turnInFlight = null;
|
|
5121
|
-
dispatcher.setTurnRunning(false);
|
|
5122
6160
|
adjustPendingTurns(-1);
|
|
5123
6161
|
appendRender(
|
|
5124
6162
|
stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
|
|
@@ -5174,7 +6212,7 @@ async function runSession(term, config, opts) {
|
|
|
5174
6212
|
const elapsed = end - toolsBlockStartedAt;
|
|
5175
6213
|
let summary;
|
|
5176
6214
|
if (total === 0) {
|
|
5177
|
-
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `
|
|
6215
|
+
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
|
|
5178
6216
|
} else {
|
|
5179
6217
|
const noun = total === 1 ? "tool" : "tools";
|
|
5180
6218
|
const timing = inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
@@ -5245,6 +6283,10 @@ async function runSession(term, config, opts) {
|
|
|
5245
6283
|
if (event.title !== void 0) {
|
|
5246
6284
|
screen.setHeader({ title: event.title });
|
|
5247
6285
|
}
|
|
6286
|
+
if (event.agentId !== void 0 && event.agentId !== resolvedAgentId) {
|
|
6287
|
+
resolvedAgentId = event.agentId;
|
|
6288
|
+
screen.setHeader({ agent: event.agentId });
|
|
6289
|
+
}
|
|
5248
6290
|
return;
|
|
5249
6291
|
}
|
|
5250
6292
|
if (event.kind === "usage-update") {
|
|
@@ -5277,6 +6319,12 @@ async function runSession(term, config, opts) {
|
|
|
5277
6319
|
if (formatted2.length > 0) {
|
|
5278
6320
|
screen.appendLines(formatted2);
|
|
5279
6321
|
}
|
|
6322
|
+
screen.clearKey("tools");
|
|
6323
|
+
screen.clearKey("plan");
|
|
6324
|
+
toolStates.clear();
|
|
6325
|
+
toolCallOrder.length = 0;
|
|
6326
|
+
toolsExpanded = false;
|
|
6327
|
+
toolsBlockEndedAt = null;
|
|
5280
6328
|
startToolsBlock();
|
|
5281
6329
|
screen.redraw();
|
|
5282
6330
|
return;
|
|
@@ -5317,12 +6365,10 @@ async function runSession(term, config, opts) {
|
|
|
5317
6365
|
if (event.kind === "turn-complete") {
|
|
5318
6366
|
closeAgentText();
|
|
5319
6367
|
screen.clearKey("plan");
|
|
5320
|
-
if (
|
|
6368
|
+
if (toolsBlockStartedAt !== null) {
|
|
5321
6369
|
toolsBlockEndedAt = Date.now();
|
|
5322
6370
|
renderToolsBlock();
|
|
5323
6371
|
screen.clearKey("tools");
|
|
5324
|
-
} else if (toolsBlockStartedAt !== null) {
|
|
5325
|
-
screen.removeBlock("tools");
|
|
5326
6372
|
}
|
|
5327
6373
|
toolStates.clear();
|
|
5328
6374
|
toolCallOrder.length = 0;
|
|
@@ -5342,22 +6388,35 @@ async function runSession(term, config, opts) {
|
|
|
5342
6388
|
} finally {
|
|
5343
6389
|
screen.resumeRepaint();
|
|
5344
6390
|
}
|
|
6391
|
+
if (initialTurnStartedAt !== void 0 && pendingTurns > 0) {
|
|
6392
|
+
sessionBusySince = initialTurnStartedAt;
|
|
6393
|
+
screen.setBanner({
|
|
6394
|
+
status: "busy",
|
|
6395
|
+
elapsedMs: Date.now() - initialTurnStartedAt
|
|
6396
|
+
});
|
|
6397
|
+
if (sessionElapsedTimer === null) {
|
|
6398
|
+
sessionElapsedTimer = setInterval(() => {
|
|
6399
|
+
if (sessionBusySince === null || screenRef === null) {
|
|
6400
|
+
return;
|
|
6401
|
+
}
|
|
6402
|
+
screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
|
|
6403
|
+
renderToolsBlock();
|
|
6404
|
+
}, 1e3);
|
|
6405
|
+
}
|
|
6406
|
+
startToolsBlock();
|
|
6407
|
+
}
|
|
5345
6408
|
const resetInFlightUiState = () => {
|
|
5346
6409
|
if (pendingPermission) {
|
|
5347
|
-
const
|
|
6410
|
+
const resolve4 = pendingPermission.resolve;
|
|
5348
6411
|
pendingPermission = null;
|
|
5349
6412
|
screen.setPermissionPrompt(null);
|
|
5350
|
-
|
|
6413
|
+
resolve4({ outcome: { outcome: "cancelled" } });
|
|
5351
6414
|
}
|
|
5352
6415
|
closeAgentText();
|
|
5353
6416
|
if (toolsBlockStartedAt !== null) {
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
screen.clearKey("tools");
|
|
5358
|
-
} else {
|
|
5359
|
-
screen.removeBlock("tools");
|
|
5360
|
-
}
|
|
6417
|
+
toolsBlockEndedAt = Date.now();
|
|
6418
|
+
renderToolsBlock();
|
|
6419
|
+
screen.clearKey("tools");
|
|
5361
6420
|
toolStates.clear();
|
|
5362
6421
|
toolCallOrder.length = 0;
|
|
5363
6422
|
toolsBlockStartedAt = null;
|
|
@@ -5427,7 +6486,7 @@ async function runSession(term, config, opts) {
|
|
|
5427
6486
|
]);
|
|
5428
6487
|
}
|
|
5429
6488
|
screen.setBanner({
|
|
5430
|
-
status: pendingTurns > 0 ? "
|
|
6489
|
+
status: pendingTurns > 0 ? "busy" : "ready",
|
|
5431
6490
|
elapsedMs: pendingTurns > 0 ? 0 : void 0
|
|
5432
6491
|
});
|
|
5433
6492
|
};
|
|
@@ -5475,7 +6534,7 @@ async function resolveSession(term, config, opts) {
|
|
|
5475
6534
|
const choice = await pickSession(term, {
|
|
5476
6535
|
cwd,
|
|
5477
6536
|
sessions,
|
|
5478
|
-
|
|
6537
|
+
config
|
|
5479
6538
|
});
|
|
5480
6539
|
if (choice.kind === "abort") {
|
|
5481
6540
|
return null;
|
|
@@ -5496,7 +6555,41 @@ function newCtx(opts, cwd, config) {
|
|
|
5496
6555
|
cwd
|
|
5497
6556
|
};
|
|
5498
6557
|
}
|
|
5499
|
-
|
|
6558
|
+
function debugLogUpdate(update, event) {
|
|
6559
|
+
writeDebugLine({
|
|
6560
|
+
src: "session/update",
|
|
6561
|
+
update,
|
|
6562
|
+
event: event === null ? null : { kind: event.kind }
|
|
6563
|
+
});
|
|
6564
|
+
}
|
|
6565
|
+
function writeDebugLine(payload) {
|
|
6566
|
+
const override = process.env.HYDRA_TUI_DEBUG_LOG;
|
|
6567
|
+
const target = override === void 0 ? paths.tuiLogFile() : override;
|
|
6568
|
+
if (target.length === 0) {
|
|
6569
|
+
return;
|
|
6570
|
+
}
|
|
6571
|
+
try {
|
|
6572
|
+
rotateIfBig(target);
|
|
6573
|
+
const line = JSON.stringify({
|
|
6574
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6575
|
+
...payload
|
|
6576
|
+
});
|
|
6577
|
+
appendFileSync(target, `${line}
|
|
6578
|
+
`);
|
|
6579
|
+
} catch {
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
6582
|
+
function rotateIfBig(target) {
|
|
6583
|
+
try {
|
|
6584
|
+
const stat3 = statSync(target);
|
|
6585
|
+
if (stat3.size < TUI_LOG_MAX_BYTES) {
|
|
6586
|
+
return;
|
|
6587
|
+
}
|
|
6588
|
+
renameSync(target, `${target}.0`);
|
|
6589
|
+
} catch {
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
var PLAN_PREFIX_TEXT, TUI_LOG_MAX_BYTES;
|
|
5500
6593
|
var init_app = __esm({
|
|
5501
6594
|
"src/tui/app.ts"() {
|
|
5502
6595
|
"use strict";
|
|
@@ -5505,6 +6598,7 @@ var init_app = __esm({
|
|
|
5505
6598
|
init_resilient_ws();
|
|
5506
6599
|
init_config();
|
|
5507
6600
|
init_daemon_bootstrap();
|
|
6601
|
+
init_session();
|
|
5508
6602
|
init_paths();
|
|
5509
6603
|
init_history();
|
|
5510
6604
|
init_discovery();
|
|
@@ -5514,6 +6608,7 @@ var init_app = __esm({
|
|
|
5514
6608
|
init_render_update();
|
|
5515
6609
|
init_format();
|
|
5516
6610
|
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.";
|
|
6611
|
+
TUI_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
5517
6612
|
}
|
|
5518
6613
|
});
|
|
5519
6614
|
|
|
@@ -5529,6 +6624,11 @@ var init_tui = __esm({
|
|
|
5529
6624
|
}
|
|
5530
6625
|
});
|
|
5531
6626
|
|
|
6627
|
+
// src/cli.ts
|
|
6628
|
+
import { readFileSync } from "fs";
|
|
6629
|
+
import { fileURLToPath } from "url";
|
|
6630
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
6631
|
+
|
|
5532
6632
|
// src/cli/parse-args.ts
|
|
5533
6633
|
function parseArgs(argv) {
|
|
5534
6634
|
const positional = [];
|
|
@@ -5598,8 +6698,7 @@ async function runInit(flags) {
|
|
|
5598
6698
|
existing = void 0;
|
|
5599
6699
|
}
|
|
5600
6700
|
if (!existing) {
|
|
5601
|
-
const config =
|
|
5602
|
-
await writeConfig(config);
|
|
6701
|
+
const config = await writeMinimalInitConfig();
|
|
5603
6702
|
process.stdout.write(
|
|
5604
6703
|
`Initialized ${paths.config()}
|
|
5605
6704
|
Auth token: ${config.daemon.authToken}
|
|
@@ -5608,11 +6707,14 @@ Auth token: ${config.daemon.authToken}
|
|
|
5608
6707
|
return;
|
|
5609
6708
|
}
|
|
5610
6709
|
if (flagBool(flags, "rotate-token")) {
|
|
5611
|
-
|
|
5612
|
-
await
|
|
6710
|
+
const newToken = generateAuthToken();
|
|
6711
|
+
await updateConfigField((raw) => {
|
|
6712
|
+
const daemon = raw.daemon ??= {};
|
|
6713
|
+
daemon.authToken = newToken;
|
|
6714
|
+
});
|
|
5613
6715
|
process.stdout.write(
|
|
5614
6716
|
`Rotated token in ${paths.config()}
|
|
5615
|
-
New token: ${
|
|
6717
|
+
New token: ${newToken}
|
|
5616
6718
|
`
|
|
5617
6719
|
);
|
|
5618
6720
|
return;
|
|
@@ -5630,7 +6732,7 @@ import { setTimeout as sleep2 } from "timers/promises";
|
|
|
5630
6732
|
|
|
5631
6733
|
// src/daemon/server.ts
|
|
5632
6734
|
init_config();
|
|
5633
|
-
import * as
|
|
6735
|
+
import * as fs9 from "fs";
|
|
5634
6736
|
import * as fsp2 from "fs/promises";
|
|
5635
6737
|
import Fastify from "fastify";
|
|
5636
6738
|
import websocketPlugin from "@fastify/websocket";
|
|
@@ -5802,6 +6904,10 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
5802
6904
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
5803
6905
|
}
|
|
5804
6906
|
|
|
6907
|
+
// src/core/session-manager.ts
|
|
6908
|
+
import * as fs7 from "fs/promises";
|
|
6909
|
+
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
6910
|
+
|
|
5805
6911
|
// src/core/agent-instance.ts
|
|
5806
6912
|
import { spawn } from "child_process";
|
|
5807
6913
|
|
|
@@ -5859,13 +6965,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
5859
6965
|
throw new Error("stream is closed");
|
|
5860
6966
|
}
|
|
5861
6967
|
const line = JSON.stringify(message) + "\n";
|
|
5862
|
-
await new Promise((
|
|
6968
|
+
await new Promise((resolve4, reject) => {
|
|
5863
6969
|
stdin.write(line, (err) => {
|
|
5864
6970
|
if (err) {
|
|
5865
6971
|
reject(err);
|
|
5866
6972
|
return;
|
|
5867
6973
|
}
|
|
5868
|
-
|
|
6974
|
+
resolve4();
|
|
5869
6975
|
});
|
|
5870
6976
|
});
|
|
5871
6977
|
},
|
|
@@ -5946,7 +7052,14 @@ init_session();
|
|
|
5946
7052
|
init_paths();
|
|
5947
7053
|
import * as fs4 from "fs/promises";
|
|
5948
7054
|
import * as path2 from "path";
|
|
7055
|
+
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
5949
7056
|
import { z as z4 } from "zod";
|
|
7057
|
+
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7058
|
+
var generateRawId = customAlphabet2(HYDRA_ID_ALPHABET2, 16);
|
|
7059
|
+
var HYDRA_LINEAGE_PREFIX = "hydra_lineage_";
|
|
7060
|
+
function generateLineageId() {
|
|
7061
|
+
return `${HYDRA_LINEAGE_PREFIX}${generateRawId()}`;
|
|
7062
|
+
}
|
|
5950
7063
|
var PersistedAgentCommand = z4.object({
|
|
5951
7064
|
name: z4.string(),
|
|
5952
7065
|
description: z4.string().optional()
|
|
@@ -5954,7 +7067,20 @@ var PersistedAgentCommand = z4.object({
|
|
|
5954
7067
|
var SessionRecord = z4.object({
|
|
5955
7068
|
version: z4.literal(1),
|
|
5956
7069
|
sessionId: z4.string(),
|
|
7070
|
+
// Optional for back-compat with records written before this field
|
|
7071
|
+
// existed; mergeForPersistence generates one on next write so any
|
|
7072
|
+
// touched session converges to having a lineageId. A record that
|
|
7073
|
+
// never gets written again (truly cold and untouched) just won't
|
|
7074
|
+
// participate in lineage-based dedup, which is correct — it was
|
|
7075
|
+
// never exported, so no incoming bundle can claim its lineage.
|
|
7076
|
+
lineageId: z4.string().optional(),
|
|
5957
7077
|
upstreamSessionId: z4.string(),
|
|
7078
|
+
// When non-empty, marks a session that was created by import and is
|
|
7079
|
+
// waiting for its first attach to bootstrap a fresh upstream agent
|
|
7080
|
+
// and replay the imported history as a takeover transcript. The
|
|
7081
|
+
// origin's local id at export time, kept for debuggability and as a
|
|
7082
|
+
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
7083
|
+
importedFromSessionId: z4.string().optional(),
|
|
5958
7084
|
agentId: z4.string(),
|
|
5959
7085
|
cwd: z4.string(),
|
|
5960
7086
|
title: z4.string().optional(),
|
|
@@ -6027,6 +7153,25 @@ var SessionStore = class {
|
|
|
6027
7153
|
}
|
|
6028
7154
|
}
|
|
6029
7155
|
}
|
|
7156
|
+
// Find a persisted session by lineageId. Used by SessionManager.import
|
|
7157
|
+
// to detect bundles that have already been imported (lineageId match)
|
|
7158
|
+
// so we can either error out or, with replace:true, overwrite.
|
|
7159
|
+
// Returns undefined if no record has that lineageId. Records that
|
|
7160
|
+
// pre-date the lineageId field simply don't match — which is
|
|
7161
|
+
// correct: they were never exported, so no incoming bundle can
|
|
7162
|
+
// legitimately claim their lineage.
|
|
7163
|
+
async findByLineageId(lineageId) {
|
|
7164
|
+
if (lineageId.length === 0) {
|
|
7165
|
+
return void 0;
|
|
7166
|
+
}
|
|
7167
|
+
const all = await this.list().catch(() => []);
|
|
7168
|
+
for (const record of all) {
|
|
7169
|
+
if (record.lineageId === lineageId) {
|
|
7170
|
+
return record;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
return void 0;
|
|
7174
|
+
}
|
|
6030
7175
|
async list() {
|
|
6031
7176
|
let entries;
|
|
6032
7177
|
try {
|
|
@@ -6052,7 +7197,9 @@ function recordFromMemorySession(args) {
|
|
|
6052
7197
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6053
7198
|
return {
|
|
6054
7199
|
sessionId: args.sessionId,
|
|
7200
|
+
lineageId: args.lineageId,
|
|
6055
7201
|
upstreamSessionId: args.upstreamSessionId,
|
|
7202
|
+
importedFromSessionId: args.importedFromSessionId,
|
|
6056
7203
|
agentId: args.agentId,
|
|
6057
7204
|
cwd: args.cwd,
|
|
6058
7205
|
title: args.title,
|
|
@@ -6101,6 +7248,36 @@ var HistoryStore = class {
|
|
|
6101
7248
|
});
|
|
6102
7249
|
});
|
|
6103
7250
|
}
|
|
7251
|
+
// Trim the on-disk history file to the most recent maxEntries lines.
|
|
7252
|
+
// Runs through the same per-session write queue as append/rewrite so
|
|
7253
|
+
// it's safe to invoke alongside ongoing writes; a no-op if the file is
|
|
7254
|
+
// already at or below the cap.
|
|
7255
|
+
async compact(sessionId, maxEntries) {
|
|
7256
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
7257
|
+
return;
|
|
7258
|
+
}
|
|
7259
|
+
return this.enqueue(sessionId, async () => {
|
|
7260
|
+
let raw;
|
|
7261
|
+
try {
|
|
7262
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
7263
|
+
} catch (err) {
|
|
7264
|
+
const e = err;
|
|
7265
|
+
if (e.code === "ENOENT") {
|
|
7266
|
+
return;
|
|
7267
|
+
}
|
|
7268
|
+
throw err;
|
|
7269
|
+
}
|
|
7270
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
7271
|
+
if (lines.length <= maxEntries) {
|
|
7272
|
+
return;
|
|
7273
|
+
}
|
|
7274
|
+
const trimmed = lines.slice(-maxEntries);
|
|
7275
|
+
await fs5.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
7276
|
+
encoding: "utf8",
|
|
7277
|
+
mode: 384
|
|
7278
|
+
});
|
|
7279
|
+
});
|
|
7280
|
+
}
|
|
6104
7281
|
async load(sessionId) {
|
|
6105
7282
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6106
7283
|
return [];
|
|
@@ -6189,7 +7366,11 @@ var HistoryStore = class {
|
|
|
6189
7366
|
};
|
|
6190
7367
|
|
|
6191
7368
|
// src/core/session-manager.ts
|
|
7369
|
+
init_paths();
|
|
7370
|
+
init_history();
|
|
6192
7371
|
init_types();
|
|
7372
|
+
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7373
|
+
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
6193
7374
|
var SessionManager = class {
|
|
6194
7375
|
constructor(registry, spawner, store, options = {}) {
|
|
6195
7376
|
this.registry = registry;
|
|
@@ -6268,6 +7449,9 @@ var SessionManager = class {
|
|
|
6268
7449
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
6269
7450
|
throw err;
|
|
6270
7451
|
}
|
|
7452
|
+
if (params.upstreamSessionId === "") {
|
|
7453
|
+
return this.doResurrectFromImport(params);
|
|
7454
|
+
}
|
|
6271
7455
|
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
6272
7456
|
const agent = this.spawner({
|
|
6273
7457
|
agentId: params.agentId,
|
|
@@ -6304,12 +7488,53 @@ var SessionManager = class {
|
|
|
6304
7488
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
6305
7489
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
6306
7490
|
historyStore: this.histories,
|
|
6307
|
-
seedHistory: params.seedHistory,
|
|
6308
7491
|
currentModel: params.currentModel,
|
|
6309
7492
|
currentMode: params.currentMode,
|
|
6310
|
-
agentCommands: params.agentCommands
|
|
7493
|
+
agentCommands: params.agentCommands,
|
|
7494
|
+
// Only gate the first-prompt title heuristic when we actually have
|
|
7495
|
+
// a title to preserve. A title-less session (lost to a write race
|
|
7496
|
+
// or never seeded) should re-derive from the next prompt rather
|
|
7497
|
+
// than stay stuck.
|
|
7498
|
+
firstPromptSeeded: !!params.title,
|
|
7499
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
7500
|
+
});
|
|
7501
|
+
await this.attachManagerHooks(session);
|
|
7502
|
+
return session;
|
|
7503
|
+
}
|
|
7504
|
+
// First-attach path for a session that was created via import(). The
|
|
7505
|
+
// on-disk meta.json carries upstreamSessionId="" as the import
|
|
7506
|
+
// marker; bootstrap a fresh agent (gets a real upstream id) and kick
|
|
7507
|
+
// off seedFromImport so the agent absorbs the historical transcript.
|
|
7508
|
+
// attachManagerHooks rewrites meta.json with the new upstreamSessionId,
|
|
7509
|
+
// so subsequent resurrects of this session use the normal session/load
|
|
7510
|
+
// path.
|
|
7511
|
+
async doResurrectFromImport(params) {
|
|
7512
|
+
const fresh = await this.bootstrapAgent({
|
|
7513
|
+
agentId: params.agentId,
|
|
7514
|
+
cwd: params.cwd,
|
|
7515
|
+
agentArgs: params.agentArgs,
|
|
7516
|
+
mcpServers: []
|
|
7517
|
+
});
|
|
7518
|
+
const session = new Session({
|
|
7519
|
+
sessionId: params.hydraSessionId,
|
|
7520
|
+
cwd: params.cwd,
|
|
7521
|
+
agentId: params.agentId,
|
|
7522
|
+
agent: fresh.agent,
|
|
7523
|
+
upstreamSessionId: fresh.upstreamSessionId,
|
|
7524
|
+
agentMeta: fresh.agentMeta,
|
|
7525
|
+
title: params.title,
|
|
7526
|
+
agentArgs: params.agentArgs,
|
|
7527
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
7528
|
+
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7529
|
+
historyStore: this.histories,
|
|
7530
|
+
currentModel: params.currentModel,
|
|
7531
|
+
currentMode: params.currentMode,
|
|
7532
|
+
agentCommands: params.agentCommands,
|
|
7533
|
+
firstPromptSeeded: !!params.title,
|
|
7534
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
6311
7535
|
});
|
|
6312
7536
|
await this.attachManagerHooks(session);
|
|
7537
|
+
void session.seedFromImport().catch(() => void 0);
|
|
6313
7538
|
return session;
|
|
6314
7539
|
}
|
|
6315
7540
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
@@ -6391,28 +7616,20 @@ var SessionManager = class {
|
|
|
6391
7616
|
}).catch(() => void 0);
|
|
6392
7617
|
});
|
|
6393
7618
|
this.sessions.set(session.sessionId, session);
|
|
6394
|
-
await this.
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
cwd: session.cwd,
|
|
6400
|
-
title: session.title,
|
|
6401
|
-
agentArgs: session.agentArgs,
|
|
6402
|
-
currentModel: session.currentModel,
|
|
6403
|
-
currentMode: session.currentMode
|
|
6404
|
-
})
|
|
6405
|
-
).catch(() => void 0);
|
|
7619
|
+
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
7620
|
+
const existing = await this.store.read(session.sessionId);
|
|
7621
|
+
const merged = mergeForPersistence(session, existing);
|
|
7622
|
+
await this.store.write(merged);
|
|
7623
|
+
}).catch(() => void 0);
|
|
6406
7624
|
}
|
|
6407
7625
|
// Resolve a session's recorded history without forcing a resurrect.
|
|
6408
|
-
//
|
|
6409
|
-
//
|
|
6410
|
-
//
|
|
6411
|
-
//
|
|
7626
|
+
// Always loads from disk — that's the source of truth whether the
|
|
7627
|
+
// session is hot or cold. Returns undefined if the session id is
|
|
7628
|
+
// unknown to both the live map and disk store, so the caller can
|
|
7629
|
+
// distinguish "no history yet" (empty array) from "404".
|
|
6412
7630
|
async getHistory(sessionId) {
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
return live.getHistorySnapshot();
|
|
7631
|
+
if (this.sessions.has(sessionId)) {
|
|
7632
|
+
return this.histories.load(sessionId).catch(() => []);
|
|
6416
7633
|
}
|
|
6417
7634
|
const record = await this.store.read(sessionId);
|
|
6418
7635
|
if (!record) {
|
|
@@ -6425,20 +7642,41 @@ var SessionManager = class {
|
|
|
6425
7642
|
if (!record) {
|
|
6426
7643
|
return void 0;
|
|
6427
7644
|
}
|
|
6428
|
-
|
|
7645
|
+
let title = record.title;
|
|
7646
|
+
if (!title) {
|
|
7647
|
+
title = await this.deriveTitleFromHistory(sessionId);
|
|
7648
|
+
}
|
|
6429
7649
|
return {
|
|
6430
7650
|
hydraSessionId: record.sessionId,
|
|
6431
7651
|
upstreamSessionId: record.upstreamSessionId,
|
|
6432
7652
|
agentId: record.agentId,
|
|
6433
7653
|
cwd: record.cwd,
|
|
6434
|
-
title
|
|
7654
|
+
title,
|
|
6435
7655
|
agentArgs: record.agentArgs,
|
|
6436
|
-
seedHistory: seedHistory.length > 0 ? seedHistory : void 0,
|
|
6437
7656
|
currentModel: record.currentModel,
|
|
6438
7657
|
currentMode: record.currentMode,
|
|
6439
|
-
agentCommands: record.agentCommands
|
|
7658
|
+
agentCommands: record.agentCommands,
|
|
7659
|
+
createdAt: record.createdAt
|
|
6440
7660
|
};
|
|
6441
7661
|
}
|
|
7662
|
+
// Best-effort: peek at the persisted history's first prompt and use
|
|
7663
|
+
// its first line (capped to 200 chars) as a session title. Returns
|
|
7664
|
+
// undefined if no usable prompt is found or any I/O fails.
|
|
7665
|
+
async deriveTitleFromHistory(sessionId) {
|
|
7666
|
+
const history = await this.histories.load(sessionId).catch(() => []);
|
|
7667
|
+
for (const entry of history) {
|
|
7668
|
+
const params = entry.params;
|
|
7669
|
+
if (params?.update?.sessionUpdate !== "prompt_received") {
|
|
7670
|
+
continue;
|
|
7671
|
+
}
|
|
7672
|
+
const text = extractPromptText(params.update.prompt);
|
|
7673
|
+
const line = firstLine(text, 200);
|
|
7674
|
+
if (line) {
|
|
7675
|
+
return line;
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
return void 0;
|
|
7679
|
+
}
|
|
6442
7680
|
get(sessionId) {
|
|
6443
7681
|
return this.sessions.get(sessionId);
|
|
6444
7682
|
}
|
|
@@ -6478,13 +7716,14 @@ var SessionManager = class {
|
|
|
6478
7716
|
continue;
|
|
6479
7717
|
}
|
|
6480
7718
|
liveIds.add(session.sessionId);
|
|
7719
|
+
const used = await historyMtimeIso(session.sessionId) ?? new Date(session.updatedAt).toISOString();
|
|
6481
7720
|
entries.push({
|
|
6482
7721
|
sessionId: session.sessionId,
|
|
6483
7722
|
upstreamSessionId: session.upstreamSessionId,
|
|
6484
7723
|
cwd: session.cwd,
|
|
6485
7724
|
title: session.title,
|
|
6486
7725
|
agentId: session.agentId,
|
|
6487
|
-
updatedAt:
|
|
7726
|
+
updatedAt: used,
|
|
6488
7727
|
attachedClients: session.attachedCount,
|
|
6489
7728
|
status: "live"
|
|
6490
7729
|
});
|
|
@@ -6497,13 +7736,14 @@ var SessionManager = class {
|
|
|
6497
7736
|
if (filter.cwd && r.cwd !== filter.cwd) {
|
|
6498
7737
|
continue;
|
|
6499
7738
|
}
|
|
7739
|
+
const used = await historyMtimeIso(r.sessionId) ?? r.updatedAt;
|
|
6500
7740
|
entries.push({
|
|
6501
7741
|
sessionId: r.sessionId,
|
|
6502
7742
|
upstreamSessionId: r.upstreamSessionId,
|
|
6503
7743
|
cwd: r.cwd,
|
|
6504
7744
|
title: r.title,
|
|
6505
7745
|
agentId: r.agentId,
|
|
6506
|
-
updatedAt:
|
|
7746
|
+
updatedAt: used,
|
|
6507
7747
|
attachedClients: 0,
|
|
6508
7748
|
status: "cold"
|
|
6509
7749
|
});
|
|
@@ -6511,6 +7751,111 @@ var SessionManager = class {
|
|
|
6511
7751
|
entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
|
|
6512
7752
|
return entries;
|
|
6513
7753
|
}
|
|
7754
|
+
// Build an export bundle for a session, reading meta + history from
|
|
7755
|
+
// disk. Backfills lineageId if the on-disk record pre-dates that
|
|
7756
|
+
// field. Returns undefined if the session doesn't exist. Callers
|
|
7757
|
+
// populate the bundle's exportedFrom metadata themselves.
|
|
7758
|
+
async exportBundle(sessionId) {
|
|
7759
|
+
const record = await this.store.read(sessionId);
|
|
7760
|
+
if (!record) {
|
|
7761
|
+
return void 0;
|
|
7762
|
+
}
|
|
7763
|
+
let withLineage;
|
|
7764
|
+
if (record.lineageId) {
|
|
7765
|
+
withLineage = record;
|
|
7766
|
+
} else {
|
|
7767
|
+
const lineageId = generateLineageId();
|
|
7768
|
+
const backfilled = { ...record, lineageId };
|
|
7769
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
7770
|
+
const latest = await this.store.read(sessionId);
|
|
7771
|
+
if (!latest) {
|
|
7772
|
+
return;
|
|
7773
|
+
}
|
|
7774
|
+
if (latest.lineageId) {
|
|
7775
|
+
return;
|
|
7776
|
+
}
|
|
7777
|
+
await this.store.write({ ...latest, lineageId });
|
|
7778
|
+
}).catch(() => void 0);
|
|
7779
|
+
withLineage = backfilled;
|
|
7780
|
+
}
|
|
7781
|
+
const history = await this.histories.load(sessionId).catch(() => []);
|
|
7782
|
+
const promptHistory = await loadPromptHistorySafely(sessionId);
|
|
7783
|
+
return { record: withLineage, history, promptHistory };
|
|
7784
|
+
}
|
|
7785
|
+
// Create a local session from an imported bundle. Without `replace`,
|
|
7786
|
+
// a bundle with a lineageId we already have on disk throws
|
|
7787
|
+
// BundleAlreadyImported citing the existing local id. With
|
|
7788
|
+
// `replace: true`, the existing record is overwritten in-place (its
|
|
7789
|
+
// local sessionId is preserved so bookmarks/Slack thread links still
|
|
7790
|
+
// resolve), and any live in-memory session is closed so the next
|
|
7791
|
+
// attach triggers the import-reseed path.
|
|
7792
|
+
async importBundle(bundle, opts = {}) {
|
|
7793
|
+
const existing = await this.store.findByLineageId(bundle.session.lineageId);
|
|
7794
|
+
if (existing) {
|
|
7795
|
+
if (!opts.replace) {
|
|
7796
|
+
const err = new Error(
|
|
7797
|
+
`bundle already imported as ${existing.sessionId}`
|
|
7798
|
+
);
|
|
7799
|
+
err.code = JsonRpcErrorCodes.BundleAlreadyImported;
|
|
7800
|
+
err.existingSessionId = existing.sessionId;
|
|
7801
|
+
throw err;
|
|
7802
|
+
}
|
|
7803
|
+
const live = this.sessions.get(existing.sessionId);
|
|
7804
|
+
if (live) {
|
|
7805
|
+
await live.close({ deleteRecord: false }).catch(() => void 0);
|
|
7806
|
+
}
|
|
7807
|
+
await this.writeImportedRecord({
|
|
7808
|
+
sessionId: existing.sessionId,
|
|
7809
|
+
bundle,
|
|
7810
|
+
preservedCreatedAt: existing.createdAt
|
|
7811
|
+
});
|
|
7812
|
+
return {
|
|
7813
|
+
sessionId: existing.sessionId,
|
|
7814
|
+
importedFromSessionId: bundle.session.sessionId,
|
|
7815
|
+
replaced: true
|
|
7816
|
+
};
|
|
7817
|
+
}
|
|
7818
|
+
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
7819
|
+
await this.writeImportedRecord({ sessionId: newId, bundle });
|
|
7820
|
+
return {
|
|
7821
|
+
sessionId: newId,
|
|
7822
|
+
importedFromSessionId: bundle.session.sessionId,
|
|
7823
|
+
replaced: false
|
|
7824
|
+
};
|
|
7825
|
+
}
|
|
7826
|
+
// Write the imported bundle's history.jsonl, prompt-history (if
|
|
7827
|
+
// present), and meta.json. upstreamSessionId is left empty as the
|
|
7828
|
+
// marker that the first attach should bootstrap a fresh agent and
|
|
7829
|
+
// run seedFromImport rather than calling session/load.
|
|
7830
|
+
async writeImportedRecord(args) {
|
|
7831
|
+
await this.histories.rewrite(
|
|
7832
|
+
args.sessionId,
|
|
7833
|
+
args.bundle.history
|
|
7834
|
+
);
|
|
7835
|
+
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
7836
|
+
await saveHistory(
|
|
7837
|
+
paths.tuiHistoryFile(args.sessionId),
|
|
7838
|
+
args.bundle.promptHistory
|
|
7839
|
+
).catch(() => void 0);
|
|
7840
|
+
}
|
|
7841
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7842
|
+
await this.enqueueMetaWrite(args.sessionId, async () => {
|
|
7843
|
+
await this.store.write({
|
|
7844
|
+
sessionId: args.sessionId,
|
|
7845
|
+
lineageId: args.bundle.session.lineageId,
|
|
7846
|
+
upstreamSessionId: "",
|
|
7847
|
+
importedFromSessionId: args.bundle.session.sessionId,
|
|
7848
|
+
agentId: args.bundle.session.agentId,
|
|
7849
|
+
cwd: args.bundle.session.cwd,
|
|
7850
|
+
title: args.bundle.session.title,
|
|
7851
|
+
currentModel: args.bundle.session.currentModel,
|
|
7852
|
+
currentMode: args.bundle.session.currentMode,
|
|
7853
|
+
agentCommands: args.bundle.session.agentCommands,
|
|
7854
|
+
createdAt: args.preservedCreatedAt ?? now,
|
|
7855
|
+
updatedAt: now
|
|
7856
|
+
});
|
|
7857
|
+
});
|
|
7858
|
+
}
|
|
6514
7859
|
async deleteRecord(sessionId) {
|
|
6515
7860
|
const record = await this.store.read(sessionId);
|
|
6516
7861
|
if (!record) {
|
|
@@ -6519,6 +7864,10 @@ var SessionManager = class {
|
|
|
6519
7864
|
await this.store.delete(sessionId).catch(() => void 0);
|
|
6520
7865
|
return true;
|
|
6521
7866
|
}
|
|
7867
|
+
async hasRecord(sessionId) {
|
|
7868
|
+
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
7869
|
+
return record !== void 0;
|
|
7870
|
+
}
|
|
6522
7871
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
6523
7872
|
// was written at create time; updating it here keeps the session
|
|
6524
7873
|
// record's title in sync with what was broadcast to clients so a
|
|
@@ -6592,14 +7941,76 @@ var SessionManager = class {
|
|
|
6592
7941
|
await Promise.allSettled(sessions.map((s) => s.close()));
|
|
6593
7942
|
this.sessions.clear();
|
|
6594
7943
|
}
|
|
7944
|
+
// Wait for every pending meta.json write to settle. Daemon shutdown
|
|
7945
|
+
// hooks call this so a SIGTERM doesn't kill the process mid-write
|
|
7946
|
+
// and lose a freshly-set title (or model/mode/commands).
|
|
7947
|
+
async flushMetaWrites() {
|
|
7948
|
+
const pending = [...this.metaWriteQueues.values()];
|
|
7949
|
+
if (pending.length === 0) {
|
|
7950
|
+
return;
|
|
7951
|
+
}
|
|
7952
|
+
await Promise.allSettled(pending);
|
|
7953
|
+
}
|
|
6595
7954
|
};
|
|
7955
|
+
function mergeForPersistence(session, existing) {
|
|
7956
|
+
const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
|
|
7957
|
+
if (c.description !== void 0) {
|
|
7958
|
+
return { name: c.name, description: c.description };
|
|
7959
|
+
}
|
|
7960
|
+
return { name: c.name };
|
|
7961
|
+
}) : void 0;
|
|
7962
|
+
const agentCommands = persistedCommands ?? existing?.agentCommands;
|
|
7963
|
+
return recordFromMemorySession({
|
|
7964
|
+
sessionId: session.sessionId,
|
|
7965
|
+
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
7966
|
+
upstreamSessionId: session.upstreamSessionId,
|
|
7967
|
+
importedFromSessionId: existing?.importedFromSessionId,
|
|
7968
|
+
agentId: session.agentId,
|
|
7969
|
+
cwd: session.cwd,
|
|
7970
|
+
title: session.title,
|
|
7971
|
+
agentArgs: session.agentArgs,
|
|
7972
|
+
currentModel: session.currentModel ?? existing?.currentModel,
|
|
7973
|
+
currentMode: session.currentMode ?? existing?.currentMode,
|
|
7974
|
+
agentCommands,
|
|
7975
|
+
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
7976
|
+
});
|
|
7977
|
+
}
|
|
7978
|
+
async function loadPromptHistorySafely(sessionId) {
|
|
7979
|
+
try {
|
|
7980
|
+
const raw = await fs7.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
7981
|
+
const out = [];
|
|
7982
|
+
for (const line of raw.split("\n")) {
|
|
7983
|
+
if (line.length === 0) {
|
|
7984
|
+
continue;
|
|
7985
|
+
}
|
|
7986
|
+
try {
|
|
7987
|
+
const decoded = JSON.parse(line);
|
|
7988
|
+
if (typeof decoded === "string") {
|
|
7989
|
+
out.push(decoded);
|
|
7990
|
+
}
|
|
7991
|
+
} catch {
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
return out;
|
|
7995
|
+
} catch {
|
|
7996
|
+
return [];
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
async function historyMtimeIso(sessionId) {
|
|
8000
|
+
try {
|
|
8001
|
+
const st = await fs7.stat(paths.historyFile(sessionId));
|
|
8002
|
+
return new Date(st.mtimeMs).toISOString();
|
|
8003
|
+
} catch {
|
|
8004
|
+
return void 0;
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
6596
8007
|
|
|
6597
8008
|
// src/core/extensions.ts
|
|
6598
8009
|
init_paths();
|
|
6599
8010
|
import { spawn as spawn2 } from "child_process";
|
|
6600
|
-
import * as
|
|
8011
|
+
import * as fs8 from "fs";
|
|
6601
8012
|
import * as fsp from "fs/promises";
|
|
6602
|
-
import * as
|
|
8013
|
+
import * as path4 from "path";
|
|
6603
8014
|
var RESTART_BASE_MS = 1e3;
|
|
6604
8015
|
var RESTART_CAP_MS = 6e4;
|
|
6605
8016
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -6646,9 +8057,9 @@ var ExtensionManager = class {
|
|
|
6646
8057
|
} catch {
|
|
6647
8058
|
}
|
|
6648
8059
|
tasks.push(
|
|
6649
|
-
new Promise((
|
|
8060
|
+
new Promise((resolve4) => {
|
|
6650
8061
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
6651
|
-
|
|
8062
|
+
resolve4();
|
|
6652
8063
|
return;
|
|
6653
8064
|
}
|
|
6654
8065
|
const timer = setTimeout(() => {
|
|
@@ -6656,11 +8067,11 @@ var ExtensionManager = class {
|
|
|
6656
8067
|
child.kill("SIGKILL");
|
|
6657
8068
|
} catch {
|
|
6658
8069
|
}
|
|
6659
|
-
|
|
8070
|
+
resolve4();
|
|
6660
8071
|
}, STOP_GRACE_MS);
|
|
6661
8072
|
child.on("exit", () => {
|
|
6662
8073
|
clearTimeout(timer);
|
|
6663
|
-
|
|
8074
|
+
resolve4();
|
|
6664
8075
|
});
|
|
6665
8076
|
})
|
|
6666
8077
|
);
|
|
@@ -6768,8 +8179,8 @@ var ExtensionManager = class {
|
|
|
6768
8179
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
6769
8180
|
return;
|
|
6770
8181
|
}
|
|
6771
|
-
const exited = new Promise((
|
|
6772
|
-
entry.exitWaiters.push(
|
|
8182
|
+
const exited = new Promise((resolve4) => {
|
|
8183
|
+
entry.exitWaiters.push(resolve4);
|
|
6773
8184
|
});
|
|
6774
8185
|
try {
|
|
6775
8186
|
child.kill("SIGTERM");
|
|
@@ -6841,7 +8252,7 @@ var ExtensionManager = class {
|
|
|
6841
8252
|
if (!entry.endsWith(".pid")) {
|
|
6842
8253
|
continue;
|
|
6843
8254
|
}
|
|
6844
|
-
const pidPath =
|
|
8255
|
+
const pidPath = path4.join(paths.extensionsDir(), entry);
|
|
6845
8256
|
let pid;
|
|
6846
8257
|
try {
|
|
6847
8258
|
const raw = await fsp.readFile(pidPath, "utf8");
|
|
@@ -6880,7 +8291,7 @@ var ExtensionManager = class {
|
|
|
6880
8291
|
}
|
|
6881
8292
|
const ext = entry.config;
|
|
6882
8293
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
6883
|
-
const logStream =
|
|
8294
|
+
const logStream = fs8.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
6884
8295
|
flags: "a"
|
|
6885
8296
|
});
|
|
6886
8297
|
logStream.write(
|
|
@@ -6930,7 +8341,7 @@ var ExtensionManager = class {
|
|
|
6930
8341
|
}
|
|
6931
8342
|
if (typeof child.pid === "number") {
|
|
6932
8343
|
try {
|
|
6933
|
-
|
|
8344
|
+
fs8.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
6934
8345
|
`, {
|
|
6935
8346
|
encoding: "utf8",
|
|
6936
8347
|
mode: 384
|
|
@@ -6955,7 +8366,7 @@ var ExtensionManager = class {
|
|
|
6955
8366
|
});
|
|
6956
8367
|
child.on("exit", (code, signal) => {
|
|
6957
8368
|
try {
|
|
6958
|
-
|
|
8369
|
+
fs8.unlinkSync(paths.extensionPidFile(ext.name));
|
|
6959
8370
|
} catch {
|
|
6960
8371
|
}
|
|
6961
8372
|
logStream.write(
|
|
@@ -6966,8 +8377,8 @@ var ExtensionManager = class {
|
|
|
6966
8377
|
entry.pid = void 0;
|
|
6967
8378
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
6968
8379
|
const waiters = entry.exitWaiters.splice(0);
|
|
6969
|
-
for (const
|
|
6970
|
-
|
|
8380
|
+
for (const resolve4 of waiters) {
|
|
8381
|
+
resolve4();
|
|
6971
8382
|
}
|
|
6972
8383
|
if (this.stopping || entry.manuallyStopped) {
|
|
6973
8384
|
try {
|
|
@@ -7068,6 +8479,76 @@ function constantTimeEqual(a, b) {
|
|
|
7068
8479
|
|
|
7069
8480
|
// src/daemon/routes/sessions.ts
|
|
7070
8481
|
init_config();
|
|
8482
|
+
import * as os2 from "os";
|
|
8483
|
+
|
|
8484
|
+
// src/core/bundle.ts
|
|
8485
|
+
import { z as z5 } from "zod";
|
|
8486
|
+
var HistoryEntrySchema = z5.object({
|
|
8487
|
+
method: z5.string(),
|
|
8488
|
+
params: z5.unknown(),
|
|
8489
|
+
recordedAt: z5.number()
|
|
8490
|
+
});
|
|
8491
|
+
var BundleSession = z5.object({
|
|
8492
|
+
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
8493
|
+
// the local namespace; lineageId is what survives across hops).
|
|
8494
|
+
sessionId: z5.string(),
|
|
8495
|
+
// Required on bundles — the export path backfills if the source
|
|
8496
|
+
// record was written before lineageId existed.
|
|
8497
|
+
lineageId: z5.string(),
|
|
8498
|
+
agentId: z5.string(),
|
|
8499
|
+
cwd: z5.string(),
|
|
8500
|
+
title: z5.string().optional(),
|
|
8501
|
+
currentModel: z5.string().optional(),
|
|
8502
|
+
currentMode: z5.string().optional(),
|
|
8503
|
+
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
8504
|
+
createdAt: z5.string(),
|
|
8505
|
+
updatedAt: z5.string()
|
|
8506
|
+
});
|
|
8507
|
+
var Bundle = z5.object({
|
|
8508
|
+
version: z5.literal(1),
|
|
8509
|
+
exportedAt: z5.string(),
|
|
8510
|
+
exportedFrom: z5.object({
|
|
8511
|
+
hydraVersion: z5.string(),
|
|
8512
|
+
machine: z5.string()
|
|
8513
|
+
}),
|
|
8514
|
+
session: BundleSession,
|
|
8515
|
+
history: z5.array(HistoryEntrySchema),
|
|
8516
|
+
promptHistory: z5.array(z5.string()).optional()
|
|
8517
|
+
});
|
|
8518
|
+
function encodeBundle(params) {
|
|
8519
|
+
const bundle = {
|
|
8520
|
+
version: 1,
|
|
8521
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8522
|
+
exportedFrom: {
|
|
8523
|
+
hydraVersion: params.hydraVersion,
|
|
8524
|
+
machine: params.machine
|
|
8525
|
+
},
|
|
8526
|
+
session: {
|
|
8527
|
+
sessionId: params.record.sessionId,
|
|
8528
|
+
lineageId: params.record.lineageId,
|
|
8529
|
+
agentId: params.record.agentId,
|
|
8530
|
+
cwd: params.record.cwd,
|
|
8531
|
+
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
8532
|
+
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
8533
|
+
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
8534
|
+
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
8535
|
+
createdAt: params.record.createdAt,
|
|
8536
|
+
updatedAt: params.record.updatedAt
|
|
8537
|
+
},
|
|
8538
|
+
history: params.history
|
|
8539
|
+
};
|
|
8540
|
+
if (params.promptHistory !== void 0) {
|
|
8541
|
+
bundle.promptHistory = params.promptHistory;
|
|
8542
|
+
}
|
|
8543
|
+
return bundle;
|
|
8544
|
+
}
|
|
8545
|
+
function decodeBundle(raw) {
|
|
8546
|
+
return Bundle.parse(raw);
|
|
8547
|
+
}
|
|
8548
|
+
|
|
8549
|
+
// src/daemon/routes/sessions.ts
|
|
8550
|
+
init_types();
|
|
8551
|
+
var HYDRA_VERSION = "0.1.0";
|
|
7071
8552
|
function registerSessionRoutes(app, manager, defaults) {
|
|
7072
8553
|
app.get("/v1/sessions", async (request) => {
|
|
7073
8554
|
const query = request.query;
|
|
@@ -7093,6 +8574,22 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7093
8574
|
reply.code(500).send({ error: err.message });
|
|
7094
8575
|
}
|
|
7095
8576
|
});
|
|
8577
|
+
app.post("/v1/sessions/:id/kill", async (request, reply) => {
|
|
8578
|
+
const raw = request.params.id;
|
|
8579
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
8580
|
+
const session = manager.get(id);
|
|
8581
|
+
if (session) {
|
|
8582
|
+
await session.close({ deleteRecord: false });
|
|
8583
|
+
reply.code(204).send();
|
|
8584
|
+
return;
|
|
8585
|
+
}
|
|
8586
|
+
const exists = await manager.hasRecord(id);
|
|
8587
|
+
if (!exists) {
|
|
8588
|
+
reply.code(404).send({ error: "session not found" });
|
|
8589
|
+
return;
|
|
8590
|
+
}
|
|
8591
|
+
reply.code(204).send();
|
|
8592
|
+
});
|
|
7096
8593
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
7097
8594
|
const raw = request.params.id;
|
|
7098
8595
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -7109,6 +8606,61 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7109
8606
|
}
|
|
7110
8607
|
reply.code(204).send();
|
|
7111
8608
|
});
|
|
8609
|
+
app.get("/v1/sessions/:id/export", async (request, reply) => {
|
|
8610
|
+
const raw = request.params.id;
|
|
8611
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
8612
|
+
const exported = await manager.exportBundle(id);
|
|
8613
|
+
if (!exported) {
|
|
8614
|
+
reply.code(404).send({ error: "session not found" });
|
|
8615
|
+
return;
|
|
8616
|
+
}
|
|
8617
|
+
const bundle = encodeBundle({
|
|
8618
|
+
record: exported.record,
|
|
8619
|
+
history: exported.history,
|
|
8620
|
+
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
8621
|
+
hydraVersion: HYDRA_VERSION,
|
|
8622
|
+
machine: os2.hostname()
|
|
8623
|
+
});
|
|
8624
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
8625
|
+
reply.header(
|
|
8626
|
+
"Content-Disposition",
|
|
8627
|
+
`attachment; filename="hydra-${id}-${stamp}.hydra"`
|
|
8628
|
+
);
|
|
8629
|
+
reply.code(200).send(bundle);
|
|
8630
|
+
});
|
|
8631
|
+
app.post("/v1/sessions/import", async (request, reply) => {
|
|
8632
|
+
const body = request.body ?? {};
|
|
8633
|
+
if (body.bundle === void 0) {
|
|
8634
|
+
reply.code(400).send({ error: "missing bundle" });
|
|
8635
|
+
return;
|
|
8636
|
+
}
|
|
8637
|
+
let bundle;
|
|
8638
|
+
try {
|
|
8639
|
+
bundle = decodeBundle(body.bundle);
|
|
8640
|
+
} catch (err) {
|
|
8641
|
+
reply.code(400).send({
|
|
8642
|
+
error: "invalid bundle",
|
|
8643
|
+
details: err.message
|
|
8644
|
+
});
|
|
8645
|
+
return;
|
|
8646
|
+
}
|
|
8647
|
+
try {
|
|
8648
|
+
const result = await manager.importBundle(bundle, {
|
|
8649
|
+
replace: body.replace === true
|
|
8650
|
+
});
|
|
8651
|
+
reply.code(201).send(result);
|
|
8652
|
+
} catch (err) {
|
|
8653
|
+
const e = err;
|
|
8654
|
+
if (e.code === JsonRpcErrorCodes.BundleAlreadyImported) {
|
|
8655
|
+
reply.code(409).send({
|
|
8656
|
+
error: "bundle already imported",
|
|
8657
|
+
existingSessionId: e.existingSessionId
|
|
8658
|
+
});
|
|
8659
|
+
return;
|
|
8660
|
+
}
|
|
8661
|
+
reply.code(500).send({ error: e.message });
|
|
8662
|
+
}
|
|
8663
|
+
});
|
|
7112
8664
|
app.get("/v1/sessions/:id/history", async (request, reply) => {
|
|
7113
8665
|
const raw = request.params.id;
|
|
7114
8666
|
const query = request.query;
|
|
@@ -7117,16 +8669,22 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7117
8669
|
const live = manager.get(id);
|
|
7118
8670
|
let snapshot;
|
|
7119
8671
|
let unsubscribe;
|
|
8672
|
+
let snapshotDone = false;
|
|
8673
|
+
const pending = [];
|
|
7120
8674
|
if (live) {
|
|
7121
|
-
snapshot = live.getHistorySnapshot();
|
|
7122
8675
|
if (follow) {
|
|
7123
8676
|
unsubscribe = live.onBroadcast((entry) => {
|
|
7124
8677
|
if (reply.raw.writableEnded) {
|
|
7125
8678
|
return;
|
|
7126
8679
|
}
|
|
7127
|
-
|
|
8680
|
+
if (snapshotDone) {
|
|
8681
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
8682
|
+
} else {
|
|
8683
|
+
pending.push(entry);
|
|
8684
|
+
}
|
|
7128
8685
|
});
|
|
7129
8686
|
}
|
|
8687
|
+
snapshot = await live.getHistorySnapshot();
|
|
7130
8688
|
} else {
|
|
7131
8689
|
const cold = await manager.getHistory(id);
|
|
7132
8690
|
if (cold === void 0) {
|
|
@@ -7138,9 +8696,23 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7138
8696
|
reply.raw.setHeader("Content-Type", "application/x-ndjson");
|
|
7139
8697
|
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
7140
8698
|
reply.raw.statusCode = 200;
|
|
8699
|
+
const snapshotKeys = /* @__PURE__ */ new Set();
|
|
7141
8700
|
for (const entry of snapshot ?? []) {
|
|
7142
8701
|
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
8702
|
+
const e = entry;
|
|
8703
|
+
if (typeof e.recordedAt === "number") {
|
|
8704
|
+
snapshotKeys.add(String(e.recordedAt));
|
|
8705
|
+
}
|
|
8706
|
+
}
|
|
8707
|
+
for (const entry of pending) {
|
|
8708
|
+
const e = entry;
|
|
8709
|
+
const key = typeof e.recordedAt === "number" ? String(e.recordedAt) : "";
|
|
8710
|
+
if (key && snapshotKeys.has(key)) {
|
|
8711
|
+
continue;
|
|
8712
|
+
}
|
|
8713
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
7143
8714
|
}
|
|
8715
|
+
snapshotDone = true;
|
|
7144
8716
|
if (!unsubscribe) {
|
|
7145
8717
|
reply.raw.end();
|
|
7146
8718
|
return reply;
|
|
@@ -7300,12 +8872,22 @@ function parseRegisterBody(body) {
|
|
|
7300
8872
|
};
|
|
7301
8873
|
}
|
|
7302
8874
|
|
|
8875
|
+
// src/daemon/routes/config.ts
|
|
8876
|
+
function registerConfigRoutes(app, defaults) {
|
|
8877
|
+
app.get("/v1/config", async () => {
|
|
8878
|
+
return {
|
|
8879
|
+
defaultAgent: defaults.defaultAgent,
|
|
8880
|
+
defaultCwd: defaults.defaultCwd
|
|
8881
|
+
};
|
|
8882
|
+
});
|
|
8883
|
+
}
|
|
8884
|
+
|
|
7303
8885
|
// src/daemon/acp-ws.ts
|
|
7304
8886
|
init_connection();
|
|
7305
8887
|
init_ws_stream();
|
|
7306
8888
|
init_types();
|
|
7307
8889
|
import { nanoid as nanoid2 } from "nanoid";
|
|
7308
|
-
var
|
|
8890
|
+
var HYDRA_VERSION2 = "0.1.0";
|
|
7309
8891
|
var HYDRA_PROTOCOL_VERSION = 1;
|
|
7310
8892
|
function registerAcpWsEndpoint(app, deps) {
|
|
7311
8893
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
@@ -7351,7 +8933,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7351
8933
|
agentArgs: hydraMeta.agentArgs
|
|
7352
8934
|
});
|
|
7353
8935
|
const client = bindClientToSession(connection, session, state);
|
|
7354
|
-
session.attach(client, "full");
|
|
8936
|
+
await session.attach(client, "full");
|
|
7355
8937
|
state.attached.set(session.sessionId, {
|
|
7356
8938
|
sessionId: session.sessionId,
|
|
7357
8939
|
clientId: client.clientId
|
|
@@ -7370,14 +8952,22 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7370
8952
|
const lookupId = hydraHints ? params.sessionId : await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
7371
8953
|
let session = deps.manager.get(lookupId);
|
|
7372
8954
|
if (!session) {
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
8955
|
+
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
8956
|
+
let resurrectParams = fromDisk;
|
|
8957
|
+
if (hydraHints) {
|
|
8958
|
+
resurrectParams = {
|
|
8959
|
+
hydraSessionId: params.sessionId,
|
|
8960
|
+
upstreamSessionId: hydraHints.upstreamSessionId,
|
|
8961
|
+
agentId: hydraHints.agentId,
|
|
8962
|
+
cwd: hydraHints.cwd,
|
|
8963
|
+
title: hydraHints.title ?? fromDisk?.title,
|
|
8964
|
+
agentArgs: hydraHints.agentArgs ?? fromDisk?.agentArgs,
|
|
8965
|
+
currentModel: fromDisk?.currentModel,
|
|
8966
|
+
currentMode: fromDisk?.currentMode,
|
|
8967
|
+
agentCommands: fromDisk?.agentCommands,
|
|
8968
|
+
createdAt: fromDisk?.createdAt
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
7381
8971
|
if (!resurrectParams) {
|
|
7382
8972
|
const err = new Error(
|
|
7383
8973
|
`session ${params.sessionId} not found and no resume hints provided`
|
|
@@ -7393,13 +8983,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7393
8983
|
state,
|
|
7394
8984
|
params.clientInfo
|
|
7395
8985
|
);
|
|
7396
|
-
const replay = session.attach(client, params.historyPolicy);
|
|
8986
|
+
const replay = await session.attach(client, params.historyPolicy);
|
|
7397
8987
|
state.attached.set(session.sessionId, {
|
|
7398
8988
|
sessionId: session.sessionId,
|
|
7399
8989
|
clientId: client.clientId
|
|
7400
8990
|
});
|
|
7401
8991
|
app.log.info(
|
|
7402
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size}`
|
|
8992
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} replayed=${replay.length}`
|
|
7403
8993
|
);
|
|
7404
8994
|
for (const note of replay) {
|
|
7405
8995
|
await connection.notify(note.method, note.params);
|
|
@@ -7495,7 +9085,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7495
9085
|
session = await deps.manager.resurrect(fromDisk);
|
|
7496
9086
|
}
|
|
7497
9087
|
const client = bindClientToSession(connection, session, state);
|
|
7498
|
-
const replay = session.attach(client, "pending_only");
|
|
9088
|
+
const replay = await session.attach(client, "pending_only");
|
|
7499
9089
|
state.attached.set(session.sessionId, {
|
|
7500
9090
|
sessionId: session.sessionId,
|
|
7501
9091
|
clientId: client.clientId
|
|
@@ -7553,12 +9143,15 @@ function buildResponseMeta(session) {
|
|
|
7553
9143
|
if (commands.length > 0) {
|
|
7554
9144
|
ours.availableCommands = commands;
|
|
7555
9145
|
}
|
|
9146
|
+
if (session.turnStartedAt !== void 0) {
|
|
9147
|
+
ours.turnStartedAt = session.turnStartedAt;
|
|
9148
|
+
}
|
|
7556
9149
|
return mergeMeta(session.agentMeta, ours);
|
|
7557
9150
|
}
|
|
7558
9151
|
function buildInitializeResult() {
|
|
7559
9152
|
return {
|
|
7560
9153
|
protocolVersion: HYDRA_PROTOCOL_VERSION,
|
|
7561
|
-
agentInfo: { name: "hydra", version:
|
|
9154
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION2 },
|
|
7562
9155
|
agentCapabilities: {
|
|
7563
9156
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
7564
9157
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -7597,7 +9190,7 @@ function bindClientToSession(connection, session, state, clientInfo) {
|
|
|
7597
9190
|
}
|
|
7598
9191
|
|
|
7599
9192
|
// src/daemon/server.ts
|
|
7600
|
-
var
|
|
9193
|
+
var HYDRA_VERSION3 = "0.1.0";
|
|
7601
9194
|
async function startDaemon(config) {
|
|
7602
9195
|
ensureLoopbackOrTls(config);
|
|
7603
9196
|
const httpsOptions = config.daemon.tls ? {
|
|
@@ -7631,13 +9224,17 @@ async function startDaemon(config) {
|
|
|
7631
9224
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3
|
|
7632
9225
|
});
|
|
7633
9226
|
const extensions = new ExtensionManager(extensionList(config));
|
|
7634
|
-
registerHealthRoutes(app,
|
|
9227
|
+
registerHealthRoutes(app, HYDRA_VERSION3);
|
|
7635
9228
|
registerSessionRoutes(app, manager, {
|
|
7636
9229
|
agentId: config.defaultAgent,
|
|
7637
9230
|
cwd: config.defaultCwd
|
|
7638
9231
|
});
|
|
7639
9232
|
registerAgentRoutes(app, registry);
|
|
7640
9233
|
registerExtensionRoutes(app, extensions);
|
|
9234
|
+
registerConfigRoutes(app, {
|
|
9235
|
+
defaultAgent: config.defaultAgent,
|
|
9236
|
+
defaultCwd: config.defaultCwd
|
|
9237
|
+
});
|
|
7641
9238
|
registerAcpWsEndpoint(app, {
|
|
7642
9239
|
config,
|
|
7643
9240
|
manager,
|
|
@@ -7671,9 +9268,10 @@ async function startDaemon(config) {
|
|
|
7671
9268
|
const shutdown = async () => {
|
|
7672
9269
|
await extensions.stop();
|
|
7673
9270
|
await manager.closeAll();
|
|
9271
|
+
await manager.flushMetaWrites();
|
|
7674
9272
|
await app.close();
|
|
7675
9273
|
try {
|
|
7676
|
-
|
|
9274
|
+
fs9.unlinkSync(paths.pidFile());
|
|
7677
9275
|
} catch {
|
|
7678
9276
|
}
|
|
7679
9277
|
try {
|
|
@@ -7712,13 +9310,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
7712
9310
|
init_daemon_bootstrap();
|
|
7713
9311
|
|
|
7714
9312
|
// src/cli/commands/log-tail.ts
|
|
7715
|
-
import * as
|
|
9313
|
+
import * as fs10 from "fs";
|
|
7716
9314
|
import * as fsp3 from "fs/promises";
|
|
7717
9315
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
7718
9316
|
const opts = parseLogTailFlags(argv);
|
|
7719
|
-
let
|
|
9317
|
+
let stat3;
|
|
7720
9318
|
try {
|
|
7721
|
-
|
|
9319
|
+
stat3 = await fsp3.stat(logPath);
|
|
7722
9320
|
} catch (err) {
|
|
7723
9321
|
const e = err;
|
|
7724
9322
|
if (e.code === "ENOENT") {
|
|
@@ -7729,14 +9327,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
7729
9327
|
}
|
|
7730
9328
|
throw err;
|
|
7731
9329
|
}
|
|
7732
|
-
let position = await printTail(logPath,
|
|
9330
|
+
let position = await printTail(logPath, stat3.size, opts.tail);
|
|
7733
9331
|
if (!opts.follow) {
|
|
7734
9332
|
return;
|
|
7735
9333
|
}
|
|
7736
9334
|
process.stdout.write(`-- following ${logPath} --
|
|
7737
9335
|
`);
|
|
7738
9336
|
let pending = false;
|
|
7739
|
-
const watcher =
|
|
9337
|
+
const watcher = fs10.watch(logPath, () => {
|
|
7740
9338
|
if (pending) {
|
|
7741
9339
|
return;
|
|
7742
9340
|
}
|
|
@@ -7764,10 +9362,10 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
7764
9362
|
}
|
|
7765
9363
|
});
|
|
7766
9364
|
});
|
|
7767
|
-
await new Promise((
|
|
9365
|
+
await new Promise((resolve4) => {
|
|
7768
9366
|
const finish = () => {
|
|
7769
9367
|
watcher.close();
|
|
7770
|
-
|
|
9368
|
+
resolve4();
|
|
7771
9369
|
};
|
|
7772
9370
|
process.once("SIGINT", finish);
|
|
7773
9371
|
process.once("SIGTERM", finish);
|
|
@@ -8839,14 +10437,16 @@ async function main() {
|
|
|
8839
10437
|
process.exit(2);
|
|
8840
10438
|
return;
|
|
8841
10439
|
}
|
|
8842
|
-
const
|
|
10440
|
+
const launchResume = flags2.resume;
|
|
10441
|
+
const sessionId2 = typeof launchResume === "string" ? launchResume : resolveOption(flags2, "session-id");
|
|
8843
10442
|
const name2 = resolveOption(flags2, "name");
|
|
8844
10443
|
await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2 });
|
|
8845
10444
|
return;
|
|
8846
10445
|
}
|
|
8847
10446
|
const { positional, flags } = parseArgs(argv);
|
|
8848
10447
|
if (flags.version === true || positional[0] === "--version") {
|
|
8849
|
-
process.stdout.write(
|
|
10448
|
+
process.stdout.write(`hydra-acp ${readVersion()}
|
|
10449
|
+
`);
|
|
8850
10450
|
return;
|
|
8851
10451
|
}
|
|
8852
10452
|
if (flags.help === true) {
|
|
@@ -8854,7 +10454,8 @@ async function main() {
|
|
|
8854
10454
|
return;
|
|
8855
10455
|
}
|
|
8856
10456
|
const subcommand = positional[0];
|
|
8857
|
-
const
|
|
10457
|
+
const resumeFlag = flags.resume;
|
|
10458
|
+
const sessionId = typeof resumeFlag === "string" ? resumeFlag : resolveOption(flags, "session-id");
|
|
8858
10459
|
const name = resolveOption(flags, "name");
|
|
8859
10460
|
const agentIdFromFlag = resolveOption(flags, "agent-id");
|
|
8860
10461
|
if (!subcommand) {
|
|
@@ -8915,6 +10516,21 @@ async function main() {
|
|
|
8915
10516
|
await runSessionsKill(positional[2]);
|
|
8916
10517
|
return;
|
|
8917
10518
|
}
|
|
10519
|
+
if (sub === "rm") {
|
|
10520
|
+
await runSessionsRm(positional[2]);
|
|
10521
|
+
return;
|
|
10522
|
+
}
|
|
10523
|
+
if (sub === "export") {
|
|
10524
|
+
const out = resolveOption(flags, "out");
|
|
10525
|
+
await runSessionsExport(positional[2], out);
|
|
10526
|
+
return;
|
|
10527
|
+
}
|
|
10528
|
+
if (sub === "import") {
|
|
10529
|
+
await runSessionsImport(positional[2], {
|
|
10530
|
+
replace: flags.replace === true
|
|
10531
|
+
});
|
|
10532
|
+
return;
|
|
10533
|
+
}
|
|
8918
10534
|
process.stderr.write(`Unknown sessions subcommand: ${sub}
|
|
8919
10535
|
`);
|
|
8920
10536
|
process.exit(2);
|
|
@@ -9008,6 +10624,17 @@ async function dispatchTui(flags, base) {
|
|
|
9008
10624
|
}
|
|
9009
10625
|
await runTui(tuiOpts);
|
|
9010
10626
|
}
|
|
10627
|
+
function readVersion() {
|
|
10628
|
+
try {
|
|
10629
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
10630
|
+
const pkg = JSON.parse(
|
|
10631
|
+
readFileSync(resolve3(here, "../package.json"), "utf8")
|
|
10632
|
+
);
|
|
10633
|
+
return pkg.version ?? "unknown";
|
|
10634
|
+
} catch {
|
|
10635
|
+
return "unknown";
|
|
10636
|
+
}
|
|
10637
|
+
}
|
|
9011
10638
|
function printHelp() {
|
|
9012
10639
|
process.stdout.write(
|
|
9013
10640
|
[
|
|
@@ -9021,12 +10648,17 @@ function printHelp() {
|
|
|
9021
10648
|
" Shim mode, force daemon to spawn <agent-id>",
|
|
9022
10649
|
" from the registry. Args after <agent-id>",
|
|
9023
10650
|
" are forwarded to the agent's command.",
|
|
9024
|
-
" hydra-acp --
|
|
10651
|
+
" hydra-acp --resume <id> Attach to an existing session (TUI when in a terminal, shim otherwise)",
|
|
9025
10652
|
" hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
|
|
9026
10653
|
" hydra-acp daemon start|stop|restart|status",
|
|
9027
10654
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
9028
10655
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
9029
|
-
" hydra-acp sessions kill <id>
|
|
10656
|
+
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
10657
|
+
" hydra-acp sessions rm <id> Remove a session entirely (live or cold)",
|
|
10658
|
+
" hydra-acp sessions export <id> [--out <file>|.]",
|
|
10659
|
+
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
10660
|
+
" hydra-acp sessions import <file>|- [--replace]",
|
|
10661
|
+
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live)",
|
|
9030
10662
|
" hydra-acp extensions list List configured extensions and live state",
|
|
9031
10663
|
" hydra-acp extensions add <name> [opts] Add an extension to config",
|
|
9032
10664
|
" hydra-acp extensions remove <name> Remove an extension from config",
|
|
@@ -9034,15 +10666,16 @@ function printHelp() {
|
|
|
9034
10666
|
" hydra-acp extensions logs <name> [-f] [-n N]Tail or follow an extension's log",
|
|
9035
10667
|
" hydra-acp agents [list] List agents in the cached registry",
|
|
9036
10668
|
" hydra-acp agents refresh Force a registry re-fetch",
|
|
9037
|
-
" hydra-acp tui flags: [--
|
|
9038
|
-
"
|
|
10669
|
+
" hydra-acp tui flags: [--resume [<id>]] [--new] [--agent-id <id>] [--cwd <path>] [--name <label>]",
|
|
10670
|
+
" --resume <id> attaches to a specific session; bare --resume picks the most-recent",
|
|
10671
|
+
" in cwd. Smart default (no flags): picks if any live sessions exist, else new.",
|
|
9039
10672
|
" hydra-acp --version Print version",
|
|
9040
10673
|
" hydra-acp --help Show this help",
|
|
9041
10674
|
"",
|
|
9042
10675
|
"Config knob flags accept env-var equivalents (flag wins):",
|
|
9043
|
-
" --agent-id
|
|
9044
|
-
" --session-id
|
|
9045
|
-
" --name
|
|
10676
|
+
" --agent-id HYDRA_ACP_AGENT_ID",
|
|
10677
|
+
" --resume / --session-id HYDRA_ACP_SESSION_ID",
|
|
10678
|
+
" --name HYDRA_ACP_NAME",
|
|
9046
10679
|
""
|
|
9047
10680
|
].join("\n")
|
|
9048
10681
|
);
|