@hydra-acp/cli 0.1.6 → 0.1.8
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 +3 -1
- package/dist/cli.js +2185 -634
- package/dist/index.d.ts +68 -5
- package/dist/index.js +471 -106
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,19 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/core/paths.ts
|
|
13
13
|
import * as path from "path";
|
|
14
14
|
import * as os from "os";
|
|
15
|
+
function shortenHomePath(p) {
|
|
16
|
+
const home = os.homedir();
|
|
17
|
+
if (!home) {
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
if (p === home) {
|
|
21
|
+
return "~";
|
|
22
|
+
}
|
|
23
|
+
if (p.startsWith(home + "/")) {
|
|
24
|
+
return "~" + p.slice(home.length);
|
|
25
|
+
}
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
15
28
|
function hydraHome() {
|
|
16
29
|
const override = process.env[ROOT_ENV];
|
|
17
30
|
if (override && override.length > 0) {
|
|
@@ -32,6 +45,9 @@ var init_paths = __esm({
|
|
|
32
45
|
paths = {
|
|
33
46
|
home: hydraHome,
|
|
34
47
|
config: () => path.join(hydraHome(), "config.json"),
|
|
48
|
+
// Auth token lives in its own file so config.json can be version-
|
|
49
|
+
// controlled without leaking the secret. Raw string contents, mode 0600.
|
|
50
|
+
authToken: () => path.join(hydraHome(), "auth-token"),
|
|
35
51
|
pidFile: () => path.join(hydraHome(), "daemon.pid"),
|
|
36
52
|
logFile: () => path.join(hydraHome(), "daemon.log"),
|
|
37
53
|
currentLogFile: () => path.join(hydraHome(), "current.log"),
|
|
@@ -42,6 +58,18 @@ var init_paths = __esm({
|
|
|
42
58
|
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
43
59
|
// shows which platforms have ever installed anything.
|
|
44
60
|
agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
|
|
61
|
+
// npm install cache for npx-distributed agents. The trailing
|
|
62
|
+
// node<ABI> segment keys on process.versions.modules so a Node
|
|
63
|
+
// major bump (different ABI → native modules incompatible) yields
|
|
64
|
+
// a fresh install rather than failing at require() time.
|
|
65
|
+
agentNpmInstallDir: (id, platformKey, version) => path.join(
|
|
66
|
+
hydraHome(),
|
|
67
|
+
"agents",
|
|
68
|
+
platformKey,
|
|
69
|
+
id,
|
|
70
|
+
version,
|
|
71
|
+
`node${process.versions.modules}`
|
|
72
|
+
),
|
|
45
73
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
46
74
|
// One directory per session id under sessions/. Co-locates the
|
|
47
75
|
// session record, its transcript, and any future per-session state
|
|
@@ -68,61 +96,98 @@ function extensionList(config) {
|
|
|
68
96
|
...body
|
|
69
97
|
}));
|
|
70
98
|
}
|
|
71
|
-
async function
|
|
72
|
-
const configPath = paths.config();
|
|
99
|
+
async function readConfigFile() {
|
|
73
100
|
let raw;
|
|
74
101
|
try {
|
|
75
|
-
raw = await fs.readFile(
|
|
102
|
+
raw = await fs.readFile(paths.config(), "utf8");
|
|
76
103
|
} catch (err) {
|
|
77
104
|
const e = err;
|
|
78
105
|
if (e.code === "ENOENT") {
|
|
79
|
-
|
|
80
|
-
`No config found at ${configPath}. Run \`hydra-acp init\` to create one.`
|
|
81
|
-
);
|
|
106
|
+
return {};
|
|
82
107
|
}
|
|
83
108
|
throw err;
|
|
84
109
|
}
|
|
85
|
-
|
|
86
|
-
return HydraConfig.parse(parsed);
|
|
110
|
+
return JSON.parse(raw);
|
|
87
111
|
}
|
|
88
|
-
async function
|
|
112
|
+
async function loadAuthToken() {
|
|
113
|
+
let tokenFile;
|
|
89
114
|
try {
|
|
90
|
-
await fs.
|
|
115
|
+
const text = await fs.readFile(paths.authToken(), "utf8");
|
|
116
|
+
const trimmed = text.trim();
|
|
117
|
+
if (trimmed.length > 0) {
|
|
118
|
+
tokenFile = trimmed;
|
|
119
|
+
}
|
|
91
120
|
} catch (err) {
|
|
92
121
|
const e = err;
|
|
93
122
|
if (e.code !== "ENOENT") {
|
|
94
123
|
throw err;
|
|
95
124
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
}
|
|
126
|
+
const raw = await readConfigFile();
|
|
127
|
+
const daemon = raw.daemon;
|
|
128
|
+
const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
|
|
129
|
+
if (tokenFile && legacy) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Auth token present in both ${paths.authToken()} and ${paths.config()} (daemon.authToken). Remove daemon.authToken from config.json to resolve.`
|
|
100
132
|
);
|
|
101
|
-
return config;
|
|
102
133
|
}
|
|
103
|
-
|
|
134
|
+
if (tokenFile) {
|
|
135
|
+
return tokenFile;
|
|
136
|
+
}
|
|
137
|
+
if (legacy) {
|
|
138
|
+
await migrateLegacyAuthToken(raw, daemon, legacy);
|
|
139
|
+
return legacy;
|
|
140
|
+
}
|
|
141
|
+
return void 0;
|
|
104
142
|
}
|
|
105
|
-
async function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
async function migrateLegacyAuthToken(raw, daemon, token) {
|
|
144
|
+
await writeAuthToken(token);
|
|
145
|
+
delete daemon.authToken;
|
|
146
|
+
if (Object.keys(daemon).length === 0) {
|
|
147
|
+
delete raw.daemon;
|
|
148
|
+
}
|
|
149
|
+
await fs.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
|
|
110
150
|
encoding: "utf8",
|
|
111
151
|
mode: 384
|
|
112
152
|
});
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
await fs.writeFile(path7, JSON.stringify(raw, null, 2) + "\n", {
|
|
153
|
+
process.stderr.write(
|
|
154
|
+
`hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
|
|
155
|
+
`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
async function writeAuthToken(token) {
|
|
159
|
+
await fs.mkdir(paths.home(), { recursive: true });
|
|
160
|
+
await fs.writeFile(paths.authToken(), token + "\n", {
|
|
122
161
|
encoding: "utf8",
|
|
123
162
|
mode: 384
|
|
124
163
|
});
|
|
125
164
|
}
|
|
165
|
+
async function loadConfig() {
|
|
166
|
+
const token = await loadAuthToken();
|
|
167
|
+
if (!token) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`No auth token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const raw = await readConfigFile();
|
|
173
|
+
const daemon = raw.daemon ??= {};
|
|
174
|
+
daemon.authToken = token;
|
|
175
|
+
return HydraConfig.parse(raw);
|
|
176
|
+
}
|
|
177
|
+
async function loadConfigReadOnly() {
|
|
178
|
+
return HydraConfigReadOnly.parse(await readConfigFile());
|
|
179
|
+
}
|
|
180
|
+
async function ensureConfig() {
|
|
181
|
+
if (!await loadAuthToken()) {
|
|
182
|
+
const token = generateAuthToken();
|
|
183
|
+
await writeAuthToken(token);
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`hydra-acp: initialized ${paths.authToken()} with a fresh auth token.
|
|
186
|
+
`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return loadConfig();
|
|
190
|
+
}
|
|
126
191
|
function generateAuthToken() {
|
|
127
192
|
const bytes = new Uint8Array(32);
|
|
128
193
|
crypto.getRandomValues(bytes);
|
|
@@ -144,7 +209,7 @@ function expandHome(p) {
|
|
|
144
209
|
}
|
|
145
210
|
return p;
|
|
146
211
|
}
|
|
147
|
-
var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig;
|
|
212
|
+
var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig, HydraConfigReadOnly;
|
|
148
213
|
var init_config = __esm({
|
|
149
214
|
"src/core/config.ts"() {
|
|
150
215
|
"use strict";
|
|
@@ -160,7 +225,16 @@ var init_config = __esm({
|
|
|
160
225
|
authToken: z.string().min(16),
|
|
161
226
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
162
227
|
tls: TlsConfig.optional(),
|
|
163
|
-
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
|
|
228
|
+
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
|
|
229
|
+
// Cap on entries kept in a session's on-disk replay log (history.jsonl).
|
|
230
|
+
// Compaction trims to this many on a periodic basis; reads also slice
|
|
231
|
+
// to the tail at this length as a defensive measure against older
|
|
232
|
+
// daemons that may have written unbounded files.
|
|
233
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e3),
|
|
234
|
+
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
235
|
+
// daemon can include it in the diagnostic message when a spawn fails.
|
|
236
|
+
// Bump if your agents emit large tracebacks you want surfaced.
|
|
237
|
+
agentStderrTailBytes: z.number().int().positive().default(4096)
|
|
164
238
|
});
|
|
165
239
|
RegistryConfig = z.object({
|
|
166
240
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -177,7 +251,20 @@ var init_config = __esm({
|
|
|
177
251
|
// Cap on logical lines retained in the in-memory scrollback render
|
|
178
252
|
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
179
253
|
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
180
|
-
maxScrollbackLines: z.number().int().positive().default(1e4)
|
|
254
|
+
maxScrollbackLines: z.number().int().positive().default(1e4),
|
|
255
|
+
// When true (default), the TUI captures mouse events so the wheel can
|
|
256
|
+
// drive scrollback. The cost: terminals route clicks to the app, so
|
|
257
|
+
// text selection requires shift+drag to bypass mouse reporting. Set
|
|
258
|
+
// false to disable capture — wheel scrollback stops working, but
|
|
259
|
+
// plain click-drag selects text via the terminal emulator.
|
|
260
|
+
mouse: z.boolean().default(true),
|
|
261
|
+
// Size at which the TUI's session/update debug log (tui.log) rotates
|
|
262
|
+
// to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
|
|
263
|
+
logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
|
|
264
|
+
// Width cap on the cwd column in the `sessions list` output and the
|
|
265
|
+
// TUI picker. Set higher if you keep deeply-nested working directories
|
|
266
|
+
// and want them visible; the elastic title column shrinks to make room.
|
|
267
|
+
cwdColumnMaxWidth: z.number().int().positive().default(24)
|
|
181
268
|
});
|
|
182
269
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
183
270
|
ExtensionBody = z.object({
|
|
@@ -210,7 +297,16 @@ var init_config = __esm({
|
|
|
210
297
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
211
298
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
212
299
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
213
|
-
tui: TuiConfig.default({
|
|
300
|
+
tui: TuiConfig.default({
|
|
301
|
+
repaintThrottleMs: 1e3,
|
|
302
|
+
maxScrollbackLines: 1e4,
|
|
303
|
+
mouse: true,
|
|
304
|
+
logMaxBytes: 5 * 1024 * 1024,
|
|
305
|
+
cwdColumnMaxWidth: 24
|
|
306
|
+
})
|
|
307
|
+
});
|
|
308
|
+
HydraConfigReadOnly = HydraConfig.extend({
|
|
309
|
+
daemon: DaemonConfig.omit({ authToken: true })
|
|
214
310
|
});
|
|
215
311
|
}
|
|
216
312
|
});
|
|
@@ -285,10 +381,11 @@ function extractHydraMeta(meta) {
|
|
|
285
381
|
function mergeMeta(passthrough, ours) {
|
|
286
382
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
287
383
|
}
|
|
288
|
-
var JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
384
|
+
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
289
385
|
var init_types = __esm({
|
|
290
386
|
"src/acp/types.ts"() {
|
|
291
387
|
"use strict";
|
|
388
|
+
ACP_PROTOCOL_VERSION = 1;
|
|
292
389
|
JsonRpcErrorCodes = {
|
|
293
390
|
ParseError: -32700,
|
|
294
391
|
InvalidRequest: -32600,
|
|
@@ -395,7 +492,7 @@ var init_connection = __esm({
|
|
|
395
492
|
"src/acp/connection.ts"() {
|
|
396
493
|
"use strict";
|
|
397
494
|
init_types();
|
|
398
|
-
JsonRpcConnection = class {
|
|
495
|
+
JsonRpcConnection = class _JsonRpcConnection {
|
|
399
496
|
constructor(stream) {
|
|
400
497
|
this.stream = stream;
|
|
401
498
|
this.stream.onMessage((m) => this.handleIncoming(m));
|
|
@@ -405,6 +502,16 @@ var init_connection = __esm({
|
|
|
405
502
|
requestHandlers = /* @__PURE__ */ new Map();
|
|
406
503
|
defaultRequestHandler;
|
|
407
504
|
notificationHandlers = /* @__PURE__ */ new Map();
|
|
505
|
+
// Notifications received before a handler was registered. Some agents
|
|
506
|
+
// (e.g. claude-acp) advertise their command list in the same chunk as
|
|
507
|
+
// the `session/new` response, which is processed before the consumer
|
|
508
|
+
// can attach its `session/update` handler. Without this buffer those
|
|
509
|
+
// notifications would be silently dropped, so e.g. `/model` would
|
|
510
|
+
// never appear in the TUI's slash-completion palette. Capped per
|
|
511
|
+
// method to keep the buffer from growing unboundedly when nothing
|
|
512
|
+
// ever subscribes.
|
|
513
|
+
bufferedNotifications = /* @__PURE__ */ new Map();
|
|
514
|
+
static MAX_BUFFERED_PER_METHOD = 64;
|
|
408
515
|
pending = /* @__PURE__ */ new Map();
|
|
409
516
|
closed = false;
|
|
410
517
|
closeHandlers = [];
|
|
@@ -416,6 +523,17 @@ var init_connection = __esm({
|
|
|
416
523
|
}
|
|
417
524
|
onNotification(method, handler) {
|
|
418
525
|
this.notificationHandlers.set(method, handler);
|
|
526
|
+
const queued = this.bufferedNotifications.get(method);
|
|
527
|
+
if (!queued) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
this.bufferedNotifications.delete(method);
|
|
531
|
+
for (const note of queued) {
|
|
532
|
+
try {
|
|
533
|
+
handler(note.params, note.method);
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
}
|
|
419
537
|
}
|
|
420
538
|
onClose(handler) {
|
|
421
539
|
this.closeHandlers.push(handler);
|
|
@@ -460,6 +578,13 @@ var init_connection = __esm({
|
|
|
460
578
|
}
|
|
461
579
|
await this.stream.close();
|
|
462
580
|
}
|
|
581
|
+
// Force-close with an error. Rejects all pending requests and fires
|
|
582
|
+
// close handlers carrying `err`. Used by transports that detect a
|
|
583
|
+
// failure (e.g. child process crash, spawn ENOENT) the stream itself
|
|
584
|
+
// can't surface as a stdout/stdin error.
|
|
585
|
+
fail(err) {
|
|
586
|
+
this.handleClose(err);
|
|
587
|
+
}
|
|
463
588
|
handleIncoming(message) {
|
|
464
589
|
if ("method" in message) {
|
|
465
590
|
if ("id" in message && message.id !== void 0) {
|
|
@@ -501,6 +626,16 @@ var init_connection = __esm({
|
|
|
501
626
|
const handler = this.notificationHandlers.get(note.method);
|
|
502
627
|
if (handler) {
|
|
503
628
|
handler(note.params, note.method);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
let queued = this.bufferedNotifications.get(note.method);
|
|
632
|
+
if (!queued) {
|
|
633
|
+
queued = [];
|
|
634
|
+
this.bufferedNotifications.set(note.method, queued);
|
|
635
|
+
}
|
|
636
|
+
queued.push(note);
|
|
637
|
+
if (queued.length > _JsonRpcConnection.MAX_BUFFERED_PER_METHOD) {
|
|
638
|
+
queued.shift();
|
|
504
639
|
}
|
|
505
640
|
}
|
|
506
641
|
handleResponse(res) {
|
|
@@ -557,12 +692,12 @@ var init_hydra_commands = __esm({
|
|
|
557
692
|
HYDRA_COMMANDS = [
|
|
558
693
|
{
|
|
559
694
|
verb: "title",
|
|
560
|
-
name: "
|
|
695
|
+
name: "hydra title",
|
|
561
696
|
description: "Regenerate the session title via the agent (or set manually with an arg)"
|
|
562
697
|
},
|
|
563
698
|
{
|
|
564
699
|
verb: "agent",
|
|
565
|
-
name: "
|
|
700
|
+
name: "hydra agent",
|
|
566
701
|
argsHint: "<agent>",
|
|
567
702
|
description: "Swap the agent backing this session, preserving context"
|
|
568
703
|
}
|
|
@@ -661,7 +796,7 @@ function firstLine(text, max) {
|
|
|
661
796
|
}
|
|
662
797
|
return void 0;
|
|
663
798
|
}
|
|
664
|
-
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX,
|
|
799
|
+
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, DEFAULT_HISTORY_MAX_ENTRIES, Session, STATE_UPDATE_KINDS;
|
|
665
800
|
var init_session = __esm({
|
|
666
801
|
"src/core/session.ts"() {
|
|
667
802
|
"use strict";
|
|
@@ -670,8 +805,7 @@ var init_session = __esm({
|
|
|
670
805
|
HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
671
806
|
generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
672
807
|
HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
673
|
-
|
|
674
|
-
COMPACT_EVERY = 200;
|
|
808
|
+
DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
675
809
|
Session = class {
|
|
676
810
|
sessionId;
|
|
677
811
|
cwd;
|
|
@@ -713,11 +847,13 @@ var init_session = __esm({
|
|
|
713
847
|
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
714
848
|
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
715
849
|
promptStartedAt;
|
|
716
|
-
// Counts appends since the last compaction. When it hits
|
|
850
|
+
// Counts appends since the last compaction. When it hits compactEvery
|
|
717
851
|
// we ask the history store to trim the file to the most recent
|
|
718
|
-
//
|
|
852
|
+
// historyMaxEntries. Keeps file growth bounded without per-append
|
|
719
853
|
// file-size checks.
|
|
720
854
|
appendCount = 0;
|
|
855
|
+
historyMaxEntries;
|
|
856
|
+
compactEvery;
|
|
721
857
|
// Permission requests that have been broadcast to one or more
|
|
722
858
|
// clients but have not yet resolved. Replayed to clients that
|
|
723
859
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -774,6 +910,8 @@ var init_session = __esm({
|
|
|
774
910
|
this.firstPromptSeeded = true;
|
|
775
911
|
}
|
|
776
912
|
this.historyStore = init.historyStore;
|
|
913
|
+
this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
|
|
914
|
+
this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
|
|
777
915
|
this.updatedAt = Date.now();
|
|
778
916
|
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
779
917
|
this.lastRecordedAt = this.updatedAt;
|
|
@@ -1612,9 +1750,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1612
1750
|
if (this.historyStore) {
|
|
1613
1751
|
const store = this.historyStore;
|
|
1614
1752
|
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
1615
|
-
if (this.appendCount >=
|
|
1753
|
+
if (this.appendCount >= this.compactEvery) {
|
|
1616
1754
|
this.appendCount = 0;
|
|
1617
|
-
void store.compact(this.sessionId,
|
|
1755
|
+
void store.compact(this.sessionId, this.historyMaxEntries).catch(
|
|
1618
1756
|
() => void 0
|
|
1619
1757
|
);
|
|
1620
1758
|
}
|
|
@@ -1731,9 +1869,187 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1731
1869
|
}
|
|
1732
1870
|
});
|
|
1733
1871
|
|
|
1872
|
+
// src/core/session-store.ts
|
|
1873
|
+
import * as fs5 from "fs/promises";
|
|
1874
|
+
import * as path4 from "path";
|
|
1875
|
+
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
1876
|
+
import { z as z4 } from "zod";
|
|
1877
|
+
function generateLineageId() {
|
|
1878
|
+
return `${HYDRA_LINEAGE_PREFIX}${generateRawId()}`;
|
|
1879
|
+
}
|
|
1880
|
+
function assertSafeId(id) {
|
|
1881
|
+
if (!SESSION_ID_PATTERN.test(id)) {
|
|
1882
|
+
throw new Error(`unsafe session id: ${id}`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
function recordFromMemorySession(args) {
|
|
1886
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1887
|
+
return {
|
|
1888
|
+
sessionId: args.sessionId,
|
|
1889
|
+
lineageId: args.lineageId,
|
|
1890
|
+
upstreamSessionId: args.upstreamSessionId,
|
|
1891
|
+
importedFromSessionId: args.importedFromSessionId,
|
|
1892
|
+
agentId: args.agentId,
|
|
1893
|
+
cwd: args.cwd,
|
|
1894
|
+
title: args.title,
|
|
1895
|
+
agentArgs: args.agentArgs,
|
|
1896
|
+
currentModel: args.currentModel,
|
|
1897
|
+
currentMode: args.currentMode,
|
|
1898
|
+
currentUsage: args.currentUsage,
|
|
1899
|
+
agentCommands: args.agentCommands,
|
|
1900
|
+
createdAt: args.createdAt ?? now,
|
|
1901
|
+
updatedAt: args.updatedAt ?? now
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
1905
|
+
var init_session_store = __esm({
|
|
1906
|
+
"src/core/session-store.ts"() {
|
|
1907
|
+
"use strict";
|
|
1908
|
+
init_paths();
|
|
1909
|
+
HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1910
|
+
generateRawId = customAlphabet2(HYDRA_ID_ALPHABET2, 16);
|
|
1911
|
+
HYDRA_LINEAGE_PREFIX = "hydra_lineage_";
|
|
1912
|
+
PersistedAgentCommand = z4.object({
|
|
1913
|
+
name: z4.string(),
|
|
1914
|
+
description: z4.string().optional()
|
|
1915
|
+
});
|
|
1916
|
+
PersistedUsage = z4.object({
|
|
1917
|
+
used: z4.number().optional(),
|
|
1918
|
+
size: z4.number().optional(),
|
|
1919
|
+
costAmount: z4.number().optional(),
|
|
1920
|
+
costCurrency: z4.string().optional()
|
|
1921
|
+
});
|
|
1922
|
+
SessionRecord = z4.object({
|
|
1923
|
+
version: z4.literal(1),
|
|
1924
|
+
sessionId: z4.string(),
|
|
1925
|
+
// Optional for back-compat with records written before this field
|
|
1926
|
+
// existed; mergeForPersistence generates one on next write so any
|
|
1927
|
+
// touched session converges to having a lineageId. A record that
|
|
1928
|
+
// never gets written again (truly cold and untouched) just won't
|
|
1929
|
+
// participate in lineage-based dedup, which is correct — it was
|
|
1930
|
+
// never exported, so no incoming bundle can claim its lineage.
|
|
1931
|
+
lineageId: z4.string().optional(),
|
|
1932
|
+
upstreamSessionId: z4.string(),
|
|
1933
|
+
// When non-empty, marks a session that was created by import and is
|
|
1934
|
+
// waiting for its first attach to bootstrap a fresh upstream agent
|
|
1935
|
+
// and replay the imported history as a takeover transcript. The
|
|
1936
|
+
// origin's local id at export time, kept for debuggability and as a
|
|
1937
|
+
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
1938
|
+
importedFromSessionId: z4.string().optional(),
|
|
1939
|
+
agentId: z4.string(),
|
|
1940
|
+
cwd: z4.string(),
|
|
1941
|
+
title: z4.string().optional(),
|
|
1942
|
+
agentArgs: z4.array(z4.string()).optional(),
|
|
1943
|
+
// Snapshot of "what is currently true about this session" carried in
|
|
1944
|
+
// meta.json so a late-attaching or cold-resurrected client can be
|
|
1945
|
+
// told via the attach response _meta without depending on history
|
|
1946
|
+
// replay of a snapshot-shaped notification.
|
|
1947
|
+
currentModel: z4.string().optional(),
|
|
1948
|
+
currentMode: z4.string().optional(),
|
|
1949
|
+
currentUsage: PersistedUsage.optional(),
|
|
1950
|
+
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
1951
|
+
createdAt: z4.string(),
|
|
1952
|
+
updatedAt: z4.string()
|
|
1953
|
+
});
|
|
1954
|
+
SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
1955
|
+
SessionStore = class {
|
|
1956
|
+
async write(record) {
|
|
1957
|
+
assertSafeId(record.sessionId);
|
|
1958
|
+
await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
1959
|
+
const full = { version: 1, ...record };
|
|
1960
|
+
await fs5.writeFile(
|
|
1961
|
+
paths.sessionFile(record.sessionId),
|
|
1962
|
+
JSON.stringify(full, null, 2) + "\n",
|
|
1963
|
+
{ encoding: "utf8", mode: 384 }
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
async read(sessionId) {
|
|
1967
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
1968
|
+
return void 0;
|
|
1969
|
+
}
|
|
1970
|
+
let raw;
|
|
1971
|
+
try {
|
|
1972
|
+
raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
const e = err;
|
|
1975
|
+
if (e.code === "ENOENT") {
|
|
1976
|
+
return void 0;
|
|
1977
|
+
}
|
|
1978
|
+
throw err;
|
|
1979
|
+
}
|
|
1980
|
+
try {
|
|
1981
|
+
return SessionRecord.parse(JSON.parse(raw));
|
|
1982
|
+
} catch {
|
|
1983
|
+
return void 0;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
async delete(sessionId) {
|
|
1987
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
try {
|
|
1991
|
+
await fs5.unlink(paths.sessionFile(sessionId));
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
const e = err;
|
|
1994
|
+
if (e.code !== "ENOENT") {
|
|
1995
|
+
throw err;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
try {
|
|
1999
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
2000
|
+
} catch (err) {
|
|
2001
|
+
const e = err;
|
|
2002
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
2003
|
+
throw err;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
// Find a persisted session by lineageId. Used by SessionManager.import
|
|
2008
|
+
// to detect bundles that have already been imported (lineageId match)
|
|
2009
|
+
// so we can either error out or, with replace:true, overwrite.
|
|
2010
|
+
// Returns undefined if no record has that lineageId. Records that
|
|
2011
|
+
// pre-date the lineageId field simply don't match — which is
|
|
2012
|
+
// correct: they were never exported, so no incoming bundle can
|
|
2013
|
+
// legitimately claim their lineage.
|
|
2014
|
+
async findByLineageId(lineageId) {
|
|
2015
|
+
if (lineageId.length === 0) {
|
|
2016
|
+
return void 0;
|
|
2017
|
+
}
|
|
2018
|
+
const all = await this.list().catch(() => []);
|
|
2019
|
+
for (const record of all) {
|
|
2020
|
+
if (record.lineageId === lineageId) {
|
|
2021
|
+
return record;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return void 0;
|
|
2025
|
+
}
|
|
2026
|
+
async list() {
|
|
2027
|
+
let entries;
|
|
2028
|
+
try {
|
|
2029
|
+
entries = await fs5.readdir(paths.sessionsDir());
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
const e = err;
|
|
2032
|
+
if (e.code === "ENOENT") {
|
|
2033
|
+
return [];
|
|
2034
|
+
}
|
|
2035
|
+
throw err;
|
|
2036
|
+
}
|
|
2037
|
+
const records = [];
|
|
2038
|
+
for (const entry of entries) {
|
|
2039
|
+
const record = await this.read(entry);
|
|
2040
|
+
if (record) {
|
|
2041
|
+
records.push(record);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
return records;
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
|
|
1734
2050
|
// src/tui/history.ts
|
|
1735
2051
|
import { promises as fs7 } from "fs";
|
|
1736
|
-
import * as
|
|
2052
|
+
import * as path5 from "path";
|
|
1737
2053
|
async function loadHistory(file) {
|
|
1738
2054
|
let text;
|
|
1739
2055
|
try {
|
|
@@ -1777,7 +2093,7 @@ function appendEntry(history, entry) {
|
|
|
1777
2093
|
return out;
|
|
1778
2094
|
}
|
|
1779
2095
|
async function saveHistory(file, history) {
|
|
1780
|
-
await fs7.mkdir(
|
|
2096
|
+
await fs7.mkdir(path5.dirname(file), { recursive: true });
|
|
1781
2097
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1782
2098
|
await fs7.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1783
2099
|
}
|
|
@@ -1789,6 +2105,113 @@ var init_history = __esm({
|
|
|
1789
2105
|
}
|
|
1790
2106
|
});
|
|
1791
2107
|
|
|
2108
|
+
// src/core/hydra-version.ts
|
|
2109
|
+
import { fileURLToPath } from "url";
|
|
2110
|
+
import * as path6 from "path";
|
|
2111
|
+
import * as fs8 from "fs";
|
|
2112
|
+
function resolveVersion() {
|
|
2113
|
+
try {
|
|
2114
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
2115
|
+
for (let i = 0; i < 8; i += 1) {
|
|
2116
|
+
const candidate = path6.join(dir, "package.json");
|
|
2117
|
+
if (fs8.existsSync(candidate)) {
|
|
2118
|
+
const pkg = JSON.parse(fs8.readFileSync(candidate, "utf8"));
|
|
2119
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
2120
|
+
return pkg.version;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
const parent = path6.dirname(dir);
|
|
2124
|
+
if (parent === dir) {
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
dir = parent;
|
|
2128
|
+
}
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
return "0.0.0";
|
|
2132
|
+
}
|
|
2133
|
+
var HYDRA_VERSION;
|
|
2134
|
+
var init_hydra_version = __esm({
|
|
2135
|
+
"src/core/hydra-version.ts"() {
|
|
2136
|
+
"use strict";
|
|
2137
|
+
HYDRA_VERSION = resolveVersion();
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// src/core/bundle.ts
|
|
2142
|
+
import { z as z5 } from "zod";
|
|
2143
|
+
function encodeBundle(params) {
|
|
2144
|
+
const bundle = {
|
|
2145
|
+
version: 1,
|
|
2146
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2147
|
+
exportedFrom: {
|
|
2148
|
+
hydraVersion: params.hydraVersion,
|
|
2149
|
+
machine: params.machine
|
|
2150
|
+
},
|
|
2151
|
+
session: {
|
|
2152
|
+
sessionId: params.record.sessionId,
|
|
2153
|
+
lineageId: params.record.lineageId,
|
|
2154
|
+
agentId: params.record.agentId,
|
|
2155
|
+
cwd: params.record.cwd,
|
|
2156
|
+
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
2157
|
+
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
2158
|
+
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
2159
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
2160
|
+
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
2161
|
+
createdAt: params.record.createdAt,
|
|
2162
|
+
updatedAt: params.record.updatedAt
|
|
2163
|
+
},
|
|
2164
|
+
history: params.history
|
|
2165
|
+
};
|
|
2166
|
+
if (params.promptHistory !== void 0) {
|
|
2167
|
+
bundle.promptHistory = params.promptHistory;
|
|
2168
|
+
}
|
|
2169
|
+
return bundle;
|
|
2170
|
+
}
|
|
2171
|
+
function decodeBundle(raw) {
|
|
2172
|
+
return Bundle.parse(raw);
|
|
2173
|
+
}
|
|
2174
|
+
var HistoryEntrySchema, BundleSession, Bundle;
|
|
2175
|
+
var init_bundle = __esm({
|
|
2176
|
+
"src/core/bundle.ts"() {
|
|
2177
|
+
"use strict";
|
|
2178
|
+
init_session_store();
|
|
2179
|
+
HistoryEntrySchema = z5.object({
|
|
2180
|
+
method: z5.string(),
|
|
2181
|
+
params: z5.unknown(),
|
|
2182
|
+
recordedAt: z5.number()
|
|
2183
|
+
});
|
|
2184
|
+
BundleSession = z5.object({
|
|
2185
|
+
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
2186
|
+
// the local namespace; lineageId is what survives across hops).
|
|
2187
|
+
sessionId: z5.string(),
|
|
2188
|
+
// Required on bundles — the export path backfills if the source
|
|
2189
|
+
// record was written before lineageId existed.
|
|
2190
|
+
lineageId: z5.string(),
|
|
2191
|
+
agentId: z5.string(),
|
|
2192
|
+
cwd: z5.string(),
|
|
2193
|
+
title: z5.string().optional(),
|
|
2194
|
+
currentModel: z5.string().optional(),
|
|
2195
|
+
currentMode: z5.string().optional(),
|
|
2196
|
+
currentUsage: PersistedUsage.optional(),
|
|
2197
|
+
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
2198
|
+
createdAt: z5.string(),
|
|
2199
|
+
updatedAt: z5.string()
|
|
2200
|
+
});
|
|
2201
|
+
Bundle = z5.object({
|
|
2202
|
+
version: z5.literal(1),
|
|
2203
|
+
exportedAt: z5.string(),
|
|
2204
|
+
exportedFrom: z5.object({
|
|
2205
|
+
hydraVersion: z5.string(),
|
|
2206
|
+
machine: z5.string()
|
|
2207
|
+
}),
|
|
2208
|
+
session: BundleSession,
|
|
2209
|
+
history: z5.array(HistoryEntrySchema),
|
|
2210
|
+
promptHistory: z5.array(z5.string()).optional()
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
|
|
1792
2215
|
// src/acp/ws-stream.ts
|
|
1793
2216
|
function wsToMessageStream(ws) {
|
|
1794
2217
|
const messageHandlers = [];
|
|
@@ -1867,7 +2290,7 @@ var init_ws_stream = __esm({
|
|
|
1867
2290
|
});
|
|
1868
2291
|
|
|
1869
2292
|
// src/core/daemon-bootstrap.ts
|
|
1870
|
-
import { spawn as
|
|
2293
|
+
import { spawn as spawn5 } from "child_process";
|
|
1871
2294
|
import { setTimeout as sleep } from "timers/promises";
|
|
1872
2295
|
async function ensureDaemonReachable(config) {
|
|
1873
2296
|
if (await pingHealth(config)) {
|
|
@@ -1894,7 +2317,7 @@ function spawnDaemonDetached() {
|
|
|
1894
2317
|
if (!cliPath) {
|
|
1895
2318
|
throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
|
|
1896
2319
|
}
|
|
1897
|
-
const child =
|
|
2320
|
+
const child = spawn5(
|
|
1898
2321
|
process.execPath,
|
|
1899
2322
|
[cliPath, "daemon", "start", "--foreground"],
|
|
1900
2323
|
{
|
|
@@ -1942,8 +2365,8 @@ function formatAgentWithModel(agentId, model) {
|
|
|
1942
2365
|
}
|
|
1943
2366
|
return `${agent}${AGENT_MODEL_SEP}${short}`;
|
|
1944
2367
|
}
|
|
1945
|
-
function formatAgentCell(agentId,
|
|
1946
|
-
const base =
|
|
2368
|
+
function formatAgentCell(agentId, usage) {
|
|
2369
|
+
const base = agentId ?? "?";
|
|
1947
2370
|
if (!usage || typeof usage.costAmount !== "number") {
|
|
1948
2371
|
return base;
|
|
1949
2372
|
}
|
|
@@ -1980,10 +2403,10 @@ function toRow(s, now = Date.now()) {
|
|
|
1980
2403
|
session: stripHydraSessionPrefix(s.sessionId),
|
|
1981
2404
|
upstream: s.upstreamSessionId ?? "-",
|
|
1982
2405
|
state: formatState(s.status, s.attachedClients),
|
|
1983
|
-
agent: formatAgentCell(s.agentId, s.
|
|
2406
|
+
agent: formatAgentCell(s.agentId, s.currentUsage),
|
|
1984
2407
|
age: formatRelativeAge(s.updatedAt, now),
|
|
1985
2408
|
title: s.title ?? "-",
|
|
1986
|
-
cwd: s.cwd
|
|
2409
|
+
cwd: shortenHomePath(s.cwd)
|
|
1987
2410
|
};
|
|
1988
2411
|
}
|
|
1989
2412
|
function formatState(status, clients) {
|
|
@@ -1999,6 +2422,7 @@ function computeWidths(rows) {
|
|
|
1999
2422
|
state: maxLen(HEADER.state, rows.map((r) => r.state)),
|
|
2000
2423
|
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
2001
2424
|
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
2425
|
+
cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
|
|
2002
2426
|
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
2003
2427
|
};
|
|
2004
2428
|
}
|
|
@@ -2047,7 +2471,7 @@ function maxLen(headerCell, values) {
|
|
|
2047
2471
|
}
|
|
2048
2472
|
return max;
|
|
2049
2473
|
}
|
|
2050
|
-
function formatRow(r, w, maxWidth) {
|
|
2474
|
+
function formatRow(r, w, maxWidth, cwdMaxWidth = DEFAULT_CWD_MAX_WIDTH) {
|
|
2051
2475
|
const fixed = [
|
|
2052
2476
|
r.session.padEnd(w.session),
|
|
2053
2477
|
r.upstream.padEnd(w.upstream),
|
|
@@ -2056,20 +2480,18 @@ function formatRow(r, w, maxWidth) {
|
|
|
2056
2480
|
r.age.padStart(w.age)
|
|
2057
2481
|
].join(SEP);
|
|
2058
2482
|
if (maxWidth === void 0) {
|
|
2059
|
-
return [fixed, r.
|
|
2483
|
+
return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
|
|
2060
2484
|
}
|
|
2061
|
-
const titleCap = Math.min(w.title, TITLE_MAX_WIDTH);
|
|
2062
2485
|
const budget = maxWidth - fixed.length - SEP.length;
|
|
2063
2486
|
if (budget <= 0) {
|
|
2064
2487
|
return fixed.slice(0, maxWidth);
|
|
2065
2488
|
}
|
|
2066
|
-
const
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
const
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
return [fixed, titleCell, cwdCell].join(SEP);
|
|
2489
|
+
const cwdCap = Math.min(w.cwd, cwdMaxWidth);
|
|
2490
|
+
const cwdAlloc = Math.min(cwdCap, Math.max(0, budget - SEP.length - 1));
|
|
2491
|
+
const cwdCell = truncateMiddle(r.cwd, cwdAlloc).padEnd(cwdAlloc);
|
|
2492
|
+
const titleBudget = Math.max(0, budget - cwdAlloc - SEP.length);
|
|
2493
|
+
const titleCell = truncateRight(r.title, titleBudget);
|
|
2494
|
+
return [fixed, cwdCell, titleCell].join(SEP);
|
|
2073
2495
|
}
|
|
2074
2496
|
function truncateRight(s, max) {
|
|
2075
2497
|
if (max <= 0) {
|
|
@@ -2097,11 +2519,12 @@ function truncateMiddle(s, max) {
|
|
|
2097
2519
|
const tail = max - 1 - head;
|
|
2098
2520
|
return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
|
|
2099
2521
|
}
|
|
2100
|
-
var HEADER, SEP,
|
|
2522
|
+
var HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
|
|
2101
2523
|
var init_session_row = __esm({
|
|
2102
2524
|
"src/cli/session-row.ts"() {
|
|
2103
2525
|
"use strict";
|
|
2104
2526
|
init_agent_display();
|
|
2527
|
+
init_paths();
|
|
2105
2528
|
init_session();
|
|
2106
2529
|
HEADER = {
|
|
2107
2530
|
session: "SESSION",
|
|
@@ -2113,14 +2536,13 @@ var init_session_row = __esm({
|
|
|
2113
2536
|
cwd: "CWD"
|
|
2114
2537
|
};
|
|
2115
2538
|
SEP = " ";
|
|
2116
|
-
|
|
2117
|
-
TITLE_MAX_WIDTH = 40;
|
|
2539
|
+
DEFAULT_CWD_MAX_WIDTH = 24;
|
|
2118
2540
|
}
|
|
2119
2541
|
});
|
|
2120
2542
|
|
|
2121
2543
|
// src/cli/commands/sessions.ts
|
|
2122
|
-
import * as
|
|
2123
|
-
import * as
|
|
2544
|
+
import * as fs13 from "fs/promises";
|
|
2545
|
+
import * as path8 from "path";
|
|
2124
2546
|
async function runSessionsList(opts = {}) {
|
|
2125
2547
|
const config = await loadConfig();
|
|
2126
2548
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -2159,9 +2581,10 @@ async function runSessionsList(opts = {}) {
|
|
|
2159
2581
|
const rows = visible.map((s) => toRow(s, now));
|
|
2160
2582
|
const widths = computeWidths(rows);
|
|
2161
2583
|
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2162
|
-
|
|
2584
|
+
const cwdMax = config.tui.cwdColumnMaxWidth;
|
|
2585
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
|
|
2163
2586
|
for (const r of rows) {
|
|
2164
|
-
process.stdout.write(formatRow(r, widths, maxWidth) + "\n");
|
|
2587
|
+
process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
|
|
2165
2588
|
}
|
|
2166
2589
|
if (truncated > 0) {
|
|
2167
2590
|
process.stdout.write(
|
|
@@ -2190,9 +2613,9 @@ async function runSessionsKill(id) {
|
|
|
2190
2613
|
process.stdout.write(`Killed ${id}
|
|
2191
2614
|
`);
|
|
2192
2615
|
}
|
|
2193
|
-
async function
|
|
2616
|
+
async function runSessionsRemove(id) {
|
|
2194
2617
|
if (!id) {
|
|
2195
|
-
process.stderr.write("Usage: hydra-acp sessions
|
|
2618
|
+
process.stderr.write("Usage: hydra-acp sessions remove <session-id>\n");
|
|
2196
2619
|
process.exit(2);
|
|
2197
2620
|
}
|
|
2198
2621
|
const config = await loadConfig();
|
|
@@ -2239,23 +2662,40 @@ async function runSessionsExport(id, outPath) {
|
|
|
2239
2662
|
return;
|
|
2240
2663
|
}
|
|
2241
2664
|
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
2242
|
-
await
|
|
2243
|
-
await
|
|
2665
|
+
await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
|
|
2666
|
+
await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2244
2667
|
process.stdout.write(`Wrote ${resolved}
|
|
2245
2668
|
`);
|
|
2246
2669
|
}
|
|
2247
2670
|
async function runSessionsImport(file, opts = {}) {
|
|
2248
2671
|
if (!file) {
|
|
2249
2672
|
process.stderr.write(
|
|
2250
|
-
"Usage: hydra-acp sessions import <file>|- [--replace]\n"
|
|
2673
|
+
"Usage: hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]\n"
|
|
2251
2674
|
);
|
|
2252
2675
|
process.exit(2);
|
|
2253
2676
|
}
|
|
2677
|
+
let cwdOverride;
|
|
2678
|
+
if (opts.cwd !== void 0) {
|
|
2679
|
+
const resolved = path8.resolve(opts.cwd);
|
|
2680
|
+
try {
|
|
2681
|
+
const stat4 = await fs13.stat(resolved);
|
|
2682
|
+
if (!stat4.isDirectory()) {
|
|
2683
|
+
process.stderr.write(`--cwd ${resolved} is not a directory
|
|
2684
|
+
`);
|
|
2685
|
+
process.exit(1);
|
|
2686
|
+
}
|
|
2687
|
+
} catch {
|
|
2688
|
+
process.stderr.write(`--cwd ${resolved} does not exist
|
|
2689
|
+
`);
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
cwdOverride = resolved;
|
|
2693
|
+
}
|
|
2254
2694
|
let body;
|
|
2255
2695
|
if (file === "-") {
|
|
2256
2696
|
body = await readStdin();
|
|
2257
2697
|
} else {
|
|
2258
|
-
body = await
|
|
2698
|
+
body = await fs13.readFile(file, "utf8");
|
|
2259
2699
|
}
|
|
2260
2700
|
let bundle;
|
|
2261
2701
|
try {
|
|
@@ -2265,6 +2705,11 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2265
2705
|
`);
|
|
2266
2706
|
process.exit(1);
|
|
2267
2707
|
}
|
|
2708
|
+
if (opts.info === true) {
|
|
2709
|
+
const inspectConfig = await loadConfigReadOnly();
|
|
2710
|
+
printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2268
2713
|
const config = await loadConfig();
|
|
2269
2714
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2270
2715
|
const response = await fetch(`${baseUrl}/v1/sessions/import`, {
|
|
@@ -2273,7 +2718,11 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2273
2718
|
"Content-Type": "application/json",
|
|
2274
2719
|
Authorization: `Bearer ${config.daemon.authToken}`
|
|
2275
2720
|
},
|
|
2276
|
-
body: JSON.stringify({
|
|
2721
|
+
body: JSON.stringify({
|
|
2722
|
+
bundle,
|
|
2723
|
+
replace: opts.replace === true,
|
|
2724
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
2725
|
+
})
|
|
2277
2726
|
});
|
|
2278
2727
|
if (response.status === 409) {
|
|
2279
2728
|
const detail = await response.json().catch(() => ({}));
|
|
@@ -2296,6 +2745,42 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2296
2745
|
`
|
|
2297
2746
|
);
|
|
2298
2747
|
}
|
|
2748
|
+
function bundleToSummary(parsed) {
|
|
2749
|
+
return {
|
|
2750
|
+
sessionId: parsed.session.sessionId,
|
|
2751
|
+
upstreamSessionId: "-",
|
|
2752
|
+
cwd: parsed.session.cwd,
|
|
2753
|
+
agentId: parsed.session.agentId,
|
|
2754
|
+
currentUsage: parsed.session.currentUsage,
|
|
2755
|
+
title: parsed.session.title,
|
|
2756
|
+
attachedClients: 0,
|
|
2757
|
+
updatedAt: parsed.session.updatedAt,
|
|
2758
|
+
status: "cold"
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
function printBundleInfo(raw, cwdColumnMaxWidth) {
|
|
2762
|
+
let parsed;
|
|
2763
|
+
try {
|
|
2764
|
+
parsed = decodeBundle(raw);
|
|
2765
|
+
} catch (err) {
|
|
2766
|
+
process.stderr.write(`Not a valid bundle: ${err.message}
|
|
2767
|
+
`);
|
|
2768
|
+
process.exit(1);
|
|
2769
|
+
}
|
|
2770
|
+
const summary = bundleToSummary(parsed);
|
|
2771
|
+
const row = toRow(summary);
|
|
2772
|
+
const widths = computeWidths([row]);
|
|
2773
|
+
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2774
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
2775
|
+
process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
2776
|
+
process.stdout.write(
|
|
2777
|
+
`
|
|
2778
|
+
lineage: ${parsed.session.lineageId}
|
|
2779
|
+
exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
|
|
2780
|
+
history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
|
|
2781
|
+
` : "\n")
|
|
2782
|
+
);
|
|
2783
|
+
}
|
|
2299
2784
|
async function readStdin() {
|
|
2300
2785
|
const chunks = [];
|
|
2301
2786
|
for await (const chunk of process.stdin) {
|
|
@@ -2322,6 +2807,7 @@ var init_sessions = __esm({
|
|
|
2322
2807
|
"src/cli/commands/sessions.ts"() {
|
|
2323
2808
|
"use strict";
|
|
2324
2809
|
init_config();
|
|
2810
|
+
init_bundle();
|
|
2325
2811
|
init_session_row();
|
|
2326
2812
|
}
|
|
2327
2813
|
});
|
|
@@ -2656,12 +3142,15 @@ async function pickSession(term, opts) {
|
|
|
2656
3142
|
return b.updatedAt.localeCompare(a.updatedAt);
|
|
2657
3143
|
});
|
|
2658
3144
|
};
|
|
2659
|
-
let
|
|
3145
|
+
let allSessions = sortSessions(opts.sessions);
|
|
3146
|
+
let visible = allSessions;
|
|
2660
3147
|
let rows = visible.map((s) => toRow(s, Date.now()));
|
|
2661
3148
|
let widths = computeWidths(rows);
|
|
2662
3149
|
let total = 1 + visible.length;
|
|
2663
3150
|
let selectedIdx = 0;
|
|
2664
3151
|
let scrollOffset = 0;
|
|
3152
|
+
let searchActive = false;
|
|
3153
|
+
let searchTerm = "";
|
|
2665
3154
|
let mode = "normal";
|
|
2666
3155
|
let pendingAction = null;
|
|
2667
3156
|
let transientStatus = null;
|
|
@@ -2672,6 +3161,7 @@ async function pickSession(term, opts) {
|
|
|
2672
3161
|
let headerLine = "";
|
|
2673
3162
|
let sessionLines = [];
|
|
2674
3163
|
let startRow = 1;
|
|
3164
|
+
const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
|
|
2675
3165
|
const computeLayout = () => {
|
|
2676
3166
|
termHeight = readTermHeight(term);
|
|
2677
3167
|
termWidth = readTermWidth(term);
|
|
@@ -2679,8 +3169,8 @@ async function pickSession(term, opts) {
|
|
|
2679
3169
|
viewportSize = Math.min(visible.length, maxViewportRows);
|
|
2680
3170
|
const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
|
|
2681
3171
|
newSessionLabel = formatNewSessionLabel(opts.cwd, rowMaxWidth);
|
|
2682
|
-
headerLine = formatRow(HEADER, widths, rowMaxWidth);
|
|
2683
|
-
sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth));
|
|
3172
|
+
headerLine = formatRow(HEADER, widths, rowMaxWidth, cwdMaxWidth);
|
|
3173
|
+
sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth, cwdMaxWidth));
|
|
2684
3174
|
};
|
|
2685
3175
|
const rebuildRows = () => {
|
|
2686
3176
|
rows = visible.map((s) => toRow(s, Date.now()));
|
|
@@ -2688,6 +3178,24 @@ async function pickSession(term, opts) {
|
|
|
2688
3178
|
total = 1 + visible.length;
|
|
2689
3179
|
computeLayout();
|
|
2690
3180
|
};
|
|
3181
|
+
const applyFilter = () => {
|
|
3182
|
+
if (searchActive && searchTerm.length > 0) {
|
|
3183
|
+
visible = allSessions.filter((s) => matchesSearch(s, searchTerm));
|
|
3184
|
+
} else {
|
|
3185
|
+
visible = allSessions;
|
|
3186
|
+
}
|
|
3187
|
+
rebuildRows();
|
|
3188
|
+
if (searchActive) {
|
|
3189
|
+
scrollOffset = 0;
|
|
3190
|
+
selectedIdx = visible.length > 0 ? 1 : 0;
|
|
3191
|
+
} else if (selectedIdx > total - 1) {
|
|
3192
|
+
selectedIdx = Math.max(0, total - 1);
|
|
3193
|
+
}
|
|
3194
|
+
if (scrollOffset + viewportSize > visible.length) {
|
|
3195
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
3196
|
+
}
|
|
3197
|
+
adjustScroll();
|
|
3198
|
+
};
|
|
2691
3199
|
const adjustScroll = () => {
|
|
2692
3200
|
if (selectedIdx === 0) {
|
|
2693
3201
|
return;
|
|
@@ -2750,6 +3258,13 @@ async function pickSession(term, opts) {
|
|
|
2750
3258
|
term.dim.noFormat(` ${transientStatus}`);
|
|
2751
3259
|
return;
|
|
2752
3260
|
}
|
|
3261
|
+
if (searchActive) {
|
|
3262
|
+
term.brightYellow.noFormat(` /${searchTerm}`);
|
|
3263
|
+
term.bgBrightYellow(" ");
|
|
3264
|
+
const hint = visible.length === 0 ? " no matches" : ` ${visible.length} match${visible.length === 1 ? "" : "es"}`;
|
|
3265
|
+
term.dim.noFormat(`${hint} \xB7 ^c clears`);
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
2753
3268
|
term.dim.noFormat(formatIndicator());
|
|
2754
3269
|
};
|
|
2755
3270
|
const indicatorRow = () => startRow + 3 + viewportSize;
|
|
@@ -2816,8 +3331,8 @@ async function pickSession(term, opts) {
|
|
|
2816
3331
|
const refresh = async (preferredId) => {
|
|
2817
3332
|
try {
|
|
2818
3333
|
const next = await listSessions(opts.config);
|
|
2819
|
-
|
|
2820
|
-
|
|
3334
|
+
allSessions = sortSessions(next);
|
|
3335
|
+
applyFilter();
|
|
2821
3336
|
if (preferredId !== void 0) {
|
|
2822
3337
|
const idx = visible.findIndex((s) => s.sessionId === preferredId);
|
|
2823
3338
|
if (idx >= 0) {
|
|
@@ -2914,7 +3429,37 @@ async function pickSession(term, opts) {
|
|
|
2914
3429
|
return;
|
|
2915
3430
|
}
|
|
2916
3431
|
clearTransient();
|
|
3432
|
+
if (searchActive) {
|
|
3433
|
+
if (data?.isCharacter) {
|
|
3434
|
+
searchTerm += name;
|
|
3435
|
+
applyFilter();
|
|
3436
|
+
renderFromScratch();
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
if (name === "BACKSPACE") {
|
|
3440
|
+
if (searchTerm.length > 0) {
|
|
3441
|
+
searchTerm = searchTerm.slice(0, -1);
|
|
3442
|
+
applyFilter();
|
|
3443
|
+
renderFromScratch();
|
|
3444
|
+
}
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
if (name === "ESCAPE" || name === "CTRL_C") {
|
|
3448
|
+
searchActive = false;
|
|
3449
|
+
searchTerm = "";
|
|
3450
|
+
applyFilter();
|
|
3451
|
+
renderFromScratch();
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
2917
3455
|
if (data?.isCharacter) {
|
|
3456
|
+
if (name === "/") {
|
|
3457
|
+
searchActive = true;
|
|
3458
|
+
searchTerm = "";
|
|
3459
|
+
applyFilter();
|
|
3460
|
+
renderFromScratch();
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
2918
3463
|
if (name === "r" || name === "R") {
|
|
2919
3464
|
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
2920
3465
|
void refresh(currentId);
|
|
@@ -3019,13 +3564,34 @@ function readTermWidth(term) {
|
|
|
3019
3564
|
function formatNewSessionLabel(cwd, maxWidth) {
|
|
3020
3565
|
const prefix = "+ New session in ";
|
|
3021
3566
|
const budget = Math.max(1, maxWidth - prefix.length);
|
|
3022
|
-
return prefix + truncateMiddle(cwd, budget);
|
|
3567
|
+
return prefix + truncateMiddle(shortenHomePath(cwd), budget);
|
|
3568
|
+
}
|
|
3569
|
+
function matchesSearch(s, term) {
|
|
3570
|
+
if (term.length === 0) {
|
|
3571
|
+
return true;
|
|
3572
|
+
}
|
|
3573
|
+
const t = term.toLowerCase();
|
|
3574
|
+
const haystacks = [
|
|
3575
|
+
stripHydraSessionPrefix(s.sessionId),
|
|
3576
|
+
s.upstreamSessionId ?? "",
|
|
3577
|
+
s.agentId ?? "",
|
|
3578
|
+
s.title ?? "",
|
|
3579
|
+
s.cwd,
|
|
3580
|
+
shortenHomePath(s.cwd)
|
|
3581
|
+
];
|
|
3582
|
+
for (const h of haystacks) {
|
|
3583
|
+
if (h.toLowerCase().includes(t)) {
|
|
3584
|
+
return true;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
return false;
|
|
3023
3588
|
}
|
|
3024
3589
|
var ROW_PREFIX_WIDTH;
|
|
3025
3590
|
var init_picker = __esm({
|
|
3026
3591
|
"src/tui/picker.ts"() {
|
|
3027
3592
|
"use strict";
|
|
3028
3593
|
init_session_row();
|
|
3594
|
+
init_paths();
|
|
3029
3595
|
init_session();
|
|
3030
3596
|
init_discovery();
|
|
3031
3597
|
ROW_PREFIX_WIDTH = 2;
|
|
@@ -3033,14 +3599,14 @@ var init_picker = __esm({
|
|
|
3033
3599
|
});
|
|
3034
3600
|
|
|
3035
3601
|
// src/tui/screen.ts
|
|
3036
|
-
import os3 from "os";
|
|
3037
3602
|
import stringWidth from "string-width";
|
|
3038
3603
|
import wrapAnsi from "wrap-ansi";
|
|
3039
|
-
function formattedLineSig(zone, width, line) {
|
|
3604
|
+
function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null) {
|
|
3605
|
+
const active = activeCol === null ? "" : `a${activeCol}`;
|
|
3040
3606
|
if (!line) {
|
|
3041
|
-
return `${zone}|${width}|empty`;
|
|
3607
|
+
return `${zone}|${width}|empty|${highlight2 ?? ""}|${active}`;
|
|
3042
3608
|
}
|
|
3043
|
-
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}`;
|
|
3609
|
+
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}|${highlight2 ?? ""}|${active}`;
|
|
3044
3610
|
}
|
|
3045
3611
|
function computePromptVisualRows(buffer, room) {
|
|
3046
3612
|
const rows = [];
|
|
@@ -3052,12 +3618,27 @@ function computePromptVisualRows(buffer, room) {
|
|
|
3052
3618
|
}
|
|
3053
3619
|
let pos = 0;
|
|
3054
3620
|
while (pos < line.length) {
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3621
|
+
if (line.length - pos <= room) {
|
|
3622
|
+
rows.push({ bufferIdx: i, startCol: pos, endCol: line.length });
|
|
3623
|
+
pos = line.length;
|
|
3624
|
+
break;
|
|
3625
|
+
}
|
|
3626
|
+
let breakAt = -1;
|
|
3627
|
+
for (let j = pos + room - 1; j >= pos; j--) {
|
|
3628
|
+
const c = line[j];
|
|
3629
|
+
if (c === " " || c === " ") {
|
|
3630
|
+
breakAt = j + 1;
|
|
3631
|
+
break;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
if (breakAt === -1) {
|
|
3635
|
+
breakAt = pos + room;
|
|
3636
|
+
}
|
|
3637
|
+
rows.push({ bufferIdx: i, startCol: pos, endCol: breakAt });
|
|
3638
|
+
pos = breakAt;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
if (rows.length === 0) {
|
|
3061
3642
|
rows.push({ bufferIdx: 0, startCol: 0, endCol: 0 });
|
|
3062
3643
|
}
|
|
3063
3644
|
return rows;
|
|
@@ -3102,6 +3683,34 @@ function computePromptLayout(visualRows, state, maxRows) {
|
|
|
3102
3683
|
}
|
|
3103
3684
|
return { cursorVisualRow, cursorVisualCol, windowStart, rendered };
|
|
3104
3685
|
}
|
|
3686
|
+
function writeBodyWithHighlight(termObj, text, style, term, activeCol = null, _activeLength = 0) {
|
|
3687
|
+
if (text.length === 0) {
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
if (term.length === 0) {
|
|
3691
|
+
writeStyled(termObj, text, style);
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
const haystack = text.toLowerCase();
|
|
3695
|
+
let i = 0;
|
|
3696
|
+
while (i < text.length) {
|
|
3697
|
+
const next = haystack.indexOf(term, i);
|
|
3698
|
+
if (next === -1) {
|
|
3699
|
+
writeStyled(termObj, text.slice(i), style);
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
if (next > i) {
|
|
3703
|
+
writeStyled(termObj, text.slice(i, next), style);
|
|
3704
|
+
}
|
|
3705
|
+
const isActive = activeCol !== null && next === activeCol;
|
|
3706
|
+
writeStyled(
|
|
3707
|
+
termObj,
|
|
3708
|
+
text.slice(next, next + term.length),
|
|
3709
|
+
isActive ? "search-highlight-active" : "search-highlight"
|
|
3710
|
+
);
|
|
3711
|
+
i = next + term.length;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3105
3714
|
function writeStyled(term, text, style) {
|
|
3106
3715
|
if (text.length === 0) {
|
|
3107
3716
|
return;
|
|
@@ -3164,6 +3773,12 @@ function writeStyled(term, text, style) {
|
|
|
3164
3773
|
case "heading-3":
|
|
3165
3774
|
term.bold.noFormat(text);
|
|
3166
3775
|
return;
|
|
3776
|
+
case "search-highlight":
|
|
3777
|
+
term.bgBrightYellow.black.noFormat(text);
|
|
3778
|
+
return;
|
|
3779
|
+
case "search-highlight-active":
|
|
3780
|
+
term.bgRed.brightWhite.noFormat(text);
|
|
3781
|
+
return;
|
|
3167
3782
|
default:
|
|
3168
3783
|
term.noFormat(text);
|
|
3169
3784
|
}
|
|
@@ -3177,17 +3792,80 @@ function wrapAnsiBody(text, width) {
|
|
|
3177
3792
|
}
|
|
3178
3793
|
return wrapAnsi(text, width, { hard: true, trim: false }).split("\n");
|
|
3179
3794
|
}
|
|
3180
|
-
function
|
|
3795
|
+
function matchTkMarkupAt(text, i) {
|
|
3796
|
+
if (text.charCodeAt(i) !== 94) {
|
|
3797
|
+
return null;
|
|
3798
|
+
}
|
|
3799
|
+
const c = text[i + 1];
|
|
3800
|
+
if (c === void 0) {
|
|
3801
|
+
return null;
|
|
3802
|
+
}
|
|
3803
|
+
if (c === "^") {
|
|
3804
|
+
return { text: "^^", width: 1 };
|
|
3805
|
+
}
|
|
3806
|
+
if (c === "[") {
|
|
3807
|
+
const end = text.indexOf("]", i + 2);
|
|
3808
|
+
if (end !== -1) {
|
|
3809
|
+
return { text: text.slice(i, end + 1), width: 0 };
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
if (TK_MARKUP_STYLE_CHAR.test(c)) {
|
|
3813
|
+
return { text: text.slice(i, i + 2), width: 0 };
|
|
3814
|
+
}
|
|
3815
|
+
return null;
|
|
3816
|
+
}
|
|
3817
|
+
function hasTkMarkup(text) {
|
|
3818
|
+
if (!text.includes("^")) {
|
|
3819
|
+
return false;
|
|
3820
|
+
}
|
|
3821
|
+
for (let i = 0; i < text.length; i++) {
|
|
3822
|
+
if (matchTkMarkupAt(text, i)) {
|
|
3823
|
+
return true;
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
return false;
|
|
3827
|
+
}
|
|
3828
|
+
function* segmentForWidth(text) {
|
|
3829
|
+
let i = 0;
|
|
3830
|
+
while (i < text.length) {
|
|
3831
|
+
const m = matchTkMarkupAt(text, i);
|
|
3832
|
+
if (m) {
|
|
3833
|
+
yield { text: m.text, width: m.width };
|
|
3834
|
+
i += m.text.length;
|
|
3835
|
+
continue;
|
|
3836
|
+
}
|
|
3837
|
+
let runEnd = text.length;
|
|
3838
|
+
let probe = text.indexOf("^", i);
|
|
3839
|
+
while (probe !== -1 && probe < text.length) {
|
|
3840
|
+
if (matchTkMarkupAt(text, probe)) {
|
|
3841
|
+
runEnd = probe;
|
|
3842
|
+
break;
|
|
3843
|
+
}
|
|
3844
|
+
probe = text.indexOf("^", probe + 1);
|
|
3845
|
+
}
|
|
3846
|
+
if (runEnd === i) {
|
|
3847
|
+
yield { text: "^", width: 1 };
|
|
3848
|
+
i += 1;
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3851
|
+
for (const { segment } of SEGMENTER.segment(text.slice(i, runEnd))) {
|
|
3852
|
+
yield { text: segment, width: stringWidth(segment) };
|
|
3853
|
+
}
|
|
3854
|
+
i = runEnd;
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
function wrap(text, width, opts = {}) {
|
|
3181
3858
|
if (width <= 0) {
|
|
3182
3859
|
return [text];
|
|
3183
3860
|
}
|
|
3184
3861
|
if (text.length === 0) {
|
|
3185
3862
|
return [""];
|
|
3186
3863
|
}
|
|
3187
|
-
|
|
3864
|
+
const stripMarkup = opts.stripMarkup === true && hasTkMarkup(text);
|
|
3865
|
+
if (!stripMarkup && !NON_ASCII.test(text)) {
|
|
3188
3866
|
return wrapAscii(text, width);
|
|
3189
3867
|
}
|
|
3190
|
-
return wrapVisible(text, width);
|
|
3868
|
+
return wrapVisible(text, width, stripMarkup);
|
|
3191
3869
|
}
|
|
3192
3870
|
function wrapAscii(text, width) {
|
|
3193
3871
|
const out = [];
|
|
@@ -3212,32 +3890,33 @@ function wrapAscii(text, width) {
|
|
|
3212
3890
|
out.push(remaining);
|
|
3213
3891
|
return out;
|
|
3214
3892
|
}
|
|
3215
|
-
function wrapVisible(text, width) {
|
|
3893
|
+
function wrapVisible(text, width, stripMarkup) {
|
|
3216
3894
|
const out = [];
|
|
3217
|
-
const
|
|
3218
|
-
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3219
|
-
graphemes.push({ seg: segment, w: stringWidth(segment) });
|
|
3220
|
-
}
|
|
3895
|
+
const segments = stripMarkup ? [...segmentForWidth(text)] : graphemeSegments(text);
|
|
3221
3896
|
let i = 0;
|
|
3222
|
-
while (i <
|
|
3897
|
+
while (i < segments.length) {
|
|
3223
3898
|
let chunk = "";
|
|
3224
3899
|
let chunkW = 0;
|
|
3225
3900
|
let lastSpaceI = -1;
|
|
3226
3901
|
let chunkAtLastSpace = "";
|
|
3227
|
-
while (i <
|
|
3228
|
-
const
|
|
3229
|
-
if (chunkW +
|
|
3902
|
+
while (i < segments.length) {
|
|
3903
|
+
const s = segments[i];
|
|
3904
|
+
if (chunkW + s.width > width) {
|
|
3905
|
+
if (s.text === " " && s.width === 1) {
|
|
3906
|
+
lastSpaceI = i;
|
|
3907
|
+
chunkAtLastSpace = chunk;
|
|
3908
|
+
}
|
|
3230
3909
|
break;
|
|
3231
3910
|
}
|
|
3232
|
-
if (
|
|
3911
|
+
if (s.text === " " && s.width === 1) {
|
|
3233
3912
|
lastSpaceI = i;
|
|
3234
3913
|
chunkAtLastSpace = chunk;
|
|
3235
3914
|
}
|
|
3236
|
-
chunk +=
|
|
3237
|
-
chunkW +=
|
|
3915
|
+
chunk += s.text;
|
|
3916
|
+
chunkW += s.width;
|
|
3238
3917
|
i += 1;
|
|
3239
3918
|
}
|
|
3240
|
-
if (i >=
|
|
3919
|
+
if (i >= segments.length) {
|
|
3241
3920
|
out.push(chunk);
|
|
3242
3921
|
break;
|
|
3243
3922
|
}
|
|
@@ -3245,7 +3924,7 @@ function wrapVisible(text, width) {
|
|
|
3245
3924
|
out.push(chunkAtLastSpace);
|
|
3246
3925
|
i = lastSpaceI + 1;
|
|
3247
3926
|
} else if (chunk.length === 0) {
|
|
3248
|
-
out.push(
|
|
3927
|
+
out.push(segments[i].text);
|
|
3249
3928
|
i += 1;
|
|
3250
3929
|
} else {
|
|
3251
3930
|
out.push(chunk);
|
|
@@ -3253,34 +3932,43 @@ function wrapVisible(text, width) {
|
|
|
3253
3932
|
}
|
|
3254
3933
|
return out;
|
|
3255
3934
|
}
|
|
3256
|
-
function
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
}
|
|
3261
|
-
if (p === home) {
|
|
3262
|
-
return "~";
|
|
3263
|
-
}
|
|
3264
|
-
if (p.startsWith(home + "/")) {
|
|
3265
|
-
return "~" + p.slice(home.length);
|
|
3935
|
+
function graphemeSegments(text) {
|
|
3936
|
+
const out = [];
|
|
3937
|
+
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3938
|
+
out.push({ text: segment, width: stringWidth(segment) });
|
|
3266
3939
|
}
|
|
3267
|
-
return
|
|
3940
|
+
return out;
|
|
3268
3941
|
}
|
|
3269
|
-
function truncate(text, max) {
|
|
3942
|
+
function truncate(text, max, opts = {}) {
|
|
3270
3943
|
if (max <= 0) {
|
|
3271
3944
|
return "";
|
|
3272
3945
|
}
|
|
3273
|
-
|
|
3946
|
+
const stripMarkup = opts.stripMarkup === true && hasTkMarkup(text);
|
|
3947
|
+
if (!stripMarkup && text.length <= max && !NON_ASCII.test(text)) {
|
|
3274
3948
|
return text;
|
|
3275
3949
|
}
|
|
3276
|
-
|
|
3950
|
+
if (!stripMarkup) {
|
|
3951
|
+
const visible2 = stringWidth(text);
|
|
3952
|
+
if (visible2 <= max) {
|
|
3953
|
+
return text;
|
|
3954
|
+
}
|
|
3955
|
+
if (max <= 1) {
|
|
3956
|
+
return takeByWidth(text, max);
|
|
3957
|
+
}
|
|
3958
|
+
return takeByWidth(text, max - 1) + "\u2026";
|
|
3959
|
+
}
|
|
3960
|
+
const segments = [...segmentForWidth(text)];
|
|
3961
|
+
let visible = 0;
|
|
3962
|
+
for (const s of segments) {
|
|
3963
|
+
visible += s.width;
|
|
3964
|
+
}
|
|
3277
3965
|
if (visible <= max) {
|
|
3278
3966
|
return text;
|
|
3279
3967
|
}
|
|
3280
3968
|
if (max <= 1) {
|
|
3281
|
-
return
|
|
3969
|
+
return takeFromSegments(segments, max);
|
|
3282
3970
|
}
|
|
3283
|
-
return
|
|
3971
|
+
return takeFromSegments(segments, max - 1) + "\u2026";
|
|
3284
3972
|
}
|
|
3285
3973
|
function takeByWidth(text, budget) {
|
|
3286
3974
|
if (budget <= 0) {
|
|
@@ -3298,6 +3986,21 @@ function takeByWidth(text, budget) {
|
|
|
3298
3986
|
}
|
|
3299
3987
|
return out;
|
|
3300
3988
|
}
|
|
3989
|
+
function takeFromSegments(segments, budget) {
|
|
3990
|
+
if (budget <= 0) {
|
|
3991
|
+
return "";
|
|
3992
|
+
}
|
|
3993
|
+
let out = "";
|
|
3994
|
+
let used = 0;
|
|
3995
|
+
for (const s of segments) {
|
|
3996
|
+
if (used + s.width > budget) {
|
|
3997
|
+
break;
|
|
3998
|
+
}
|
|
3999
|
+
out += s.text;
|
|
4000
|
+
used += s.width;
|
|
4001
|
+
}
|
|
4002
|
+
return out;
|
|
4003
|
+
}
|
|
3301
4004
|
function firstLine2(text) {
|
|
3302
4005
|
const idx = text.indexOf("\n");
|
|
3303
4006
|
return idx === -1 ? text : `${text.slice(0, idx)} \u21B5`;
|
|
@@ -3394,6 +4097,10 @@ function mapKeyName(name) {
|
|
|
3394
4097
|
return "ctrl-o";
|
|
3395
4098
|
case "CTRL_P":
|
|
3396
4099
|
return "ctrl-p";
|
|
4100
|
+
case "CTRL_R":
|
|
4101
|
+
return "ctrl-r";
|
|
4102
|
+
case "CTRL_S":
|
|
4103
|
+
return "ctrl-s";
|
|
3397
4104
|
case "CTRL_U":
|
|
3398
4105
|
return "ctrl-u";
|
|
3399
4106
|
case "CTRL_W":
|
|
@@ -3406,11 +4113,12 @@ function mapKeyName(name) {
|
|
|
3406
4113
|
return null;
|
|
3407
4114
|
}
|
|
3408
4115
|
}
|
|
3409
|
-
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;
|
|
4116
|
+
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
3410
4117
|
var init_screen = __esm({
|
|
3411
4118
|
"src/tui/screen.ts"() {
|
|
3412
4119
|
"use strict";
|
|
3413
4120
|
init_agent_display();
|
|
4121
|
+
init_paths();
|
|
3414
4122
|
init_session();
|
|
3415
4123
|
HEADER_ROWS = 2;
|
|
3416
4124
|
BANNER_ROWS = 1;
|
|
@@ -3437,6 +4145,7 @@ var init_screen = __esm({
|
|
|
3437
4145
|
streamingActive = false;
|
|
3438
4146
|
lastPromptRows = 0;
|
|
3439
4147
|
queuedTexts = [];
|
|
4148
|
+
lastQueueEditingIndex = -1;
|
|
3440
4149
|
repaintPaused = 0;
|
|
3441
4150
|
repaintPending = false;
|
|
3442
4151
|
lastRepaintAt = 0;
|
|
@@ -3453,6 +4162,12 @@ var init_screen = __esm({
|
|
|
3453
4162
|
lineIds = /* @__PURE__ */ new WeakMap();
|
|
3454
4163
|
wrapCache = /* @__PURE__ */ new Map();
|
|
3455
4164
|
wrapCacheWidth = 0;
|
|
4165
|
+
// For each wrapped chunk (produced by wrapOne), record the source
|
|
4166
|
+
// line's id and the col offset where this chunk starts in the source
|
|
4167
|
+
// body. Used by the active-match highlight in scrollback search to
|
|
4168
|
+
// map currentMatch (sourceLineId, sourceCol) onto the wrapped chunk
|
|
4169
|
+
// that owns it without scanning the wrap cache.
|
|
4170
|
+
wrapOrigin = /* @__PURE__ */ new WeakMap();
|
|
3456
4171
|
// Per-row signature of what was painted to each terminal row on the
|
|
3457
4172
|
// previous repaint. drawX methods funnel through paintRow(), which
|
|
3458
4173
|
// skips the moveTo+eraseLineAfter+write sequence when the new
|
|
@@ -3470,10 +4185,30 @@ var init_screen = __esm({
|
|
|
3470
4185
|
// above the bottom. Mouse wheel and PgUp/PgDn adjust this; new content
|
|
3471
4186
|
// pushes the view down naturally when at 0.
|
|
3472
4187
|
scrollOffset = 0;
|
|
4188
|
+
// Scrollback search state. While active the prompt area is taken over
|
|
4189
|
+
// by a single-row search input (drawSearchPrompt) and matches in the
|
|
4190
|
+
// visible scrollback are rendered with a background-highlight style.
|
|
4191
|
+
// baselineScroll captures the scrollOffset at the moment the user
|
|
4192
|
+
// engaged search so cancel can restore the view.
|
|
4193
|
+
scrollbackSearch = null;
|
|
4194
|
+
// Lowercased search term used by drawScrollback to drive per-row
|
|
4195
|
+
// highlight rendering. Mirrors scrollbackSearch?.term but cached as a
|
|
4196
|
+
// separate field so the per-row signature can include it cheaply.
|
|
4197
|
+
scrollbackHighlight = null;
|
|
4198
|
+
// Right-side banner slot. Three sources, in priority order:
|
|
4199
|
+
// 1. Active scrollback search term (auto, from this.scrollbackSearch)
|
|
4200
|
+
// 2. External search indicator pushed by the app while prompt-
|
|
4201
|
+
// history reverse-search is active (gives that mode visible
|
|
4202
|
+
// feedback for its otherwise-hidden query)
|
|
4203
|
+
// 3. Transient notification set via notify(), auto-cleared after
|
|
4204
|
+
// durationMs
|
|
4205
|
+
bannerNotification = null;
|
|
4206
|
+
bannerNotificationTimer = null;
|
|
4207
|
+
bannerSearchIndicator = null;
|
|
3473
4208
|
banner = {
|
|
3474
4209
|
status: "ready",
|
|
3475
4210
|
planMode: false,
|
|
3476
|
-
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D
|
|
4211
|
+
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
3477
4212
|
queued: 0
|
|
3478
4213
|
};
|
|
3479
4214
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -3493,12 +4228,14 @@ var init_screen = __esm({
|
|
|
3493
4228
|
pasteActive = false;
|
|
3494
4229
|
pasteBuffer = "";
|
|
3495
4230
|
rawStdinHandler;
|
|
4231
|
+
mouseEnabled;
|
|
3496
4232
|
constructor(opts) {
|
|
3497
4233
|
this.term = opts.term;
|
|
3498
4234
|
this.dispatcher = opts.dispatcher;
|
|
3499
4235
|
this.onKey = opts.onKey;
|
|
3500
4236
|
this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
|
|
3501
4237
|
this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
|
|
4238
|
+
this.mouseEnabled = opts.mouse ?? true;
|
|
3502
4239
|
this.resizeHandler = () => this.repaint();
|
|
3503
4240
|
this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
|
|
3504
4241
|
this.mouseHandler = (name) => this.handleMouse(name);
|
|
@@ -3515,10 +4252,16 @@ var init_screen = __esm({
|
|
|
3515
4252
|
this.lastFrameH = 0;
|
|
3516
4253
|
this.lastWindowTitle = null;
|
|
3517
4254
|
process.stdout.write("\x1B[?7l");
|
|
3518
|
-
this.
|
|
4255
|
+
if (this.mouseEnabled) {
|
|
4256
|
+
this.term.grabInput({ mouse: "button" });
|
|
4257
|
+
} else {
|
|
4258
|
+
this.term.grabInput(true);
|
|
4259
|
+
}
|
|
3519
4260
|
this.term.hideCursor(false);
|
|
3520
4261
|
this.term.on("key", this.keyHandler);
|
|
3521
|
-
|
|
4262
|
+
if (this.mouseEnabled) {
|
|
4263
|
+
this.term.on("mouse", this.mouseHandler);
|
|
4264
|
+
}
|
|
3522
4265
|
this.term.on("resize", this.resizeHandler);
|
|
3523
4266
|
this.installBracketedPaste();
|
|
3524
4267
|
this.repaint();
|
|
@@ -3528,9 +4271,15 @@ var init_screen = __esm({
|
|
|
3528
4271
|
return;
|
|
3529
4272
|
}
|
|
3530
4273
|
this.started = false;
|
|
4274
|
+
if (this.bannerNotificationTimer) {
|
|
4275
|
+
clearTimeout(this.bannerNotificationTimer);
|
|
4276
|
+
this.bannerNotificationTimer = null;
|
|
4277
|
+
}
|
|
3531
4278
|
this.uninstallBracketedPaste();
|
|
3532
4279
|
this.term.off("key", this.keyHandler);
|
|
3533
|
-
|
|
4280
|
+
if (this.mouseEnabled) {
|
|
4281
|
+
this.term.off("mouse", this.mouseHandler);
|
|
4282
|
+
}
|
|
3534
4283
|
this.term.off("resize", this.resizeHandler);
|
|
3535
4284
|
this.term.grabInput(false);
|
|
3536
4285
|
this.term.hideCursor(false);
|
|
@@ -3800,6 +4549,58 @@ var init_screen = __esm({
|
|
|
3800
4549
|
this.drawBanner();
|
|
3801
4550
|
this.placeCursor();
|
|
3802
4551
|
}
|
|
4552
|
+
// Transient right-side banner message. Cleared automatically after
|
|
4553
|
+
// durationMs (default 4s). Each call resets the timer, so rapid
|
|
4554
|
+
// successive notifications coalesce on the latest text. Active
|
|
4555
|
+
// scrollback / prompt-history search indicators take priority over
|
|
4556
|
+
// notifications, so a notification queued during search is held
|
|
4557
|
+
// behind it and visible once search exits — unless its timer fires
|
|
4558
|
+
// first, in which case it's dropped.
|
|
4559
|
+
notify(text, durationMs = 4e3) {
|
|
4560
|
+
if (this.bannerNotificationTimer) {
|
|
4561
|
+
clearTimeout(this.bannerNotificationTimer);
|
|
4562
|
+
}
|
|
4563
|
+
this.bannerNotification = text;
|
|
4564
|
+
this.bannerNotificationTimer = setTimeout(() => {
|
|
4565
|
+
this.bannerNotification = null;
|
|
4566
|
+
this.bannerNotificationTimer = null;
|
|
4567
|
+
this.drawBanner();
|
|
4568
|
+
this.placeCursor();
|
|
4569
|
+
}, durationMs);
|
|
4570
|
+
this.drawBanner();
|
|
4571
|
+
this.placeCursor();
|
|
4572
|
+
}
|
|
4573
|
+
// Pushed by the app each onKey tick to reflect prompt-history
|
|
4574
|
+
// reverse-search state in the banner — the only place that mode's
|
|
4575
|
+
// query is visible. Pass null when not searching.
|
|
4576
|
+
setBannerSearchIndicator(text) {
|
|
4577
|
+
if (this.bannerSearchIndicator === text) {
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
this.bannerSearchIndicator = text;
|
|
4581
|
+
this.drawBanner();
|
|
4582
|
+
this.placeCursor();
|
|
4583
|
+
}
|
|
4584
|
+
// Computes what (if anything) the right-side banner slot should show
|
|
4585
|
+
// this paint. Priority: scrollback search term > prompt-history
|
|
4586
|
+
// indicator > notification. Scrollback gets a "N/M" counter suffix
|
|
4587
|
+
// since the user can't see which match they're on from the highlight
|
|
4588
|
+
// alone; prompt-history's match is visible in the buffer, so no
|
|
4589
|
+
// counter needed there.
|
|
4590
|
+
bannerRightContent() {
|
|
4591
|
+
if (this.scrollbackSearch !== null) {
|
|
4592
|
+
const sb = this.scrollbackSearch;
|
|
4593
|
+
const counter = sb.matches.length > 0 ? ` ${sb.matchIndex + 1}/${sb.matches.length}` : sb.term.length === 0 ? "" : " 0/0";
|
|
4594
|
+
return { text: `\u{1F50D} ${sb.term}${counter}`, kind: "search" };
|
|
4595
|
+
}
|
|
4596
|
+
if (this.bannerSearchIndicator !== null) {
|
|
4597
|
+
return { text: `\u{1F50D} ${this.bannerSearchIndicator}`, kind: "search" };
|
|
4598
|
+
}
|
|
4599
|
+
if (this.bannerNotification !== null) {
|
|
4600
|
+
return { text: this.bannerNotification, kind: "notify" };
|
|
4601
|
+
}
|
|
4602
|
+
return null;
|
|
4603
|
+
}
|
|
3803
4604
|
clearScrollback() {
|
|
3804
4605
|
this.lines = [];
|
|
3805
4606
|
this.keyedBlocks.clear();
|
|
@@ -3880,6 +4681,7 @@ var init_screen = __esm({
|
|
|
3880
4681
|
}
|
|
3881
4682
|
setQueuedPrompts(texts) {
|
|
3882
4683
|
this.queuedTexts = [...texts];
|
|
4684
|
+
this.lastQueueEditingIndex = this.dispatcher.state().queueIndex;
|
|
3883
4685
|
this.repaint();
|
|
3884
4686
|
}
|
|
3885
4687
|
// While a permission prompt is active, the prompt area is replaced with
|
|
@@ -3932,12 +4734,19 @@ var init_screen = __esm({
|
|
|
3932
4734
|
// row count changed (alt+enter added a line, backspace joined two), the
|
|
3933
4735
|
// separator and scrollback bottom shift, so we need a full repaint;
|
|
3934
4736
|
// otherwise an in-place prompt redraw is enough. (Queued-zone changes
|
|
3935
|
-
// already trigger a full repaint via setQueuedPrompts.)
|
|
4737
|
+
// already trigger a full repaint via setQueuedPrompts.) Queue-edit
|
|
4738
|
+
// navigation may also change which queued row is marked, so check
|
|
4739
|
+
// for that and redraw just that zone in-place.
|
|
3936
4740
|
refreshPrompt() {
|
|
3937
4741
|
if (this.promptRows() !== this.lastPromptRows) {
|
|
3938
4742
|
this.repaint();
|
|
3939
4743
|
return;
|
|
3940
4744
|
}
|
|
4745
|
+
const editingIndex = this.dispatcher.state().queueIndex;
|
|
4746
|
+
if (editingIndex !== this.lastQueueEditingIndex) {
|
|
4747
|
+
this.lastQueueEditingIndex = editingIndex;
|
|
4748
|
+
this.drawQueuedZone();
|
|
4749
|
+
}
|
|
3941
4750
|
this.drawPrompt();
|
|
3942
4751
|
this.placeCursor();
|
|
3943
4752
|
}
|
|
@@ -3973,6 +4782,9 @@ var init_screen = __esm({
|
|
|
3973
4782
|
if (delta === 0) {
|
|
3974
4783
|
return;
|
|
3975
4784
|
}
|
|
4785
|
+
if (this.scrollbackSearch !== null) {
|
|
4786
|
+
this.acceptScrollbackSearch();
|
|
4787
|
+
}
|
|
3976
4788
|
const max = this.maxScrollOffset();
|
|
3977
4789
|
const next = Math.min(max, Math.max(0, this.scrollOffset + delta));
|
|
3978
4790
|
if (next === this.scrollOffset) {
|
|
@@ -3982,12 +4794,241 @@ var init_screen = __esm({
|
|
|
3982
4794
|
this.repaint();
|
|
3983
4795
|
}
|
|
3984
4796
|
scrollToBottom() {
|
|
4797
|
+
if (this.scrollbackSearch !== null) {
|
|
4798
|
+
this.acceptScrollbackSearch();
|
|
4799
|
+
}
|
|
3985
4800
|
if (this.scrollOffset === 0) {
|
|
3986
4801
|
return;
|
|
3987
4802
|
}
|
|
3988
4803
|
this.scrollOffset = 0;
|
|
3989
4804
|
this.repaint();
|
|
3990
4805
|
}
|
|
4806
|
+
scrollToTop() {
|
|
4807
|
+
if (this.scrollbackSearch !== null) {
|
|
4808
|
+
this.acceptScrollbackSearch();
|
|
4809
|
+
}
|
|
4810
|
+
const max = this.maxScrollOffset();
|
|
4811
|
+
if (this.scrollOffset === max) {
|
|
4812
|
+
return;
|
|
4813
|
+
}
|
|
4814
|
+
this.scrollOffset = max;
|
|
4815
|
+
this.repaint();
|
|
4816
|
+
}
|
|
4817
|
+
// True iff the user is scrolled above the live tail — gates the
|
|
4818
|
+
// app-level decision of whether ^r engages scrollback search vs.
|
|
4819
|
+
// prompt-history search.
|
|
4820
|
+
isScrolledBack() {
|
|
4821
|
+
return this.scrollOffset > 0;
|
|
4822
|
+
}
|
|
4823
|
+
// True iff a scrollback search is currently active. Used by the app
|
|
4824
|
+
// to decide whether to keep routing keys into search vs. the prompt
|
|
4825
|
+
// dispatcher.
|
|
4826
|
+
isScrollbackSearchActive() {
|
|
4827
|
+
return this.scrollbackSearch !== null;
|
|
4828
|
+
}
|
|
4829
|
+
// Engage scrollback reverse-search. Captures the current scroll
|
|
4830
|
+
// position so cancel can restore it, and seeds an empty search term
|
|
4831
|
+
// (the prompt row renders the search input immediately so the user
|
|
4832
|
+
// sees the entry). Idempotent: no-op when already active.
|
|
4833
|
+
enterScrollbackSearch() {
|
|
4834
|
+
if (this.scrollbackSearch !== null) {
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
this.scrollbackSearch = {
|
|
4838
|
+
term: "",
|
|
4839
|
+
matchIndex: 0,
|
|
4840
|
+
matches: [],
|
|
4841
|
+
baselineScroll: this.scrollOffset
|
|
4842
|
+
};
|
|
4843
|
+
this.scrollbackHighlight = null;
|
|
4844
|
+
this.repaint();
|
|
4845
|
+
}
|
|
4846
|
+
// Update the search term and recompute matches. Walks `lines` from
|
|
4847
|
+
// the tail (newest) toward the head (oldest), pushing every case-
|
|
4848
|
+
// insensitive substring hit. Snaps the viewport to the newest match
|
|
4849
|
+
// when found. Called per keystroke; sub-millisecond on typical
|
|
4850
|
+
// scrollback sizes.
|
|
4851
|
+
updateScrollbackSearchTerm(term) {
|
|
4852
|
+
if (this.scrollbackSearch === null) {
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
4855
|
+
const lowered = term.toLowerCase();
|
|
4856
|
+
const matches = [];
|
|
4857
|
+
if (lowered.length > 0) {
|
|
4858
|
+
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
4859
|
+
const line = this.lines[i];
|
|
4860
|
+
if (!line || line.body.length === 0) {
|
|
4861
|
+
continue;
|
|
4862
|
+
}
|
|
4863
|
+
if (line.ansi) {
|
|
4864
|
+
continue;
|
|
4865
|
+
}
|
|
4866
|
+
const hay = line.body.toLowerCase();
|
|
4867
|
+
const lineCols = [];
|
|
4868
|
+
let pos = 0;
|
|
4869
|
+
while (pos < hay.length) {
|
|
4870
|
+
const found = hay.indexOf(lowered, pos);
|
|
4871
|
+
if (found === -1) {
|
|
4872
|
+
break;
|
|
4873
|
+
}
|
|
4874
|
+
lineCols.push(found);
|
|
4875
|
+
pos = found + lowered.length;
|
|
4876
|
+
}
|
|
4877
|
+
for (let j = lineCols.length - 1; j >= 0; j--) {
|
|
4878
|
+
matches.push({ lineIdx: i, col: lineCols[j] });
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
this.scrollbackSearch.term = term;
|
|
4883
|
+
this.scrollbackSearch.matches = matches;
|
|
4884
|
+
this.scrollbackSearch.matchIndex = 0;
|
|
4885
|
+
this.scrollbackHighlight = lowered.length > 0 ? lowered : null;
|
|
4886
|
+
if (matches.length > 0) {
|
|
4887
|
+
this.scrollToMatch(matches[0]);
|
|
4888
|
+
}
|
|
4889
|
+
this.repaint();
|
|
4890
|
+
}
|
|
4891
|
+
// Advance to the next-older match (called for repeated ^r). Stops at
|
|
4892
|
+
// the oldest match (does not wrap). No-op when there are no matches
|
|
4893
|
+
// or search is inactive.
|
|
4894
|
+
advanceScrollbackSearch() {
|
|
4895
|
+
if (this.scrollbackSearch === null || this.scrollbackSearch.matches.length === 0) {
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
const nextIdx = Math.min(
|
|
4899
|
+
this.scrollbackSearch.matches.length - 1,
|
|
4900
|
+
this.scrollbackSearch.matchIndex + 1
|
|
4901
|
+
);
|
|
4902
|
+
if (nextIdx === this.scrollbackSearch.matchIndex) {
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4905
|
+
this.scrollbackSearch.matchIndex = nextIdx;
|
|
4906
|
+
this.scrollToMatch(this.scrollbackSearch.matches[nextIdx]);
|
|
4907
|
+
this.repaint();
|
|
4908
|
+
}
|
|
4909
|
+
// Retreat to the previous (newer) match — ^s forward-search. Stops
|
|
4910
|
+
// at the newest match (no wrap).
|
|
4911
|
+
retreatScrollbackSearch() {
|
|
4912
|
+
if (this.scrollbackSearch === null || this.scrollbackSearch.matches.length === 0) {
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
if (this.scrollbackSearch.matchIndex === 0) {
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
this.scrollbackSearch.matchIndex -= 1;
|
|
4919
|
+
this.scrollToMatch(this.scrollbackSearch.matches[this.scrollbackSearch.matchIndex]);
|
|
4920
|
+
this.repaint();
|
|
4921
|
+
}
|
|
4922
|
+
// Exit search keeping the viewport at the current match. Highlight is
|
|
4923
|
+
// cleared so subsequent scrollback content reads normally.
|
|
4924
|
+
acceptScrollbackSearch() {
|
|
4925
|
+
if (this.scrollbackSearch === null) {
|
|
4926
|
+
return;
|
|
4927
|
+
}
|
|
4928
|
+
this.scrollbackSearch = null;
|
|
4929
|
+
this.scrollbackHighlight = null;
|
|
4930
|
+
this.repaint();
|
|
4931
|
+
}
|
|
4932
|
+
// Exit search and restore the viewport to where the user was when
|
|
4933
|
+
// they engaged search.
|
|
4934
|
+
cancelScrollbackSearch() {
|
|
4935
|
+
if (this.scrollbackSearch === null) {
|
|
4936
|
+
return;
|
|
4937
|
+
}
|
|
4938
|
+
const baseline = this.scrollbackSearch.baselineScroll;
|
|
4939
|
+
this.scrollbackSearch = null;
|
|
4940
|
+
this.scrollbackHighlight = null;
|
|
4941
|
+
this.scrollOffset = baseline;
|
|
4942
|
+
this.repaint();
|
|
4943
|
+
}
|
|
4944
|
+
scrollbackSearchTerm() {
|
|
4945
|
+
return this.scrollbackSearch?.term ?? "";
|
|
4946
|
+
}
|
|
4947
|
+
// Source-line identity + col + term length for whichever match is
|
|
4948
|
+
// currently selected (advanced via ^r / retreated via ^s). Used by
|
|
4949
|
+
// drawScrollback to give the current match a distinct highlight
|
|
4950
|
+
// style without disturbing the bulk-highlight on the other matches.
|
|
4951
|
+
currentMatchInfo() {
|
|
4952
|
+
if (this.scrollbackSearch === null || this.scrollbackSearch.matches.length === 0) {
|
|
4953
|
+
return null;
|
|
4954
|
+
}
|
|
4955
|
+
const match = this.scrollbackSearch.matches[this.scrollbackSearch.matchIndex];
|
|
4956
|
+
if (!match) {
|
|
4957
|
+
return null;
|
|
4958
|
+
}
|
|
4959
|
+
const sourceLine = this.lines[match.lineIdx];
|
|
4960
|
+
if (!sourceLine) {
|
|
4961
|
+
return null;
|
|
4962
|
+
}
|
|
4963
|
+
const lineId = this.lineIds.get(sourceLine);
|
|
4964
|
+
if (lineId === void 0) {
|
|
4965
|
+
return null;
|
|
4966
|
+
}
|
|
4967
|
+
return {
|
|
4968
|
+
lineId,
|
|
4969
|
+
col: match.col,
|
|
4970
|
+
length: this.scrollbackSearch.term.length
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
// If `line` is the wrapped chunk that contains the active match,
|
|
4974
|
+
// returns the col within the chunk's body where the match starts;
|
|
4975
|
+
// otherwise null. The chunk's source identity comes from
|
|
4976
|
+
// this.wrapOrigin which wrapOne populates for every wrapped chunk.
|
|
4977
|
+
activeMatchCol(line, info) {
|
|
4978
|
+
if (!line || info === null) {
|
|
4979
|
+
return null;
|
|
4980
|
+
}
|
|
4981
|
+
const origin = this.wrapOrigin.get(line);
|
|
4982
|
+
if (!origin || origin.sourceLineId !== info.lineId) {
|
|
4983
|
+
return null;
|
|
4984
|
+
}
|
|
4985
|
+
const colInChunk = info.col - origin.sourceColOffset;
|
|
4986
|
+
if (colInChunk < 0 || colInChunk >= line.body.length) {
|
|
4987
|
+
return null;
|
|
4988
|
+
}
|
|
4989
|
+
return colInChunk;
|
|
4990
|
+
}
|
|
4991
|
+
// Position scrollOffset so the wrapped row containing the given
|
|
4992
|
+
// (lineIdx, col) lands on a visible row of the scrollback viewport.
|
|
4993
|
+
// Walks wrapTail to count wrapped rows between the target line and
|
|
4994
|
+
// the tail.
|
|
4995
|
+
scrollToMatch(match) {
|
|
4996
|
+
const w = this.term.width;
|
|
4997
|
+
const visibleRows = this.scrollbackVisibleRows();
|
|
4998
|
+
if (visibleRows <= 0) {
|
|
4999
|
+
return;
|
|
5000
|
+
}
|
|
5001
|
+
let rowsBelowMatchLine = 0;
|
|
5002
|
+
for (let i = this.lines.length - 1; i > match.lineIdx; i--) {
|
|
5003
|
+
const line = this.lines[i];
|
|
5004
|
+
if (!line) {
|
|
5005
|
+
continue;
|
|
5006
|
+
}
|
|
5007
|
+
rowsBelowMatchLine += this.wrapOne(line, w).length;
|
|
5008
|
+
}
|
|
5009
|
+
const matchLine = this.lines[match.lineIdx];
|
|
5010
|
+
let rowsWithinMatchLine = 0;
|
|
5011
|
+
if (matchLine) {
|
|
5012
|
+
const wrapped = this.wrapOne(matchLine, w);
|
|
5013
|
+
let consumed = 0;
|
|
5014
|
+
for (let r = 0; r < wrapped.length; r++) {
|
|
5015
|
+
const piece = wrapped[r];
|
|
5016
|
+
if (!piece) {
|
|
5017
|
+
continue;
|
|
5018
|
+
}
|
|
5019
|
+
const bodyLen = piece.body.length;
|
|
5020
|
+
if (match.col < consumed + bodyLen) {
|
|
5021
|
+
rowsWithinMatchLine = wrapped.length - 1 - r;
|
|
5022
|
+
break;
|
|
5023
|
+
}
|
|
5024
|
+
consumed += bodyLen;
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
const target = rowsBelowMatchLine + rowsWithinMatchLine;
|
|
5028
|
+
const desired = Math.max(0, target - Math.floor(visibleRows / 2));
|
|
5029
|
+
const max = this.maxScrollOffset();
|
|
5030
|
+
this.scrollOffset = Math.min(max, desired);
|
|
5031
|
+
}
|
|
3991
5032
|
scrollPageSize() {
|
|
3992
5033
|
return Math.max(1, this.scrollbackVisibleRows() - 2);
|
|
3993
5034
|
}
|
|
@@ -4110,8 +5151,8 @@ var init_screen = __esm({
|
|
|
4110
5151
|
}
|
|
4111
5152
|
if (usage) {
|
|
4112
5153
|
const col = Math.max(1, w - usage.length + 1);
|
|
4113
|
-
this.term.moveTo(col, 1);
|
|
4114
|
-
this.term.dim(usage);
|
|
5154
|
+
this.term.moveTo(col, 1).eraseLineAfter();
|
|
5155
|
+
this.term.dim.noFormat(usage);
|
|
4115
5156
|
}
|
|
4116
5157
|
});
|
|
4117
5158
|
}
|
|
@@ -4142,14 +5183,23 @@ var init_screen = __esm({
|
|
|
4142
5183
|
const start = Math.max(0, end - visibleRows);
|
|
4143
5184
|
const slice = wrapped.slice(start, end);
|
|
4144
5185
|
const padTop = Math.max(0, visibleRows - slice.length);
|
|
5186
|
+
const matchInfo = this.currentMatchInfo();
|
|
5187
|
+
const activeLength = matchInfo?.length ?? 0;
|
|
4145
5188
|
for (let i = 0; i < visibleRows; i++) {
|
|
4146
5189
|
const row = top + i;
|
|
4147
5190
|
const sliceIdx = i - padTop;
|
|
4148
5191
|
const line = sliceIdx >= 0 ? slice[sliceIdx] : void 0;
|
|
4149
|
-
const
|
|
5192
|
+
const activeCol = this.activeMatchCol(line, matchInfo);
|
|
5193
|
+
const sig = formattedLineSig(
|
|
5194
|
+
"sb",
|
|
5195
|
+
w,
|
|
5196
|
+
line,
|
|
5197
|
+
this.scrollbackHighlight,
|
|
5198
|
+
activeCol
|
|
5199
|
+
);
|
|
4150
5200
|
this.paintRow(row, sig, () => {
|
|
4151
5201
|
if (line) {
|
|
4152
|
-
this.writeFormattedLine(line, w);
|
|
5202
|
+
this.writeFormattedLine(line, w, activeCol, activeLength);
|
|
4153
5203
|
}
|
|
4154
5204
|
});
|
|
4155
5205
|
}
|
|
@@ -4215,19 +5265,26 @@ var init_screen = __esm({
|
|
|
4215
5265
|
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
4216
5266
|
const queuedBottom = separatorRow - 1;
|
|
4217
5267
|
const queuedTop = queuedBottom - rows + 1;
|
|
5268
|
+
const editingIndex = this.dispatcher.state().queueIndex;
|
|
4218
5269
|
for (let i = 0; i < rows; i++) {
|
|
4219
5270
|
const row = queuedTop + i;
|
|
4220
5271
|
const text = this.queuedTexts[i];
|
|
4221
5272
|
const isLast = i === rows - 1 && this.queuedTexts.length > MAX_QUEUED_ROWS;
|
|
4222
5273
|
const overflow = this.queuedTexts.length - MAX_QUEUED_ROWS;
|
|
4223
5274
|
const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
|
|
4224
|
-
const
|
|
5275
|
+
const editing = !isLast && i === editingIndex;
|
|
5276
|
+
const sig = text === void 0 ? `queued|${w}|empty` : `queued|${w}|${editing ? "edit" : isLast ? "ovf" : "row"}|${summary}`;
|
|
4225
5277
|
this.paintRow(row, sig, () => {
|
|
4226
5278
|
if (text === void 0) {
|
|
4227
5279
|
return;
|
|
4228
5280
|
}
|
|
4229
|
-
const
|
|
4230
|
-
const padded =
|
|
5281
|
+
const rest = `\u23F3 ${summary}`;
|
|
5282
|
+
const padded = rest + " ".repeat(Math.max(0, w - 1 - rest.length));
|
|
5283
|
+
if (editing) {
|
|
5284
|
+
this.term.bgBlue.brightYellow("\u25B8");
|
|
5285
|
+
} else {
|
|
5286
|
+
this.term.bgBlue(" ");
|
|
5287
|
+
}
|
|
4231
5288
|
this.term.bgBlue.brightWhite.noFormat(padded);
|
|
4232
5289
|
});
|
|
4233
5290
|
}
|
|
@@ -4342,7 +5399,9 @@ var init_screen = __esm({
|
|
|
4342
5399
|
const row = this.term.height;
|
|
4343
5400
|
const w = this.term.width;
|
|
4344
5401
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
4345
|
-
const
|
|
5402
|
+
const right = this.bannerRightContent();
|
|
5403
|
+
const rightSig = right ? `${right.kind}|${right.text}` : "";
|
|
5404
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.planMode ? "1" : "0"}|${this.banner.hint}|` + rightSig;
|
|
4346
5405
|
this.paintRow(row, sig, () => {
|
|
4347
5406
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
4348
5407
|
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
@@ -4369,6 +5428,16 @@ var init_screen = __esm({
|
|
|
4369
5428
|
this.term.dim(planLabel);
|
|
4370
5429
|
}
|
|
4371
5430
|
this.term(" \xB7 ").dim(this.banner.hint);
|
|
5431
|
+
if (right) {
|
|
5432
|
+
const visibleWidth = stringWidth(right.text);
|
|
5433
|
+
const col = Math.max(1, w - visibleWidth + 1);
|
|
5434
|
+
this.term.moveTo(col, row).eraseLineAfter();
|
|
5435
|
+
if (right.kind === "search") {
|
|
5436
|
+
this.term.brightCyan.noFormat(right.text);
|
|
5437
|
+
} else {
|
|
5438
|
+
this.term.brightYellow.noFormat(right.text);
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
4372
5441
|
});
|
|
4373
5442
|
}
|
|
4374
5443
|
placeCursor() {
|
|
@@ -4384,6 +5453,11 @@ var init_screen = __esm({
|
|
|
4384
5453
|
this.term.moveTo(2, top2);
|
|
4385
5454
|
return;
|
|
4386
5455
|
}
|
|
5456
|
+
if (this.scrollbackSearch) {
|
|
5457
|
+
this.term.hideCursor(true);
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
this.term.hideCursor(false);
|
|
4387
5461
|
const w = this.term.width;
|
|
4388
5462
|
const room = Math.max(1, w - 2);
|
|
4389
5463
|
const state = this.dispatcher.state();
|
|
@@ -4470,8 +5544,10 @@ var init_screen = __esm({
|
|
|
4470
5544
|
}
|
|
4471
5545
|
const prefix = line.prefix ?? "";
|
|
4472
5546
|
const room = Math.max(1, width - prefix.length);
|
|
4473
|
-
const
|
|
5547
|
+
const stripMarkup = line.bodyStyle === "agent";
|
|
5548
|
+
const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room, { stripMarkup });
|
|
4474
5549
|
const wrapped = [];
|
|
5550
|
+
let scanPos = 0;
|
|
4475
5551
|
for (let i = 0; i < chunks.length; i++) {
|
|
4476
5552
|
const chunk = chunks[i] ?? "";
|
|
4477
5553
|
const wrappedLine = {
|
|
@@ -4490,6 +5566,15 @@ var init_screen = __esm({
|
|
|
4490
5566
|
if (line.ansi) {
|
|
4491
5567
|
wrappedLine.ansi = true;
|
|
4492
5568
|
}
|
|
5569
|
+
if (id !== void 0 && chunk.length > 0) {
|
|
5570
|
+
const found = line.body.indexOf(chunk, scanPos);
|
|
5571
|
+
const colOffset = found === -1 ? scanPos : found;
|
|
5572
|
+
this.wrapOrigin.set(wrappedLine, {
|
|
5573
|
+
sourceLineId: id,
|
|
5574
|
+
sourceColOffset: colOffset
|
|
5575
|
+
});
|
|
5576
|
+
scanPos = colOffset + chunk.length;
|
|
5577
|
+
}
|
|
4493
5578
|
wrapped.push(wrappedLine);
|
|
4494
5579
|
}
|
|
4495
5580
|
if (id !== void 0) {
|
|
@@ -4497,13 +5582,25 @@ var init_screen = __esm({
|
|
|
4497
5582
|
}
|
|
4498
5583
|
return wrapped;
|
|
4499
5584
|
}
|
|
4500
|
-
writeFormattedLine(line, width) {
|
|
5585
|
+
writeFormattedLine(line, width, activeMatchCol = null, activeMatchLength = 0) {
|
|
4501
5586
|
if (line.prefix) {
|
|
4502
5587
|
writeStyled(this.term, line.prefix, line.prefixStyle ?? line.bodyStyle);
|
|
4503
5588
|
}
|
|
4504
5589
|
const remaining = Math.max(0, width - (line.prefix?.length ?? 0));
|
|
4505
|
-
const
|
|
4506
|
-
|
|
5590
|
+
const stripMarkup = line.bodyStyle === "agent";
|
|
5591
|
+
const bodyText = line.ansi ? line.body : truncate(line.body, remaining, { stripMarkup });
|
|
5592
|
+
if (this.scrollbackHighlight !== null && !line.ansi) {
|
|
5593
|
+
writeBodyWithHighlight(
|
|
5594
|
+
this.term,
|
|
5595
|
+
bodyText,
|
|
5596
|
+
line.bodyStyle,
|
|
5597
|
+
this.scrollbackHighlight,
|
|
5598
|
+
activeMatchCol,
|
|
5599
|
+
activeMatchLength
|
|
5600
|
+
);
|
|
5601
|
+
} else {
|
|
5602
|
+
writeStyled(this.term, bodyText, line.bodyStyle);
|
|
5603
|
+
}
|
|
4507
5604
|
if (line.fillRow) {
|
|
4508
5605
|
const visible = line.ansi ? stringWidth(bodyText) : bodyText.length;
|
|
4509
5606
|
const pad = remaining - visible;
|
|
@@ -4518,6 +5615,7 @@ var init_screen = __esm({
|
|
|
4518
5615
|
};
|
|
4519
5616
|
NON_ASCII = /[^\x20-\x7e]/;
|
|
4520
5617
|
SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
5618
|
+
TK_MARKUP_STYLE_CHAR = /[a-zA-Z+\-:_!#/]/;
|
|
4521
5619
|
shortId = stripHydraSessionPrefix;
|
|
4522
5620
|
}
|
|
4523
5621
|
});
|
|
@@ -4533,8 +5631,25 @@ var init_input = __esm({
|
|
|
4533
5631
|
col = 0;
|
|
4534
5632
|
planMode = false;
|
|
4535
5633
|
historyIndex = -1;
|
|
5634
|
+
// Queue editing: when the user walks Up past row 0 with queued prompts
|
|
5635
|
+
// present, the most-recently-queued item lands in the buffer and
|
|
5636
|
+
// queueIndex tracks which slot of `queue` is being edited. Enter submits
|
|
5637
|
+
// the edit (queue-edit) or, on an empty buffer, drops the slot
|
|
5638
|
+
// (queue-remove). -1 means not editing a queue slot.
|
|
5639
|
+
queueIndex = -1;
|
|
4536
5640
|
savedDraft = null;
|
|
4537
5641
|
history = [];
|
|
5642
|
+
// Active reverse-incremental search over `history`. Set when ^r is
|
|
5643
|
+
// pressed; cleared when the user accepts (Enter / typing / arrows)
|
|
5644
|
+
// or cancels (ESC). `query` is the lowercased substring matched
|
|
5645
|
+
// against history entries; `matchIndices` are history indices in
|
|
5646
|
+
// newest→oldest order; `cursor` is the current index into that list.
|
|
5647
|
+
// `savedDraft` snapshots the buffer/cursor at the moment search
|
|
5648
|
+
// began so ESC can restore it.
|
|
5649
|
+
historySearch = null;
|
|
5650
|
+
// Waiting queue snapshot (excludes the in-flight head). Newest item lives
|
|
5651
|
+
// at the end so Up walks the array right-to-left.
|
|
5652
|
+
queue = [];
|
|
4538
5653
|
turnRunning = false;
|
|
4539
5654
|
// Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
|
|
4540
5655
|
// here so ^Y can yank it back. Standard readline keeps a stack; we
|
|
@@ -4550,7 +5665,9 @@ var init_input = __esm({
|
|
|
4550
5665
|
row: this.row,
|
|
4551
5666
|
col: this.col,
|
|
4552
5667
|
planMode: this.planMode,
|
|
4553
|
-
historyIndex: this.historyIndex
|
|
5668
|
+
historyIndex: this.historyIndex,
|
|
5669
|
+
queueIndex: this.queueIndex,
|
|
5670
|
+
historySearchQuery: this.historySearch?.query ?? null
|
|
4554
5671
|
};
|
|
4555
5672
|
}
|
|
4556
5673
|
setTurnRunning(running) {
|
|
@@ -4560,6 +5677,17 @@ var init_input = __esm({
|
|
|
4560
5677
|
this.history = [...history];
|
|
4561
5678
|
this.historyIndex = -1;
|
|
4562
5679
|
this.savedDraft = null;
|
|
5680
|
+
this.historySearch = null;
|
|
5681
|
+
}
|
|
5682
|
+
// Snapshot of the waiting queue (head excluded). Called by the app after
|
|
5683
|
+
// every queue mutation so Up/Down can walk a fresh view. queueIndex is
|
|
5684
|
+
// only invalidated when it falls outside the new bounds — staying in
|
|
5685
|
+
// bounds preserves the user's edit if the queue grew or stayed put.
|
|
5686
|
+
setQueue(queue) {
|
|
5687
|
+
this.queue = [...queue];
|
|
5688
|
+
if (this.queueIndex >= this.queue.length) {
|
|
5689
|
+
this.queueIndex = -1;
|
|
5690
|
+
}
|
|
4563
5691
|
}
|
|
4564
5692
|
// Replace the contents of the first row, leaving subsequent rows alone.
|
|
4565
5693
|
// Used by slash-command completion: the partial /foo gets swapped for the
|
|
@@ -4570,7 +5698,52 @@ var init_input = __esm({
|
|
|
4570
5698
|
this.col = text.length;
|
|
4571
5699
|
}
|
|
4572
5700
|
}
|
|
5701
|
+
// Public seed for the buffer (used for Escape pre-fill). Treated like a
|
|
5702
|
+
// fresh draft: nav state and any saved draft are cleared, cursor lands
|
|
5703
|
+
// at the end so the user can edit immediately.
|
|
5704
|
+
setBuffer(text) {
|
|
5705
|
+
this.loadEntry(text);
|
|
5706
|
+
this.historyIndex = -1;
|
|
5707
|
+
this.queueIndex = -1;
|
|
5708
|
+
this.savedDraft = null;
|
|
5709
|
+
this.historySearch = null;
|
|
5710
|
+
}
|
|
4573
5711
|
feed(event) {
|
|
5712
|
+
if (this.historySearch !== null) {
|
|
5713
|
+
if (event.type === "char") {
|
|
5714
|
+
return this.mutateHistorySearchQuery(
|
|
5715
|
+
this.historySearch.query + event.ch.toLowerCase()
|
|
5716
|
+
);
|
|
5717
|
+
}
|
|
5718
|
+
if (event.type === "paste") {
|
|
5719
|
+
return this.mutateHistorySearchQuery(
|
|
5720
|
+
this.historySearch.query + event.text.replace(/\n/g, " ").toLowerCase()
|
|
5721
|
+
);
|
|
5722
|
+
}
|
|
5723
|
+
if (event.type === "key") {
|
|
5724
|
+
if (event.name === "ctrl-r") {
|
|
5725
|
+
return this.advanceHistorySearch();
|
|
5726
|
+
}
|
|
5727
|
+
if (event.name === "ctrl-s") {
|
|
5728
|
+
this.retreatHistorySearch();
|
|
5729
|
+
return [];
|
|
5730
|
+
}
|
|
5731
|
+
if (event.name === "escape") {
|
|
5732
|
+
this.cancelHistorySearch();
|
|
5733
|
+
return [];
|
|
5734
|
+
}
|
|
5735
|
+
if (event.name === "backspace") {
|
|
5736
|
+
if (this.historySearch.query.length === 0) {
|
|
5737
|
+
this.cancelHistorySearch();
|
|
5738
|
+
return [];
|
|
5739
|
+
}
|
|
5740
|
+
return this.mutateHistorySearchQuery(
|
|
5741
|
+
this.historySearch.query.slice(0, -1)
|
|
5742
|
+
);
|
|
5743
|
+
}
|
|
5744
|
+
this.historySearch = null;
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
4574
5747
|
if (event.type === "char") {
|
|
4575
5748
|
this.insertChar(event.ch);
|
|
4576
5749
|
return [];
|
|
@@ -4607,14 +5780,16 @@ var init_input = __esm({
|
|
|
4607
5780
|
case "right":
|
|
4608
5781
|
this.moveRight();
|
|
4609
5782
|
return [];
|
|
4610
|
-
case "home":
|
|
4611
5783
|
case "ctrl-a":
|
|
4612
5784
|
this.col = 0;
|
|
4613
5785
|
return [];
|
|
4614
|
-
case "end":
|
|
4615
5786
|
case "ctrl-e":
|
|
4616
5787
|
this.col = this.currentLine().length;
|
|
4617
5788
|
return [];
|
|
5789
|
+
case "home":
|
|
5790
|
+
return this.handleHome();
|
|
5791
|
+
case "end":
|
|
5792
|
+
return this.handleEnd();
|
|
4618
5793
|
case "ctrl-b":
|
|
4619
5794
|
this.moveLeft();
|
|
4620
5795
|
return [];
|
|
@@ -4637,11 +5812,19 @@ var init_input = __esm({
|
|
|
4637
5812
|
case "ctrl-c":
|
|
4638
5813
|
return this.handleCtrlC();
|
|
4639
5814
|
case "ctrl-d":
|
|
4640
|
-
|
|
5815
|
+
if (this.bufferIsEmpty()) {
|
|
5816
|
+
return [{ type: "exit" }];
|
|
5817
|
+
}
|
|
5818
|
+
this.deleteForward();
|
|
5819
|
+
return [];
|
|
4641
5820
|
case "ctrl-l":
|
|
4642
5821
|
return [{ type: "redraw" }];
|
|
4643
5822
|
case "ctrl-p":
|
|
4644
5823
|
return [{ type: "switch-session" }];
|
|
5824
|
+
case "ctrl-r":
|
|
5825
|
+
return this.startHistorySearch();
|
|
5826
|
+
case "ctrl-s":
|
|
5827
|
+
return [];
|
|
4645
5828
|
case "ctrl-u":
|
|
4646
5829
|
this.killLine();
|
|
4647
5830
|
return [];
|
|
@@ -4652,6 +5835,9 @@ var init_input = __esm({
|
|
|
4652
5835
|
this.yank();
|
|
4653
5836
|
return [];
|
|
4654
5837
|
case "escape":
|
|
5838
|
+
if (this.turnRunning) {
|
|
5839
|
+
return [{ type: "cancel", prefill: true }];
|
|
5840
|
+
}
|
|
4655
5841
|
return [];
|
|
4656
5842
|
}
|
|
4657
5843
|
}
|
|
@@ -4672,7 +5858,9 @@ var init_input = __esm({
|
|
|
4672
5858
|
this.row = 0;
|
|
4673
5859
|
this.col = 0;
|
|
4674
5860
|
this.historyIndex = -1;
|
|
5861
|
+
this.queueIndex = -1;
|
|
4675
5862
|
this.savedDraft = null;
|
|
5863
|
+
this.historySearch = null;
|
|
4676
5864
|
}
|
|
4677
5865
|
insertChar(ch) {
|
|
4678
5866
|
if (ch.length === 0) {
|
|
@@ -4805,50 +5993,92 @@ var init_input = __esm({
|
|
|
4805
5993
|
this.col = 0;
|
|
4806
5994
|
}
|
|
4807
5995
|
}
|
|
4808
|
-
// Up
|
|
4809
|
-
//
|
|
5996
|
+
// Up walks the navigation stack from newest to oldest: pending queue
|
|
5997
|
+
// items first (so the user can edit something they just enqueued),
|
|
5998
|
+
// then prompt history. Cursor movement within a multi-line buffer
|
|
5999
|
+
// takes priority when not already navigating.
|
|
4810
6000
|
handleUp() {
|
|
4811
6001
|
if (this.row > 0) {
|
|
4812
6002
|
this.row -= 1;
|
|
4813
6003
|
this.col = Math.min(this.col, this.currentLine().length);
|
|
4814
6004
|
return [];
|
|
4815
6005
|
}
|
|
4816
|
-
if (this.
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
6006
|
+
if (this.queueIndex === -1 && this.historyIndex === -1) {
|
|
6007
|
+
if (this.queue.length === 0 && this.history.length === 0) {
|
|
6008
|
+
return [];
|
|
6009
|
+
}
|
|
4820
6010
|
this.savedDraft = {
|
|
4821
6011
|
buffer: [...this.buffer],
|
|
4822
6012
|
row: this.row,
|
|
4823
6013
|
col: this.col
|
|
4824
6014
|
};
|
|
6015
|
+
if (this.queue.length > 0) {
|
|
6016
|
+
this.queueIndex = this.queue.length - 1;
|
|
6017
|
+
this.loadEntry(this.queue[this.queueIndex] ?? "");
|
|
6018
|
+
} else {
|
|
6019
|
+
this.historyIndex = this.history.length - 1;
|
|
6020
|
+
this.loadEntry(this.history[this.historyIndex] ?? "");
|
|
6021
|
+
}
|
|
6022
|
+
return [];
|
|
6023
|
+
}
|
|
6024
|
+
if (this.queueIndex >= 0) {
|
|
6025
|
+
if (this.queueIndex > 0) {
|
|
6026
|
+
this.queueIndex -= 1;
|
|
6027
|
+
this.loadEntry(this.queue[this.queueIndex] ?? "");
|
|
6028
|
+
return [];
|
|
6029
|
+
}
|
|
6030
|
+
if (this.history.length === 0) {
|
|
6031
|
+
return [];
|
|
6032
|
+
}
|
|
6033
|
+
this.queueIndex = -1;
|
|
4825
6034
|
this.historyIndex = this.history.length - 1;
|
|
4826
|
-
|
|
4827
|
-
this.historyIndex -= 1;
|
|
4828
|
-
} else {
|
|
6035
|
+
this.loadEntry(this.history[this.historyIndex] ?? "");
|
|
4829
6036
|
return [];
|
|
4830
6037
|
}
|
|
4831
|
-
|
|
6038
|
+
if (this.historyIndex > 0) {
|
|
6039
|
+
this.historyIndex -= 1;
|
|
6040
|
+
this.loadEntry(this.history[this.historyIndex] ?? "");
|
|
6041
|
+
}
|
|
4832
6042
|
return [];
|
|
4833
6043
|
}
|
|
4834
|
-
// Down
|
|
4835
|
-
//
|
|
4836
|
-
//
|
|
6044
|
+
// Down reverses the Up walk: history (older → newer), then queue
|
|
6045
|
+
// (oldest → newest), then restore the original draft. Within a
|
|
6046
|
+
// multi-line buffer, plain cursor movement still wins when no
|
|
6047
|
+
// navigation is in progress.
|
|
4837
6048
|
handleDown() {
|
|
4838
|
-
if (this.row < this.buffer.length - 1 && this.historyIndex === -1) {
|
|
6049
|
+
if (this.row < this.buffer.length - 1 && this.historyIndex === -1 && this.queueIndex === -1) {
|
|
4839
6050
|
this.row += 1;
|
|
4840
6051
|
this.col = Math.min(this.col, this.currentLine().length);
|
|
4841
6052
|
return [];
|
|
4842
6053
|
}
|
|
4843
|
-
if (this.historyIndex
|
|
6054
|
+
if (this.historyIndex >= 0) {
|
|
6055
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
6056
|
+
this.historyIndex += 1;
|
|
6057
|
+
this.loadEntry(this.history[this.historyIndex] ?? "");
|
|
6058
|
+
return [];
|
|
6059
|
+
}
|
|
6060
|
+
this.historyIndex = -1;
|
|
6061
|
+
if (this.queue.length > 0) {
|
|
6062
|
+
this.queueIndex = 0;
|
|
6063
|
+
this.loadEntry(this.queue[this.queueIndex] ?? "");
|
|
6064
|
+
return [];
|
|
6065
|
+
}
|
|
6066
|
+
this.restoreDraft();
|
|
4844
6067
|
return [];
|
|
4845
6068
|
}
|
|
4846
|
-
if (this.
|
|
4847
|
-
this.
|
|
4848
|
-
|
|
6069
|
+
if (this.queueIndex >= 0) {
|
|
6070
|
+
if (this.queueIndex < this.queue.length - 1) {
|
|
6071
|
+
this.queueIndex += 1;
|
|
6072
|
+
this.loadEntry(this.queue[this.queueIndex] ?? "");
|
|
6073
|
+
return [];
|
|
6074
|
+
}
|
|
6075
|
+
this.queueIndex = -1;
|
|
6076
|
+
this.restoreDraft();
|
|
4849
6077
|
return [];
|
|
4850
6078
|
}
|
|
4851
|
-
|
|
6079
|
+
return [];
|
|
6080
|
+
}
|
|
6081
|
+
restoreDraft() {
|
|
4852
6082
|
if (this.savedDraft) {
|
|
4853
6083
|
this.buffer = [...this.savedDraft.buffer];
|
|
4854
6084
|
this.row = this.savedDraft.row;
|
|
@@ -4857,11 +6087,146 @@ var init_input = __esm({
|
|
|
4857
6087
|
} else {
|
|
4858
6088
|
this.clearBuffer();
|
|
4859
6089
|
}
|
|
6090
|
+
}
|
|
6091
|
+
// Engage reverse-incremental search over prompt history. Uses the
|
|
6092
|
+
// current buffer text as the search query. With an empty buffer we
|
|
6093
|
+
// enter search mode in an "empty query, no match shown" state — the
|
|
6094
|
+
// banner indicator lights up, and as the user types we extend the
|
|
6095
|
+
// query and load top matches. We deliberately do NOT auto-load the
|
|
6096
|
+
// most recent entry on an empty ^R (that's a surprise — Up-arrow
|
|
6097
|
+
// already walks history if that's what they wanted). With a
|
|
6098
|
+
// non-empty query that has no history match, escalate straight to
|
|
6099
|
+
// scrollback search so the typed term searches session output.
|
|
6100
|
+
startHistorySearch() {
|
|
6101
|
+
const query = this.bufferText().toLowerCase();
|
|
6102
|
+
if (query.length === 0) {
|
|
6103
|
+
this.historySearch = {
|
|
6104
|
+
query: "",
|
|
6105
|
+
matchIndices: [],
|
|
6106
|
+
cursor: 0,
|
|
6107
|
+
savedDraft: {
|
|
6108
|
+
buffer: [...this.buffer],
|
|
6109
|
+
row: this.row,
|
|
6110
|
+
col: this.col
|
|
6111
|
+
}
|
|
6112
|
+
};
|
|
6113
|
+
return [];
|
|
6114
|
+
}
|
|
6115
|
+
const matchIndices = this.findHistoryMatches(query);
|
|
6116
|
+
if (matchIndices.length === 0) {
|
|
6117
|
+
return [{ type: "escalate-search", query }];
|
|
6118
|
+
}
|
|
6119
|
+
this.historySearch = {
|
|
6120
|
+
query,
|
|
6121
|
+
matchIndices,
|
|
6122
|
+
cursor: 0,
|
|
6123
|
+
savedDraft: {
|
|
6124
|
+
buffer: [...this.buffer],
|
|
6125
|
+
row: this.row,
|
|
6126
|
+
col: this.col
|
|
6127
|
+
}
|
|
6128
|
+
};
|
|
6129
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
6130
|
+
return [];
|
|
6131
|
+
}
|
|
6132
|
+
// ^R advance. At the oldest match with a non-empty query, falls
|
|
6133
|
+
// through to scrollback search (same escalate path as a never-
|
|
6134
|
+
// matched startHistorySearch). With an empty query at the oldest
|
|
6135
|
+
// match (i.e. the user walked all history with no filter), advance
|
|
6136
|
+
// is a no-op so the buffer stays on the oldest entry.
|
|
6137
|
+
advanceHistorySearch() {
|
|
6138
|
+
if (this.historySearch === null) {
|
|
6139
|
+
return [];
|
|
6140
|
+
}
|
|
6141
|
+
const search = this.historySearch;
|
|
6142
|
+
const atOldest = search.cursor >= search.matchIndices.length - 1;
|
|
6143
|
+
if (atOldest) {
|
|
6144
|
+
if (search.query.length === 0) {
|
|
6145
|
+
return [];
|
|
6146
|
+
}
|
|
6147
|
+
const query = search.query;
|
|
6148
|
+
const draft = search.savedDraft;
|
|
6149
|
+
this.historySearch = null;
|
|
6150
|
+
this.buffer = [...draft.buffer];
|
|
6151
|
+
this.row = draft.row;
|
|
6152
|
+
this.col = draft.col;
|
|
6153
|
+
return [{ type: "escalate-search", query }];
|
|
6154
|
+
}
|
|
6155
|
+
search.cursor += 1;
|
|
6156
|
+
const idx = search.matchIndices[search.cursor];
|
|
6157
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
4860
6158
|
return [];
|
|
4861
6159
|
}
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
6160
|
+
// ^S retreat — walk toward newer matches. No-op at the newest match
|
|
6161
|
+
// (no wrap, mirroring ^R no-wrap at the oldest).
|
|
6162
|
+
retreatHistorySearch() {
|
|
6163
|
+
if (this.historySearch === null) {
|
|
6164
|
+
return;
|
|
6165
|
+
}
|
|
6166
|
+
if (this.historySearch.cursor === 0) {
|
|
6167
|
+
return;
|
|
6168
|
+
}
|
|
6169
|
+
this.historySearch.cursor -= 1;
|
|
6170
|
+
const idx = this.historySearch.matchIndices[this.historySearch.cursor];
|
|
6171
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
6172
|
+
}
|
|
6173
|
+
// Backspace / typing within search mode mutates the query and
|
|
6174
|
+
// re-searches. When the new query is empty, restore the saved
|
|
6175
|
+
// draft buffer (typically empty) and stay in search mode — the
|
|
6176
|
+
// user can keep typing. When the new query has matches, load the
|
|
6177
|
+
// top one. When the new query has no matches, escalate to scrollback
|
|
6178
|
+
// search so the typed term applies there instead.
|
|
6179
|
+
mutateHistorySearchQuery(newQuery) {
|
|
6180
|
+
if (this.historySearch === null) {
|
|
6181
|
+
return [];
|
|
6182
|
+
}
|
|
6183
|
+
if (newQuery.length === 0) {
|
|
6184
|
+
this.historySearch.query = "";
|
|
6185
|
+
this.historySearch.matchIndices = [];
|
|
6186
|
+
this.historySearch.cursor = 0;
|
|
6187
|
+
const draft = this.historySearch.savedDraft;
|
|
6188
|
+
this.buffer = [...draft.buffer];
|
|
6189
|
+
this.row = draft.row;
|
|
6190
|
+
this.col = draft.col;
|
|
6191
|
+
return [];
|
|
6192
|
+
}
|
|
6193
|
+
const matchIndices = this.findHistoryMatches(newQuery);
|
|
6194
|
+
if (matchIndices.length === 0) {
|
|
6195
|
+
const draft = this.historySearch.savedDraft;
|
|
6196
|
+
this.historySearch = null;
|
|
6197
|
+
this.buffer = [...draft.buffer];
|
|
6198
|
+
this.row = draft.row;
|
|
6199
|
+
this.col = draft.col;
|
|
6200
|
+
return [{ type: "escalate-search", query: newQuery }];
|
|
6201
|
+
}
|
|
6202
|
+
this.historySearch.query = newQuery;
|
|
6203
|
+
this.historySearch.matchIndices = matchIndices;
|
|
6204
|
+
this.historySearch.cursor = 0;
|
|
6205
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
6206
|
+
return [];
|
|
6207
|
+
}
|
|
6208
|
+
findHistoryMatches(query) {
|
|
6209
|
+
const out = [];
|
|
6210
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
6211
|
+
const entry = this.history[i] ?? "";
|
|
6212
|
+
if (query.length === 0 || entry.toLowerCase().includes(query)) {
|
|
6213
|
+
out.push(i);
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
return out;
|
|
6217
|
+
}
|
|
6218
|
+
cancelHistorySearch() {
|
|
6219
|
+
if (this.historySearch === null) {
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
6222
|
+
const draft = this.historySearch.savedDraft;
|
|
6223
|
+
this.historySearch = null;
|
|
6224
|
+
this.buffer = [...draft.buffer];
|
|
6225
|
+
this.row = draft.row;
|
|
6226
|
+
this.col = draft.col;
|
|
6227
|
+
}
|
|
6228
|
+
loadEntry(text) {
|
|
6229
|
+
this.buffer = text.split("\n");
|
|
4865
6230
|
if (this.buffer.length === 0) {
|
|
4866
6231
|
this.buffer = [""];
|
|
4867
6232
|
}
|
|
@@ -4870,6 +6235,14 @@ var init_input = __esm({
|
|
|
4870
6235
|
}
|
|
4871
6236
|
send() {
|
|
4872
6237
|
const text = this.bufferText();
|
|
6238
|
+
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
6239
|
+
const index = this.queueIndex;
|
|
6240
|
+
this.clearBuffer();
|
|
6241
|
+
if (text.trim().length === 0) {
|
|
6242
|
+
return [{ type: "queue-remove", index }];
|
|
6243
|
+
}
|
|
6244
|
+
return [{ type: "queue-edit", index, text }];
|
|
6245
|
+
}
|
|
4873
6246
|
if (text.trim().length === 0) {
|
|
4874
6247
|
return [];
|
|
4875
6248
|
}
|
|
@@ -4877,25 +6250,105 @@ var init_input = __esm({
|
|
|
4877
6250
|
this.clearBuffer();
|
|
4878
6251
|
return [{ type: "send", text, planMode }];
|
|
4879
6252
|
}
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
6253
|
+
// Home: jump to the very start of the prompt buffer. If we're already
|
|
6254
|
+
// there, fall through to scrolling the scrollback to its top.
|
|
6255
|
+
handleHome() {
|
|
6256
|
+
if (this.row !== 0 || this.col !== 0) {
|
|
6257
|
+
this.row = 0;
|
|
6258
|
+
this.col = 0;
|
|
6259
|
+
return [];
|
|
6260
|
+
}
|
|
6261
|
+
return [{ type: "scroll-to-top" }];
|
|
6262
|
+
}
|
|
6263
|
+
// End: jump to the end of the last line of the prompt buffer. Already
|
|
6264
|
+
// there → scroll the scrollback to the bottom (newest).
|
|
6265
|
+
handleEnd() {
|
|
6266
|
+
const lastRow = this.buffer.length - 1;
|
|
6267
|
+
const lastCol = (this.buffer[lastRow] ?? "").length;
|
|
6268
|
+
if (this.row !== lastRow || this.col !== lastCol) {
|
|
6269
|
+
this.row = lastRow;
|
|
6270
|
+
this.col = lastCol;
|
|
6271
|
+
return [];
|
|
4883
6272
|
}
|
|
6273
|
+
return [{ type: "scroll-to-bottom" }];
|
|
6274
|
+
}
|
|
6275
|
+
handleCtrlC() {
|
|
4884
6276
|
if (!this.bufferIsEmpty()) {
|
|
4885
|
-
this.
|
|
6277
|
+
this.buffer = [""];
|
|
6278
|
+
this.row = 0;
|
|
6279
|
+
this.col = 0;
|
|
6280
|
+
if (this.queueIndex === -1) {
|
|
6281
|
+
this.historyIndex = -1;
|
|
6282
|
+
this.savedDraft = null;
|
|
6283
|
+
}
|
|
6284
|
+
return [];
|
|
6285
|
+
}
|
|
6286
|
+
if (this.queueIndex >= 0) {
|
|
6287
|
+
this.queueIndex = -1;
|
|
6288
|
+
this.restoreDraft();
|
|
4886
6289
|
return [];
|
|
4887
6290
|
}
|
|
6291
|
+
if (this.turnRunning) {
|
|
6292
|
+
return [{ type: "cancel" }];
|
|
6293
|
+
}
|
|
4888
6294
|
return [{ type: "exit" }];
|
|
4889
6295
|
}
|
|
4890
6296
|
};
|
|
4891
6297
|
}
|
|
4892
6298
|
});
|
|
4893
6299
|
|
|
6300
|
+
// src/tui/completion.ts
|
|
6301
|
+
function longestCommonPrefix(names) {
|
|
6302
|
+
if (names.length === 0) {
|
|
6303
|
+
return "";
|
|
6304
|
+
}
|
|
6305
|
+
let prefix = names[0] ?? "";
|
|
6306
|
+
for (let i = 1; i < names.length; i++) {
|
|
6307
|
+
const n = names[i] ?? "";
|
|
6308
|
+
let j = 0;
|
|
6309
|
+
while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
|
|
6310
|
+
j += 1;
|
|
6311
|
+
}
|
|
6312
|
+
prefix = prefix.slice(0, j);
|
|
6313
|
+
if (prefix.length === 0) {
|
|
6314
|
+
break;
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
return prefix;
|
|
6318
|
+
}
|
|
6319
|
+
function computeTabCompletion(args) {
|
|
6320
|
+
const { matches, firstLine: firstLine3 } = args;
|
|
6321
|
+
if (matches.length === 0) {
|
|
6322
|
+
return null;
|
|
6323
|
+
}
|
|
6324
|
+
const space = firstLine3.indexOf(" ");
|
|
6325
|
+
const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
|
|
6326
|
+
const tail = space === -1 ? "" : firstLine3.slice(space);
|
|
6327
|
+
if (matches.length === 1) {
|
|
6328
|
+
const name = matches[0] ?? "";
|
|
6329
|
+
const suffix = tail.startsWith(" ") ? "" : " ";
|
|
6330
|
+
return name + suffix + tail;
|
|
6331
|
+
}
|
|
6332
|
+
const commonPrefix = longestCommonPrefix(matches);
|
|
6333
|
+
if (commonPrefix.length <= typedPrefix.length) {
|
|
6334
|
+
return null;
|
|
6335
|
+
}
|
|
6336
|
+
return commonPrefix + tail;
|
|
6337
|
+
}
|
|
6338
|
+
var init_completion = __esm({
|
|
6339
|
+
"src/tui/completion.ts"() {
|
|
6340
|
+
"use strict";
|
|
6341
|
+
}
|
|
6342
|
+
});
|
|
6343
|
+
|
|
4894
6344
|
// src/tui/render-update.ts
|
|
4895
6345
|
import stripAnsi from "strip-ansi";
|
|
4896
6346
|
function sanitizeWireText(text) {
|
|
4897
6347
|
return stripAnsi(text).replace(STRIP_CONTROLS, "");
|
|
4898
6348
|
}
|
|
6349
|
+
function sanitizeSingleLine(text) {
|
|
6350
|
+
return sanitizeWireText(text).replace(/[\n\t]+/g, " ").replace(/ +/g, " ").trim();
|
|
6351
|
+
}
|
|
4899
6352
|
function mapUpdate(update) {
|
|
4900
6353
|
if (!update || typeof update !== "object") {
|
|
4901
6354
|
return null;
|
|
@@ -4939,7 +6392,7 @@ function mapUpdate(update) {
|
|
|
4939
6392
|
}
|
|
4940
6393
|
function mapSessionInfo(u) {
|
|
4941
6394
|
const rawTitle = readString(u, "title");
|
|
4942
|
-
const title = rawTitle !== void 0 ?
|
|
6395
|
+
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
4943
6396
|
const meta = u._meta;
|
|
4944
6397
|
let agentId;
|
|
4945
6398
|
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
@@ -4963,10 +6416,9 @@ function mapSessionInfo(u) {
|
|
|
4963
6416
|
}
|
|
4964
6417
|
return event;
|
|
4965
6418
|
}
|
|
4966
|
-
function
|
|
4967
|
-
const list = u.availableCommands ?? u.commands;
|
|
6419
|
+
function normalizeAdvertisedCommands(list) {
|
|
4968
6420
|
if (!Array.isArray(list)) {
|
|
4969
|
-
return
|
|
6421
|
+
return [];
|
|
4970
6422
|
}
|
|
4971
6423
|
const out = [];
|
|
4972
6424
|
for (const raw of list) {
|
|
@@ -4978,13 +6430,20 @@ function mapAvailableCommands(u) {
|
|
|
4978
6430
|
continue;
|
|
4979
6431
|
}
|
|
4980
6432
|
const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
|
|
4981
|
-
const cmd = { name:
|
|
6433
|
+
const cmd = { name: sanitizeSingleLine(rawName) };
|
|
4982
6434
|
if (typeof c.description === "string") {
|
|
4983
|
-
cmd.description =
|
|
6435
|
+
cmd.description = sanitizeSingleLine(c.description);
|
|
4984
6436
|
}
|
|
4985
6437
|
out.push(cmd);
|
|
4986
6438
|
}
|
|
4987
|
-
return
|
|
6439
|
+
return out;
|
|
6440
|
+
}
|
|
6441
|
+
function mapAvailableCommands(u) {
|
|
6442
|
+
const list = u.availableCommands ?? u.commands;
|
|
6443
|
+
if (!Array.isArray(list)) {
|
|
6444
|
+
return null;
|
|
6445
|
+
}
|
|
6446
|
+
return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
|
|
4988
6447
|
}
|
|
4989
6448
|
function mapUsage(u) {
|
|
4990
6449
|
const event = { kind: "usage-update" };
|
|
@@ -5046,7 +6505,7 @@ function mapToolCall(u) {
|
|
|
5046
6505
|
return null;
|
|
5047
6506
|
}
|
|
5048
6507
|
const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
|
|
5049
|
-
const title =
|
|
6508
|
+
const title = sanitizeSingleLine(rawTitle);
|
|
5050
6509
|
const status = readString(u, "status");
|
|
5051
6510
|
const rawKind = readString(u, "kind");
|
|
5052
6511
|
const event = { kind: "tool-call", toolCallId, title };
|
|
@@ -5064,7 +6523,7 @@ function mapToolCallUpdate(u) {
|
|
|
5064
6523
|
return null;
|
|
5065
6524
|
}
|
|
5066
6525
|
const rawTitle = readString(u, "title");
|
|
5067
|
-
const title = rawTitle !== void 0 ?
|
|
6526
|
+
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
5068
6527
|
const status = readString(u, "status");
|
|
5069
6528
|
const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
|
|
5070
6529
|
if (!meaningful) {
|
|
@@ -5090,7 +6549,7 @@ function mapPlan(u) {
|
|
|
5090
6549
|
continue;
|
|
5091
6550
|
}
|
|
5092
6551
|
const e = raw;
|
|
5093
|
-
const content = typeof e.content === "string" ?
|
|
6552
|
+
const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
|
|
5094
6553
|
if (!content) {
|
|
5095
6554
|
continue;
|
|
5096
6555
|
}
|
|
@@ -5110,14 +6569,14 @@ function mapMode(u) {
|
|
|
5110
6569
|
if (!mode) {
|
|
5111
6570
|
return null;
|
|
5112
6571
|
}
|
|
5113
|
-
return { kind: "mode-changed", mode:
|
|
6572
|
+
return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
|
|
5114
6573
|
}
|
|
5115
6574
|
function mapModel(u) {
|
|
5116
6575
|
const model = readString(u, "currentModel") ?? readString(u, "model");
|
|
5117
6576
|
if (!model) {
|
|
5118
6577
|
return null;
|
|
5119
6578
|
}
|
|
5120
|
-
return { kind: "model-changed", model:
|
|
6579
|
+
return { kind: "model-changed", model: sanitizeSingleLine(model) };
|
|
5121
6580
|
}
|
|
5122
6581
|
function mapTurnComplete(u) {
|
|
5123
6582
|
const stopReason = readString(u, "stopReason");
|
|
@@ -5505,6 +6964,7 @@ import { nanoid as nanoid3 } from "nanoid";
|
|
|
5505
6964
|
import termkit from "terminal-kit";
|
|
5506
6965
|
async function runTuiApp(opts) {
|
|
5507
6966
|
const config = await ensureConfig();
|
|
6967
|
+
logMaxBytes = config.tui.logMaxBytes;
|
|
5508
6968
|
await ensureDaemonReachable(config);
|
|
5509
6969
|
const term = termkit.terminal;
|
|
5510
6970
|
const exitHint = {};
|
|
@@ -5525,7 +6985,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5525
6985
|
process.exit(0);
|
|
5526
6986
|
}
|
|
5527
6987
|
const launchLabel = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
5528
|
-
term.
|
|
6988
|
+
term.brightYellow(launchLabel)("\n");
|
|
5529
6989
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
5530
6990
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
5531
6991
|
const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
|
|
@@ -5606,7 +7066,8 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5606
7066
|
const { update } = params ?? {};
|
|
5607
7067
|
const event = mapUpdate(update);
|
|
5608
7068
|
debugLogUpdate(update, event);
|
|
5609
|
-
|
|
7069
|
+
const rawTag = update?.sessionUpdate;
|
|
7070
|
+
if (rawTag === "prompt_received") {
|
|
5610
7071
|
adjustPendingTurns(1);
|
|
5611
7072
|
} else if (event?.kind === "turn-complete") {
|
|
5612
7073
|
adjustPendingTurns(-1);
|
|
@@ -5677,11 +7138,11 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5677
7138
|
const rawOptions = Array.isArray(p.options) ? p.options : [];
|
|
5678
7139
|
const options = rawOptions.map((o) => ({
|
|
5679
7140
|
optionId: o.optionId,
|
|
5680
|
-
name:
|
|
7141
|
+
name: sanitizeSingleLine(o.name ?? ""),
|
|
5681
7142
|
...o.kind !== void 0 ? { kind: o.kind } : {}
|
|
5682
7143
|
}));
|
|
5683
7144
|
const rawTitle = p.toolCall?.title ?? p.toolCall?.name ?? "tool";
|
|
5684
|
-
const title =
|
|
7145
|
+
const title = sanitizeSingleLine(rawTitle);
|
|
5685
7146
|
const toolCallId = p.toolCall?.toolCallId;
|
|
5686
7147
|
if (options.length === 0) {
|
|
5687
7148
|
screen.appendLines([
|
|
@@ -5711,12 +7172,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5711
7172
|
let agentInfoName;
|
|
5712
7173
|
try {
|
|
5713
7174
|
const initResult = await conn.request("initialize", {
|
|
5714
|
-
protocolVersion:
|
|
7175
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
5715
7176
|
clientCapabilities: {
|
|
5716
7177
|
fs: { readTextFile: false, writeTextFile: false },
|
|
5717
7178
|
terminal: false
|
|
5718
7179
|
},
|
|
5719
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
7180
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
5720
7181
|
});
|
|
5721
7182
|
agentInfoName = initResult?.agentInfo?.name;
|
|
5722
7183
|
} catch {
|
|
@@ -5759,15 +7220,13 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5759
7220
|
initialMode = hydraMeta.currentMode;
|
|
5760
7221
|
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
5761
7222
|
if (hydraMeta.availableCommands) {
|
|
5762
|
-
initialCommands = hydraMeta.availableCommands
|
|
5763
|
-
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
5764
|
-
);
|
|
7223
|
+
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
5765
7224
|
}
|
|
5766
7225
|
} else {
|
|
5767
7226
|
const attached = await conn.request("session/attach", {
|
|
5768
7227
|
sessionId: ctx.sessionId,
|
|
5769
7228
|
historyPolicy: "full",
|
|
5770
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
7229
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
5771
7230
|
});
|
|
5772
7231
|
resolvedSessionId = attached.sessionId;
|
|
5773
7232
|
exitHint.sessionId = resolvedSessionId;
|
|
@@ -5786,9 +7245,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5786
7245
|
initialMode = hydraMeta.currentMode;
|
|
5787
7246
|
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
5788
7247
|
if (hydraMeta.availableCommands) {
|
|
5789
|
-
initialCommands = hydraMeta.availableCommands
|
|
5790
|
-
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
5791
|
-
);
|
|
7248
|
+
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
5792
7249
|
}
|
|
5793
7250
|
}
|
|
5794
7251
|
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
@@ -5799,11 +7256,13 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5799
7256
|
dispatcher.setTurnRunning(true);
|
|
5800
7257
|
}
|
|
5801
7258
|
let turnInFlight = null;
|
|
7259
|
+
let pendingPrefill = null;
|
|
5802
7260
|
const screen = new Screen({
|
|
5803
7261
|
term,
|
|
5804
7262
|
dispatcher,
|
|
5805
7263
|
repaintThrottleMs: config.tui.repaintThrottleMs,
|
|
5806
7264
|
maxScrollbackLines: config.tui.maxScrollbackLines,
|
|
7265
|
+
mouse: config.tui.mouse,
|
|
5807
7266
|
onKey: (events) => {
|
|
5808
7267
|
for (const ev of events) {
|
|
5809
7268
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
@@ -5812,6 +7271,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5812
7271
|
if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
|
|
5813
7272
|
continue;
|
|
5814
7273
|
}
|
|
7274
|
+
if (tryHandleScrollbackSearchKey(ev)) {
|
|
7275
|
+
continue;
|
|
7276
|
+
}
|
|
5815
7277
|
if (tryHandleCompletionKey(ev)) {
|
|
5816
7278
|
continue;
|
|
5817
7279
|
}
|
|
@@ -5821,6 +7283,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5821
7283
|
}
|
|
5822
7284
|
}
|
|
5823
7285
|
refreshCompletions();
|
|
7286
|
+
screen.setBannerSearchIndicator(
|
|
7287
|
+
dispatcher.state().historySearchQuery
|
|
7288
|
+
);
|
|
5824
7289
|
screen.refreshPrompt();
|
|
5825
7290
|
}
|
|
5826
7291
|
});
|
|
@@ -5830,6 +7295,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5830
7295
|
{ name: "/quit", description: "Exit the TUI" },
|
|
5831
7296
|
{ name: "/clear", description: "Clear scrollback" },
|
|
5832
7297
|
{ name: "/sessions", description: "List sessions" },
|
|
7298
|
+
{ name: "/model", description: "Switch model: /model <model-id>" },
|
|
5833
7299
|
{ name: "/demo-plan", description: "Inject synthetic plan events (UI test)" },
|
|
5834
7300
|
{ name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
|
|
5835
7301
|
];
|
|
@@ -5864,48 +7330,73 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5864
7330
|
screen.setCompletions(currentCompletions());
|
|
5865
7331
|
};
|
|
5866
7332
|
const tryHandleCompletionKey = (ev) => {
|
|
5867
|
-
if (ev.type !== "key") {
|
|
7333
|
+
if (ev.type !== "key" || ev.name !== "tab") {
|
|
5868
7334
|
return false;
|
|
5869
7335
|
}
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
|
|
5881
|
-
const tail = space === -1 ? "" : firstLine3.slice(space);
|
|
5882
|
-
let next = commonPrefix;
|
|
5883
|
-
if (commonPrefix.length <= typedPrefix.length || matches.length === 1) {
|
|
5884
|
-
next = first.name + (tail.startsWith(" ") ? "" : " ");
|
|
5885
|
-
}
|
|
5886
|
-
dispatcher.replaceFirstLine(next + tail);
|
|
7336
|
+
const matches = currentCompletions();
|
|
7337
|
+
if (matches.length === 0) {
|
|
7338
|
+
return false;
|
|
7339
|
+
}
|
|
7340
|
+
const firstLine3 = dispatcher.state().buffer[0] ?? "";
|
|
7341
|
+
const next = computeTabCompletion({
|
|
7342
|
+
matches: matches.map((m) => m.name),
|
|
7343
|
+
firstLine: firstLine3
|
|
7344
|
+
});
|
|
7345
|
+
if (next === null) {
|
|
5887
7346
|
return true;
|
|
5888
7347
|
}
|
|
5889
|
-
|
|
7348
|
+
dispatcher.replaceFirstLine(next);
|
|
7349
|
+
return true;
|
|
5890
7350
|
};
|
|
5891
|
-
|
|
5892
|
-
if (
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
const n = names[i] ?? "";
|
|
5898
|
-
let j = 0;
|
|
5899
|
-
while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
|
|
5900
|
-
j += 1;
|
|
7351
|
+
const tryHandleScrollbackSearchKey = (ev) => {
|
|
7352
|
+
if (!screen.isScrollbackSearchActive()) {
|
|
7353
|
+
if (ev.type === "key" && ev.name === "ctrl-r" && screen.isScrolledBack()) {
|
|
7354
|
+
screen.enterScrollbackSearch();
|
|
7355
|
+
screen.updateScrollbackSearchTerm("");
|
|
7356
|
+
return true;
|
|
5901
7357
|
}
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
7358
|
+
return false;
|
|
7359
|
+
}
|
|
7360
|
+
if (ev.type === "char") {
|
|
7361
|
+
const term2 = screen.scrollbackSearchTerm() + ev.ch;
|
|
7362
|
+
screen.updateScrollbackSearchTerm(term2);
|
|
7363
|
+
return true;
|
|
7364
|
+
}
|
|
7365
|
+
if (ev.type === "paste") {
|
|
7366
|
+
const term2 = screen.scrollbackSearchTerm() + ev.text.replace(/\n/g, " ");
|
|
7367
|
+
screen.updateScrollbackSearchTerm(term2);
|
|
7368
|
+
return true;
|
|
7369
|
+
}
|
|
7370
|
+
if (ev.type === "key") {
|
|
7371
|
+
switch (ev.name) {
|
|
7372
|
+
case "ctrl-r":
|
|
7373
|
+
screen.advanceScrollbackSearch();
|
|
7374
|
+
return true;
|
|
7375
|
+
case "ctrl-s":
|
|
7376
|
+
screen.retreatScrollbackSearch();
|
|
7377
|
+
return true;
|
|
7378
|
+
case "backspace": {
|
|
7379
|
+
const term2 = screen.scrollbackSearchTerm();
|
|
7380
|
+
if (term2.length === 0) {
|
|
7381
|
+
screen.cancelScrollbackSearch();
|
|
7382
|
+
} else {
|
|
7383
|
+
screen.updateScrollbackSearchTerm(term2.slice(0, -1));
|
|
7384
|
+
}
|
|
7385
|
+
return true;
|
|
7386
|
+
}
|
|
7387
|
+
case "enter":
|
|
7388
|
+
screen.acceptScrollbackSearch();
|
|
7389
|
+
return true;
|
|
7390
|
+
case "escape":
|
|
7391
|
+
case "ctrl-c":
|
|
7392
|
+
screen.cancelScrollbackSearch();
|
|
7393
|
+
return true;
|
|
7394
|
+
default:
|
|
7395
|
+
return true;
|
|
5905
7396
|
}
|
|
5906
7397
|
}
|
|
5907
|
-
return
|
|
5908
|
-
}
|
|
7398
|
+
return true;
|
|
7399
|
+
};
|
|
5909
7400
|
const tryHandlePermissionKey = (ev) => {
|
|
5910
7401
|
if (!pendingPermission) {
|
|
5911
7402
|
return false;
|
|
@@ -6112,22 +7603,45 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6112
7603
|
}
|
|
6113
7604
|
resume(nextOpts);
|
|
6114
7605
|
};
|
|
7606
|
+
const queueHeadOffset = () => workerActive ? 1 : 0;
|
|
6115
7607
|
const handleEffect = (effect) => {
|
|
6116
7608
|
switch (effect.type) {
|
|
6117
7609
|
case "send":
|
|
6118
7610
|
enqueuePrompt(effect.text, effect.planMode);
|
|
6119
7611
|
return;
|
|
6120
|
-
case "
|
|
7612
|
+
case "queue-edit": {
|
|
7613
|
+
const realIdx = effect.index + queueHeadOffset();
|
|
7614
|
+
const existing = promptQueue[realIdx];
|
|
7615
|
+
if (existing) {
|
|
7616
|
+
promptQueue[realIdx] = { text: effect.text, planMode: existing.planMode };
|
|
7617
|
+
refreshQueueDisplay();
|
|
7618
|
+
}
|
|
7619
|
+
return;
|
|
7620
|
+
}
|
|
7621
|
+
case "queue-remove": {
|
|
7622
|
+
const realIdx = effect.index + queueHeadOffset();
|
|
7623
|
+
if (realIdx >= 0 && realIdx < promptQueue.length) {
|
|
7624
|
+
promptQueue.splice(realIdx, 1);
|
|
7625
|
+
refreshQueueDisplay();
|
|
7626
|
+
}
|
|
7627
|
+
return;
|
|
7628
|
+
}
|
|
7629
|
+
case "cancel": {
|
|
7630
|
+
if (effect.prefill && turnInFlight) {
|
|
7631
|
+
const headOffset = workerActive ? 1 : 0;
|
|
7632
|
+
const waitingEmpty = promptQueue.length <= headOffset;
|
|
7633
|
+
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
7634
|
+
if (waitingEmpty && bufferEmpty) {
|
|
7635
|
+
pendingPrefill = turnInFlight.text;
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
6121
7638
|
if (turnInFlight) {
|
|
6122
7639
|
turnInFlight.cancel();
|
|
6123
7640
|
} else if (pendingTurns > 0) {
|
|
6124
7641
|
cancelRemoteTurn();
|
|
6125
7642
|
}
|
|
6126
|
-
if (promptQueue.length > (workerActive ? 1 : 0)) {
|
|
6127
|
-
promptQueue.length = workerActive ? 1 : 0;
|
|
6128
|
-
refreshQueueDisplay();
|
|
6129
|
-
}
|
|
6130
7643
|
return;
|
|
7644
|
+
}
|
|
6131
7645
|
case "exit":
|
|
6132
7646
|
void requestExit();
|
|
6133
7647
|
return;
|
|
@@ -6140,6 +7654,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6140
7654
|
case "redraw":
|
|
6141
7655
|
screen.fullRedraw();
|
|
6142
7656
|
return;
|
|
7657
|
+
case "scroll-to-top":
|
|
7658
|
+
screen.scrollToTop();
|
|
7659
|
+
return;
|
|
7660
|
+
case "scroll-to-bottom":
|
|
7661
|
+
screen.scrollToBottom();
|
|
7662
|
+
return;
|
|
6143
7663
|
case "switch-session":
|
|
6144
7664
|
void switchSession();
|
|
6145
7665
|
return;
|
|
@@ -6147,6 +7667,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6147
7667
|
toolsExpanded = !toolsExpanded;
|
|
6148
7668
|
renderToolsBlock();
|
|
6149
7669
|
return;
|
|
7670
|
+
case "escalate-search":
|
|
7671
|
+
screen.enterScrollbackSearch();
|
|
7672
|
+
screen.updateScrollbackSearchTerm(effect.query);
|
|
7673
|
+
return;
|
|
6150
7674
|
}
|
|
6151
7675
|
};
|
|
6152
7676
|
const promptQueue = [];
|
|
@@ -6155,6 +7679,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6155
7679
|
const waiting = promptQueue.slice(workerActive ? 1 : 0);
|
|
6156
7680
|
screen.setQueuedPrompts(waiting.map((p) => p.text));
|
|
6157
7681
|
screen.setBanner({ queued: waiting.length });
|
|
7682
|
+
dispatcher.setQueue(waiting.map((p) => p.text));
|
|
6158
7683
|
};
|
|
6159
7684
|
const enqueuePrompt = (text, planMode) => {
|
|
6160
7685
|
screen.scrollToBottom();
|
|
@@ -6191,6 +7716,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6191
7716
|
toolCallOrder.length = 0;
|
|
6192
7717
|
toolsBlockStartedAt = null;
|
|
6193
7718
|
toolsBlockEndedAt = null;
|
|
7719
|
+
toolsBlockStopReason = null;
|
|
6194
7720
|
toolsExpanded = false;
|
|
6195
7721
|
screen.clearScrollback();
|
|
6196
7722
|
return true;
|
|
@@ -6283,6 +7809,40 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6283
7809
|
}
|
|
6284
7810
|
]);
|
|
6285
7811
|
return true;
|
|
7812
|
+
case "/model": {
|
|
7813
|
+
const arg = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
7814
|
+
if (arg === "") {
|
|
7815
|
+
screen.appendLines([
|
|
7816
|
+
{
|
|
7817
|
+
prefix: " ",
|
|
7818
|
+
body: "Usage: /model <model-id>",
|
|
7819
|
+
bodyStyle: "info"
|
|
7820
|
+
}
|
|
7821
|
+
]);
|
|
7822
|
+
return true;
|
|
7823
|
+
}
|
|
7824
|
+
conn.request("session/set_model", {
|
|
7825
|
+
sessionId: resolvedSessionId,
|
|
7826
|
+
modelId: arg
|
|
7827
|
+
}).then(() => {
|
|
7828
|
+
screen.appendLines([
|
|
7829
|
+
{
|
|
7830
|
+
prefix: " ",
|
|
7831
|
+
body: `model set to ${arg}`,
|
|
7832
|
+
bodyStyle: "system"
|
|
7833
|
+
}
|
|
7834
|
+
]);
|
|
7835
|
+
}).catch((err) => {
|
|
7836
|
+
screen.appendLines([
|
|
7837
|
+
{
|
|
7838
|
+
prefix: " ",
|
|
7839
|
+
body: `set_model failed: ${err.message}`,
|
|
7840
|
+
bodyStyle: "tool-status-fail"
|
|
7841
|
+
}
|
|
7842
|
+
]);
|
|
7843
|
+
});
|
|
7844
|
+
return true;
|
|
7845
|
+
}
|
|
6286
7846
|
default:
|
|
6287
7847
|
return false;
|
|
6288
7848
|
}
|
|
@@ -6302,6 +7862,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6302
7862
|
} finally {
|
|
6303
7863
|
workerActive = false;
|
|
6304
7864
|
refreshQueueDisplay();
|
|
7865
|
+
if (pendingPrefill !== null) {
|
|
7866
|
+
const text = pendingPrefill;
|
|
7867
|
+
pendingPrefill = null;
|
|
7868
|
+
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
7869
|
+
if (bufferEmpty) {
|
|
7870
|
+
dispatcher.setBuffer(text);
|
|
7871
|
+
screen.refreshPrompt();
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
6305
7874
|
}
|
|
6306
7875
|
};
|
|
6307
7876
|
const processPrompt = async (text, planMode) => {
|
|
@@ -6311,6 +7880,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6311
7880
|
appendRender({ kind: "user-text", text });
|
|
6312
7881
|
let cancelled = false;
|
|
6313
7882
|
turnInFlight = {
|
|
7883
|
+
text,
|
|
6314
7884
|
cancel: () => {
|
|
6315
7885
|
if (cancelled) {
|
|
6316
7886
|
return;
|
|
@@ -6350,6 +7920,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6350
7920
|
let toolsExpanded = false;
|
|
6351
7921
|
let toolsBlockStartedAt = null;
|
|
6352
7922
|
let toolsBlockEndedAt = null;
|
|
7923
|
+
let toolsBlockStopReason = null;
|
|
6353
7924
|
const TOOLS_COLLAPSED_LIMIT = 5;
|
|
6354
7925
|
let agentBuffer = "";
|
|
6355
7926
|
let agentKey = null;
|
|
@@ -6391,12 +7962,17 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6391
7962
|
const inProgress = toolsBlockEndedAt === null;
|
|
6392
7963
|
const end = toolsBlockEndedAt ?? Date.now();
|
|
6393
7964
|
const elapsed = end - toolsBlockStartedAt;
|
|
7965
|
+
const stoppedReason = !inProgress && toolsBlockStopReason !== null && toolsBlockStopReason !== "end_turn" ? toolsBlockStopReason : null;
|
|
6394
7966
|
let summary;
|
|
6395
7967
|
if (total === 0) {
|
|
6396
|
-
|
|
7968
|
+
if (stoppedReason !== null) {
|
|
7969
|
+
summary = `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}`;
|
|
7970
|
+
} else {
|
|
7971
|
+
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
|
|
7972
|
+
}
|
|
6397
7973
|
} else {
|
|
6398
7974
|
const noun = total === 1 ? "tool" : "tools";
|
|
6399
|
-
const timing = inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
7975
|
+
const timing = stoppedReason !== null ? `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}` : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
6400
7976
|
const parts = [`${total} ${noun}`, timing];
|
|
6401
7977
|
if (inProgress) {
|
|
6402
7978
|
if (hidden > 0) {
|
|
@@ -6407,12 +7983,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6407
7983
|
}
|
|
6408
7984
|
summary = parts.join(" \xB7 ");
|
|
6409
7985
|
}
|
|
7986
|
+
const pureThinking = total === 0 && inProgress;
|
|
7987
|
+
const frozenStyle = stoppedReason !== null ? "tool-status-fail" : "tool";
|
|
7988
|
+
const frozenBodyStyle = stoppedReason !== null ? "tool-status-fail" : "dim";
|
|
6410
7989
|
const lines = [
|
|
6411
7990
|
{
|
|
6412
7991
|
prefix: "\u2692 ",
|
|
6413
|
-
prefixStyle: "tool",
|
|
7992
|
+
prefixStyle: pureThinking ? "tool-status-running" : frozenStyle,
|
|
6414
7993
|
body: summary,
|
|
6415
|
-
bodyStyle: "
|
|
7994
|
+
bodyStyle: pureThinking ? "tool-status-running" : frozenBodyStyle
|
|
6416
7995
|
}
|
|
6417
7996
|
];
|
|
6418
7997
|
for (const id of visibleIds) {
|
|
@@ -6426,6 +8005,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6426
8005
|
const startToolsBlock = () => {
|
|
6427
8006
|
toolsBlockStartedAt = Date.now();
|
|
6428
8007
|
toolsBlockEndedAt = null;
|
|
8008
|
+
toolsBlockStopReason = null;
|
|
6429
8009
|
renderToolsBlock();
|
|
6430
8010
|
};
|
|
6431
8011
|
const recordToolCall = (id, title, status) => {
|
|
@@ -6450,6 +8030,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6450
8030
|
if (toolsBlockStartedAt === null) {
|
|
6451
8031
|
toolsBlockStartedAt = Date.now();
|
|
6452
8032
|
toolsBlockEndedAt = null;
|
|
8033
|
+
toolsBlockStopReason = null;
|
|
6453
8034
|
}
|
|
6454
8035
|
toolCallOrder.push(id);
|
|
6455
8036
|
}
|
|
@@ -6551,6 +8132,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6551
8132
|
screen.clearKey("plan");
|
|
6552
8133
|
if (toolsBlockStartedAt !== null) {
|
|
6553
8134
|
toolsBlockEndedAt = Date.now();
|
|
8135
|
+
toolsBlockStopReason = event.stopReason ?? null;
|
|
6554
8136
|
renderToolsBlock();
|
|
6555
8137
|
screen.clearKey("tools");
|
|
6556
8138
|
}
|
|
@@ -6558,6 +8140,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6558
8140
|
toolCallOrder.length = 0;
|
|
6559
8141
|
toolsBlockStartedAt = null;
|
|
6560
8142
|
toolsBlockEndedAt = null;
|
|
8143
|
+
toolsBlockStopReason = null;
|
|
6561
8144
|
toolsExpanded = false;
|
|
6562
8145
|
screen.ensureSeparator();
|
|
6563
8146
|
}
|
|
@@ -6588,6 +8171,8 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6588
8171
|
}, 1e3);
|
|
6589
8172
|
}
|
|
6590
8173
|
startToolsBlock();
|
|
8174
|
+
} else if (initialTurnStartedAt === void 0 && pendingTurns > 0) {
|
|
8175
|
+
adjustPendingTurns(-pendingTurns);
|
|
6591
8176
|
}
|
|
6592
8177
|
const resetInFlightUiState = () => {
|
|
6593
8178
|
if (pendingPermission) {
|
|
@@ -6599,12 +8184,14 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6599
8184
|
closeAgentText();
|
|
6600
8185
|
if (toolsBlockStartedAt !== null) {
|
|
6601
8186
|
toolsBlockEndedAt = Date.now();
|
|
8187
|
+
toolsBlockStopReason = null;
|
|
6602
8188
|
renderToolsBlock();
|
|
6603
8189
|
screen.clearKey("tools");
|
|
6604
8190
|
toolStates.clear();
|
|
6605
8191
|
toolCallOrder.length = 0;
|
|
6606
8192
|
toolsBlockStartedAt = null;
|
|
6607
8193
|
toolsBlockEndedAt = null;
|
|
8194
|
+
toolsBlockStopReason = null;
|
|
6608
8195
|
toolsExpanded = false;
|
|
6609
8196
|
}
|
|
6610
8197
|
screen.clearKey("plan");
|
|
@@ -6622,12 +8209,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6622
8209
|
id: `tui-reinit-${nanoid3()}`,
|
|
6623
8210
|
method: "initialize",
|
|
6624
8211
|
params: {
|
|
6625
|
-
protocolVersion:
|
|
8212
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
6626
8213
|
clientCapabilities: {
|
|
6627
8214
|
fs: { readTextFile: false, writeTextFile: false },
|
|
6628
8215
|
terminal: false
|
|
6629
8216
|
},
|
|
6630
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
8217
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
6631
8218
|
}
|
|
6632
8219
|
};
|
|
6633
8220
|
try {
|
|
@@ -6641,7 +8228,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6641
8228
|
params: {
|
|
6642
8229
|
sessionId: resolvedSessionId,
|
|
6643
8230
|
historyPolicy: "none",
|
|
6644
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
8231
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
|
|
6645
8232
|
...upstreamSessionId !== void 0 ? {
|
|
6646
8233
|
_meta: {
|
|
6647
8234
|
[HYDRA_META_KEY]: {
|
|
@@ -6765,15 +8352,15 @@ function writeDebugLine(payload) {
|
|
|
6765
8352
|
}
|
|
6766
8353
|
function rotateIfBig(target) {
|
|
6767
8354
|
try {
|
|
6768
|
-
const
|
|
6769
|
-
if (
|
|
8355
|
+
const stat4 = statSync(target);
|
|
8356
|
+
if (stat4.size < logMaxBytes) {
|
|
6770
8357
|
return;
|
|
6771
8358
|
}
|
|
6772
8359
|
renameSync(target, `${target}.0`);
|
|
6773
8360
|
} catch {
|
|
6774
8361
|
}
|
|
6775
8362
|
}
|
|
6776
|
-
var PLAN_PREFIX_TEXT,
|
|
8363
|
+
var PLAN_PREFIX_TEXT, logMaxBytes;
|
|
6777
8364
|
var init_app = __esm({
|
|
6778
8365
|
"src/tui/app.ts"() {
|
|
6779
8366
|
"use strict";
|
|
@@ -6784,15 +8371,17 @@ var init_app = __esm({
|
|
|
6784
8371
|
init_daemon_bootstrap();
|
|
6785
8372
|
init_session();
|
|
6786
8373
|
init_paths();
|
|
8374
|
+
init_hydra_version();
|
|
6787
8375
|
init_history();
|
|
6788
8376
|
init_discovery();
|
|
6789
8377
|
init_picker();
|
|
6790
8378
|
init_screen();
|
|
6791
8379
|
init_input();
|
|
8380
|
+
init_completion();
|
|
6792
8381
|
init_render_update();
|
|
6793
8382
|
init_format();
|
|
6794
8383
|
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.";
|
|
6795
|
-
|
|
8384
|
+
logMaxBytes = 5 * 1024 * 1024;
|
|
6796
8385
|
}
|
|
6797
8386
|
});
|
|
6798
8387
|
|
|
@@ -6809,9 +8398,9 @@ var init_tui = __esm({
|
|
|
6809
8398
|
});
|
|
6810
8399
|
|
|
6811
8400
|
// src/cli.ts
|
|
6812
|
-
import { readFileSync } from "fs";
|
|
6813
|
-
import { fileURLToPath } from "url";
|
|
6814
|
-
import { dirname as
|
|
8401
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
8402
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8403
|
+
import { dirname as dirname6, resolve as resolve4 } from "path";
|
|
6815
8404
|
|
|
6816
8405
|
// src/cli/parse-args.ts
|
|
6817
8406
|
function parseArgs(argv) {
|
|
@@ -6875,35 +8464,28 @@ init_config();
|
|
|
6875
8464
|
import * as fs2 from "fs/promises";
|
|
6876
8465
|
async function runInit(flags) {
|
|
6877
8466
|
await fs2.mkdir(paths.home(), { recursive: true });
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
existing = void 0;
|
|
6883
|
-
}
|
|
6884
|
-
if (!existing) {
|
|
6885
|
-
const config = await writeMinimalInitConfig();
|
|
8467
|
+
const existingToken = await loadAuthToken();
|
|
8468
|
+
if (!existingToken) {
|
|
8469
|
+
const token = generateAuthToken();
|
|
8470
|
+
await writeAuthToken(token);
|
|
6886
8471
|
process.stdout.write(
|
|
6887
|
-
`Initialized ${paths.
|
|
6888
|
-
Auth token: ${
|
|
8472
|
+
`Initialized ${paths.authToken()}
|
|
8473
|
+
Auth token: ${token}
|
|
6889
8474
|
`
|
|
6890
8475
|
);
|
|
6891
8476
|
return;
|
|
6892
8477
|
}
|
|
6893
8478
|
if (flagBool(flags, "rotate-token")) {
|
|
6894
8479
|
const newToken = generateAuthToken();
|
|
6895
|
-
await
|
|
6896
|
-
const daemon = raw.daemon ??= {};
|
|
6897
|
-
daemon.authToken = newToken;
|
|
6898
|
-
});
|
|
8480
|
+
await writeAuthToken(newToken);
|
|
6899
8481
|
process.stdout.write(
|
|
6900
|
-
`Rotated token in ${paths.
|
|
8482
|
+
`Rotated token in ${paths.authToken()}
|
|
6901
8483
|
New token: ${newToken}
|
|
6902
8484
|
`
|
|
6903
8485
|
);
|
|
6904
8486
|
return;
|
|
6905
8487
|
}
|
|
6906
|
-
process.stdout.write(`
|
|
8488
|
+
process.stdout.write(`Auth token already exists at ${paths.authToken()}.
|
|
6907
8489
|
`);
|
|
6908
8490
|
process.stdout.write("Pass --rotate-token to generate a new auth token.\n");
|
|
6909
8491
|
}
|
|
@@ -6911,13 +8493,13 @@ New token: ${newToken}
|
|
|
6911
8493
|
// src/cli/commands/daemon.ts
|
|
6912
8494
|
init_paths();
|
|
6913
8495
|
init_config();
|
|
6914
|
-
import * as
|
|
8496
|
+
import * as fsp6 from "fs/promises";
|
|
6915
8497
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
6916
8498
|
|
|
6917
8499
|
// src/daemon/server.ts
|
|
6918
8500
|
init_config();
|
|
6919
|
-
import * as
|
|
6920
|
-
import * as
|
|
8501
|
+
import * as fs11 from "fs";
|
|
8502
|
+
import * as fsp4 from "fs/promises";
|
|
6921
8503
|
import Fastify from "fastify";
|
|
6922
8504
|
import websocketPlugin from "@fastify/websocket";
|
|
6923
8505
|
import pino from "pino";
|
|
@@ -7072,60 +8654,177 @@ function formatProgress(agentId, received, total, done = false) {
|
|
|
7072
8654
|
const tag2 = done ? "downloaded" : "downloading";
|
|
7073
8655
|
return `hydra-acp: ${tag2} ${agentId} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
7074
8656
|
}
|
|
7075
|
-
const tag = done ? "downloaded" : "downloading";
|
|
7076
|
-
return `hydra-acp: ${tag} ${agentId} ${rxMb} MB`;
|
|
7077
|
-
}
|
|
7078
|
-
function inferArchiveName(url) {
|
|
7079
|
-
const u = new URL(url);
|
|
7080
|
-
const base = path2.posix.basename(u.pathname);
|
|
7081
|
-
return base || "archive";
|
|
7082
|
-
}
|
|
7083
|
-
async function extract(archivePath, dest) {
|
|
7084
|
-
const lower = archivePath.toLowerCase();
|
|
7085
|
-
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || lower.endsWith(".tar")) {
|
|
7086
|
-
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
7087
|
-
return;
|
|
8657
|
+
const tag = done ? "downloaded" : "downloading";
|
|
8658
|
+
return `hydra-acp: ${tag} ${agentId} ${rxMb} MB`;
|
|
8659
|
+
}
|
|
8660
|
+
function inferArchiveName(url) {
|
|
8661
|
+
const u = new URL(url);
|
|
8662
|
+
const base = path2.posix.basename(u.pathname);
|
|
8663
|
+
return base || "archive";
|
|
8664
|
+
}
|
|
8665
|
+
async function extract(archivePath, dest) {
|
|
8666
|
+
const lower = archivePath.toLowerCase();
|
|
8667
|
+
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || lower.endsWith(".tar")) {
|
|
8668
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
8669
|
+
return;
|
|
8670
|
+
}
|
|
8671
|
+
if (lower.endsWith(".zip")) {
|
|
8672
|
+
if (await hasCommand("unzip")) {
|
|
8673
|
+
await run("unzip", ["-q", archivePath, "-d", dest]);
|
|
8674
|
+
return;
|
|
8675
|
+
}
|
|
8676
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
8677
|
+
return;
|
|
8678
|
+
}
|
|
8679
|
+
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
8680
|
+
}
|
|
8681
|
+
function run(cmd, args) {
|
|
8682
|
+
return new Promise((resolve5, reject) => {
|
|
8683
|
+
const child = spawn(cmd, args, {
|
|
8684
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
8685
|
+
});
|
|
8686
|
+
child.on("error", reject);
|
|
8687
|
+
child.on("exit", (code, signal) => {
|
|
8688
|
+
if (code === 0) {
|
|
8689
|
+
resolve5();
|
|
8690
|
+
return;
|
|
8691
|
+
}
|
|
8692
|
+
reject(
|
|
8693
|
+
new Error(
|
|
8694
|
+
`${cmd} ${args.join(" ")} exited with ${code !== null ? `code ${code}` : `signal ${signal}`}`
|
|
8695
|
+
)
|
|
8696
|
+
);
|
|
8697
|
+
});
|
|
8698
|
+
});
|
|
8699
|
+
}
|
|
8700
|
+
async function hasCommand(name) {
|
|
8701
|
+
return new Promise((resolve5) => {
|
|
8702
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
8703
|
+
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
8704
|
+
child.on("error", () => resolve5(false));
|
|
8705
|
+
child.on("exit", (code) => resolve5(code === 0));
|
|
8706
|
+
});
|
|
8707
|
+
}
|
|
8708
|
+
async function fileExists(p) {
|
|
8709
|
+
try {
|
|
8710
|
+
await fsp.access(p);
|
|
8711
|
+
return true;
|
|
8712
|
+
} catch {
|
|
8713
|
+
return false;
|
|
8714
|
+
}
|
|
8715
|
+
}
|
|
8716
|
+
|
|
8717
|
+
// src/core/npm-install.ts
|
|
8718
|
+
init_paths();
|
|
8719
|
+
import * as fsp2 from "fs/promises";
|
|
8720
|
+
import * as path3 from "path";
|
|
8721
|
+
import { spawn as spawn2 } from "child_process";
|
|
8722
|
+
var logSink2 = (msg) => {
|
|
8723
|
+
process.stderr.write(msg + "\n");
|
|
8724
|
+
};
|
|
8725
|
+
function setNpmInstallLogger(log) {
|
|
8726
|
+
logSink2 = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
8727
|
+
}
|
|
8728
|
+
async function ensureNpmPackage(args) {
|
|
8729
|
+
const platformKey = currentPlatformKey();
|
|
8730
|
+
if (!platformKey) {
|
|
8731
|
+
throw new Error(
|
|
8732
|
+
`Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
|
|
8733
|
+
);
|
|
8734
|
+
}
|
|
8735
|
+
const installDir = paths.agentNpmInstallDir(
|
|
8736
|
+
args.agentId,
|
|
8737
|
+
platformKey,
|
|
8738
|
+
args.version
|
|
8739
|
+
);
|
|
8740
|
+
const binPath = path3.join(installDir, "node_modules", ".bin", args.bin);
|
|
8741
|
+
if (await fileExists2(binPath)) {
|
|
8742
|
+
return binPath;
|
|
8743
|
+
}
|
|
8744
|
+
await installInto({
|
|
8745
|
+
agentId: args.agentId,
|
|
8746
|
+
packageSpec: args.packageSpec,
|
|
8747
|
+
installDir
|
|
8748
|
+
});
|
|
8749
|
+
if (!await fileExists2(binPath)) {
|
|
8750
|
+
throw new Error(
|
|
8751
|
+
`Agent ${args.agentId}: npm install of ${args.packageSpec} did not produce bin ${args.bin} (looked in ${installDir}/node_modules/.bin/)`
|
|
8752
|
+
);
|
|
7088
8753
|
}
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
8754
|
+
return binPath;
|
|
8755
|
+
}
|
|
8756
|
+
async function installInto(args) {
|
|
8757
|
+
await fsp2.mkdir(path3.dirname(args.installDir), { recursive: true });
|
|
8758
|
+
const tempDir = await fsp2.mkdtemp(`${args.installDir}.partial-`);
|
|
8759
|
+
try {
|
|
8760
|
+
logSink2(
|
|
8761
|
+
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
8762
|
+
);
|
|
8763
|
+
await runNpmInstall({
|
|
8764
|
+
packageSpec: args.packageSpec,
|
|
8765
|
+
cwd: tempDir
|
|
8766
|
+
});
|
|
8767
|
+
try {
|
|
8768
|
+
await fsp2.rename(tempDir, args.installDir);
|
|
8769
|
+
} catch (err) {
|
|
8770
|
+
const e = err;
|
|
8771
|
+
if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists2(args.installDir)) {
|
|
8772
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
8773
|
+
() => void 0
|
|
8774
|
+
);
|
|
8775
|
+
return;
|
|
8776
|
+
}
|
|
8777
|
+
throw err;
|
|
7093
8778
|
}
|
|
7094
|
-
|
|
7095
|
-
|
|
8779
|
+
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
8780
|
+
} catch (err) {
|
|
8781
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
8782
|
+
() => void 0
|
|
8783
|
+
);
|
|
8784
|
+
throw err;
|
|
7096
8785
|
}
|
|
7097
|
-
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
7098
8786
|
}
|
|
7099
|
-
function
|
|
8787
|
+
function runNpmInstall(args) {
|
|
7100
8788
|
return new Promise((resolve5, reject) => {
|
|
7101
|
-
const child =
|
|
7102
|
-
|
|
8789
|
+
const child = spawn2(
|
|
8790
|
+
"npm",
|
|
8791
|
+
["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
|
|
8792
|
+
{
|
|
8793
|
+
cwd: args.cwd,
|
|
8794
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8795
|
+
}
|
|
8796
|
+
);
|
|
8797
|
+
let stderrTail = "";
|
|
8798
|
+
child.stdout?.on("data", (chunk) => {
|
|
8799
|
+
void chunk;
|
|
8800
|
+
});
|
|
8801
|
+
child.stderr?.setEncoding("utf8");
|
|
8802
|
+
child.stderr?.on("data", (chunk) => {
|
|
8803
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
8804
|
+
});
|
|
8805
|
+
child.on("error", (err) => {
|
|
8806
|
+
const msg = err.code === "ENOENT" ? `npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)` : err.message;
|
|
8807
|
+
reject(new Error(msg));
|
|
7103
8808
|
});
|
|
7104
|
-
child.on("error", reject);
|
|
7105
8809
|
child.on("exit", (code, signal) => {
|
|
7106
8810
|
if (code === 0) {
|
|
7107
8811
|
resolve5();
|
|
7108
8812
|
return;
|
|
7109
8813
|
}
|
|
8814
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
8815
|
+
const tail = stderrTail.trim();
|
|
7110
8816
|
reject(
|
|
7111
8817
|
new Error(
|
|
7112
|
-
|
|
8818
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
8819
|
+
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
7113
8820
|
)
|
|
7114
8821
|
);
|
|
7115
8822
|
});
|
|
7116
8823
|
});
|
|
7117
8824
|
}
|
|
7118
|
-
async function
|
|
7119
|
-
return new Promise((resolve5) => {
|
|
7120
|
-
const finder = process.platform === "win32" ? "where" : "which";
|
|
7121
|
-
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
7122
|
-
child.on("error", () => resolve5(false));
|
|
7123
|
-
child.on("exit", (code) => resolve5(code === 0));
|
|
7124
|
-
});
|
|
7125
|
-
}
|
|
7126
|
-
async function fileExists(p) {
|
|
8825
|
+
async function fileExists2(p) {
|
|
7127
8826
|
try {
|
|
7128
|
-
await
|
|
8827
|
+
await fsp2.access(p);
|
|
7129
8828
|
return true;
|
|
7130
8829
|
} catch {
|
|
7131
8830
|
return false;
|
|
@@ -7135,6 +8834,10 @@ async function fileExists(p) {
|
|
|
7135
8834
|
// src/core/registry.ts
|
|
7136
8835
|
var NpxDistribution = z2.object({
|
|
7137
8836
|
package: z2.string(),
|
|
8837
|
+
// The bin to invoke after install. Defaults to the package basename
|
|
8838
|
+
// (e.g. "claude-code" for "@anthropic-ai/claude-code"). Required when
|
|
8839
|
+
// the package exposes a bin name that differs from its basename.
|
|
8840
|
+
bin: z2.string().optional(),
|
|
7138
8841
|
args: z2.array(z2.string()).optional(),
|
|
7139
8842
|
env: z2.record(z2.string()).optional()
|
|
7140
8843
|
});
|
|
@@ -7294,13 +8997,27 @@ function npxPackageBasename(agent) {
|
|
|
7294
8997
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
7295
8998
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
7296
8999
|
}
|
|
7297
|
-
async function planSpawn(agent,
|
|
9000
|
+
async function planSpawn(agent, callerArgs = []) {
|
|
7298
9001
|
if (agent.distribution.npx) {
|
|
7299
9002
|
const npx = agent.distribution.npx;
|
|
7300
|
-
const
|
|
9003
|
+
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
9004
|
+
if (process.env.HYDRA_ACP_SKIP_NPM_PREFETCH) {
|
|
9005
|
+
return {
|
|
9006
|
+
command: "npx",
|
|
9007
|
+
args: ["-y", npx.package, ...tail],
|
|
9008
|
+
env: npx.env ?? {}
|
|
9009
|
+
};
|
|
9010
|
+
}
|
|
9011
|
+
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
9012
|
+
const binPath = await ensureNpmPackage({
|
|
9013
|
+
agentId: agent.id,
|
|
9014
|
+
version: agent.version ?? "current",
|
|
9015
|
+
packageSpec: npx.package,
|
|
9016
|
+
bin
|
|
9017
|
+
});
|
|
7301
9018
|
return {
|
|
7302
|
-
command:
|
|
7303
|
-
args,
|
|
9019
|
+
command: binPath,
|
|
9020
|
+
args: tail,
|
|
7304
9021
|
env: npx.env ?? {}
|
|
7305
9022
|
};
|
|
7306
9023
|
}
|
|
@@ -7316,30 +9033,27 @@ async function planSpawn(agent, extraArgs = []) {
|
|
|
7316
9033
|
version: agent.version ?? "current",
|
|
7317
9034
|
target
|
|
7318
9035
|
});
|
|
9036
|
+
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
7319
9037
|
return {
|
|
7320
9038
|
command: cmdPath,
|
|
7321
|
-
args:
|
|
9039
|
+
args: tail,
|
|
7322
9040
|
env: target.env ?? {}
|
|
7323
9041
|
};
|
|
7324
9042
|
}
|
|
7325
9043
|
if (agent.distribution.uvx) {
|
|
7326
9044
|
const uvx = agent.distribution.uvx;
|
|
7327
|
-
const
|
|
9045
|
+
const tail = callerArgs.length > 0 ? callerArgs : uvx.args ?? [];
|
|
7328
9046
|
return {
|
|
7329
9047
|
command: "uvx",
|
|
7330
|
-
args,
|
|
9048
|
+
args: [uvx.package, ...tail],
|
|
7331
9049
|
env: uvx.env ?? {}
|
|
7332
9050
|
};
|
|
7333
9051
|
}
|
|
7334
9052
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
7335
9053
|
}
|
|
7336
9054
|
|
|
7337
|
-
// src/core/session-manager.ts
|
|
7338
|
-
import * as fs8 from "fs/promises";
|
|
7339
|
-
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
7340
|
-
|
|
7341
9055
|
// src/core/agent-instance.ts
|
|
7342
|
-
import { spawn as
|
|
9056
|
+
import { spawn as spawn3 } from "child_process";
|
|
7343
9057
|
|
|
7344
9058
|
// src/acp/framing.ts
|
|
7345
9059
|
init_types();
|
|
@@ -7420,17 +9134,22 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
7420
9134
|
|
|
7421
9135
|
// src/core/agent-instance.ts
|
|
7422
9136
|
init_connection();
|
|
9137
|
+
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
7423
9138
|
var AgentInstance = class _AgentInstance {
|
|
7424
9139
|
agentId;
|
|
7425
9140
|
cwd;
|
|
7426
9141
|
connection;
|
|
7427
9142
|
child;
|
|
7428
9143
|
exited = false;
|
|
9144
|
+
killed = false;
|
|
9145
|
+
stderrTail = "";
|
|
9146
|
+
stderrTailBytes;
|
|
7429
9147
|
exitHandlers = [];
|
|
7430
9148
|
constructor(opts, child) {
|
|
7431
9149
|
this.agentId = opts.agentId;
|
|
7432
9150
|
this.cwd = opts.cwd;
|
|
7433
9151
|
this.child = child;
|
|
9152
|
+
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
7434
9153
|
if (!child.stdout || !child.stdin) {
|
|
7435
9154
|
throw new Error("agent subprocess missing stdio");
|
|
7436
9155
|
}
|
|
@@ -7438,22 +9157,36 @@ var AgentInstance = class _AgentInstance {
|
|
|
7438
9157
|
this.connection = new JsonRpcConnection(stream);
|
|
7439
9158
|
child.stderr?.setEncoding("utf8");
|
|
7440
9159
|
child.stderr?.on("data", (chunk) => {
|
|
9160
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
|
|
7441
9161
|
process.stderr.write(`[${opts.agentId}] ${chunk}`);
|
|
7442
9162
|
});
|
|
9163
|
+
child.on("error", (err) => {
|
|
9164
|
+
const msg = this.formatFailure(err.message);
|
|
9165
|
+
this.connection.fail(new Error(msg));
|
|
9166
|
+
});
|
|
7443
9167
|
child.on("exit", (code, signal) => {
|
|
7444
9168
|
this.exited = true;
|
|
9169
|
+
if (!this.killed) {
|
|
9170
|
+
const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
|
|
9171
|
+
this.connection.fail(new Error(this.formatFailure(reason)));
|
|
9172
|
+
}
|
|
7445
9173
|
for (const handler of this.exitHandlers) {
|
|
7446
9174
|
handler(code, signal);
|
|
7447
9175
|
}
|
|
7448
9176
|
});
|
|
7449
9177
|
}
|
|
9178
|
+
formatFailure(reason) {
|
|
9179
|
+
const tail = this.stderrTail.trim();
|
|
9180
|
+
return tail ? `${reason}
|
|
9181
|
+
stderr: ${tail}` : reason;
|
|
9182
|
+
}
|
|
7450
9183
|
static spawn(opts) {
|
|
7451
9184
|
const env = {
|
|
7452
9185
|
...process.env,
|
|
7453
9186
|
...opts.plan.env,
|
|
7454
9187
|
...opts.extraEnv ?? {}
|
|
7455
9188
|
};
|
|
7456
|
-
const child =
|
|
9189
|
+
const child = spawn3(opts.plan.command, opts.plan.args, {
|
|
7457
9190
|
cwd: opts.cwd,
|
|
7458
9191
|
env,
|
|
7459
9192
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7470,196 +9203,33 @@ var AgentInstance = class _AgentInstance {
|
|
|
7470
9203
|
if (this.exited) {
|
|
7471
9204
|
return;
|
|
7472
9205
|
}
|
|
9206
|
+
this.killed = true;
|
|
7473
9207
|
await this.connection.close().catch(() => void 0);
|
|
7474
9208
|
this.child.kill(signal);
|
|
7475
9209
|
}
|
|
7476
9210
|
};
|
|
7477
9211
|
|
|
7478
9212
|
// src/core/session-manager.ts
|
|
9213
|
+
import * as fs9 from "fs/promises";
|
|
9214
|
+
import * as os2 from "os";
|
|
9215
|
+
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
7479
9216
|
init_session();
|
|
7480
|
-
|
|
7481
|
-
// src/core/session-store.ts
|
|
7482
|
-
init_paths();
|
|
7483
|
-
import * as fs5 from "fs/promises";
|
|
7484
|
-
import * as path3 from "path";
|
|
7485
|
-
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
7486
|
-
import { z as z4 } from "zod";
|
|
7487
|
-
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7488
|
-
var generateRawId = customAlphabet2(HYDRA_ID_ALPHABET2, 16);
|
|
7489
|
-
var HYDRA_LINEAGE_PREFIX = "hydra_lineage_";
|
|
7490
|
-
function generateLineageId() {
|
|
7491
|
-
return `${HYDRA_LINEAGE_PREFIX}${generateRawId()}`;
|
|
7492
|
-
}
|
|
7493
|
-
var PersistedAgentCommand = z4.object({
|
|
7494
|
-
name: z4.string(),
|
|
7495
|
-
description: z4.string().optional()
|
|
7496
|
-
});
|
|
7497
|
-
var PersistedUsage = z4.object({
|
|
7498
|
-
used: z4.number().optional(),
|
|
7499
|
-
size: z4.number().optional(),
|
|
7500
|
-
costAmount: z4.number().optional(),
|
|
7501
|
-
costCurrency: z4.string().optional()
|
|
7502
|
-
});
|
|
7503
|
-
var SessionRecord = z4.object({
|
|
7504
|
-
version: z4.literal(1),
|
|
7505
|
-
sessionId: z4.string(),
|
|
7506
|
-
// Optional for back-compat with records written before this field
|
|
7507
|
-
// existed; mergeForPersistence generates one on next write so any
|
|
7508
|
-
// touched session converges to having a lineageId. A record that
|
|
7509
|
-
// never gets written again (truly cold and untouched) just won't
|
|
7510
|
-
// participate in lineage-based dedup, which is correct — it was
|
|
7511
|
-
// never exported, so no incoming bundle can claim its lineage.
|
|
7512
|
-
lineageId: z4.string().optional(),
|
|
7513
|
-
upstreamSessionId: z4.string(),
|
|
7514
|
-
// When non-empty, marks a session that was created by import and is
|
|
7515
|
-
// waiting for its first attach to bootstrap a fresh upstream agent
|
|
7516
|
-
// and replay the imported history as a takeover transcript. The
|
|
7517
|
-
// origin's local id at export time, kept for debuggability and as a
|
|
7518
|
-
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
7519
|
-
importedFromSessionId: z4.string().optional(),
|
|
7520
|
-
agentId: z4.string(),
|
|
7521
|
-
cwd: z4.string(),
|
|
7522
|
-
title: z4.string().optional(),
|
|
7523
|
-
agentArgs: z4.array(z4.string()).optional(),
|
|
7524
|
-
// Snapshot of "what is currently true about this session" carried in
|
|
7525
|
-
// meta.json so a late-attaching or cold-resurrected client can be
|
|
7526
|
-
// told via the attach response _meta without depending on history
|
|
7527
|
-
// replay of a snapshot-shaped notification.
|
|
7528
|
-
currentModel: z4.string().optional(),
|
|
7529
|
-
currentMode: z4.string().optional(),
|
|
7530
|
-
currentUsage: PersistedUsage.optional(),
|
|
7531
|
-
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
7532
|
-
createdAt: z4.string(),
|
|
7533
|
-
updatedAt: z4.string()
|
|
7534
|
-
});
|
|
7535
|
-
var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
7536
|
-
function assertSafeId(id) {
|
|
7537
|
-
if (!SESSION_ID_PATTERN.test(id)) {
|
|
7538
|
-
throw new Error(`unsafe session id: ${id}`);
|
|
7539
|
-
}
|
|
7540
|
-
}
|
|
7541
|
-
var SessionStore = class {
|
|
7542
|
-
async write(record) {
|
|
7543
|
-
assertSafeId(record.sessionId);
|
|
7544
|
-
await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
7545
|
-
const full = { version: 1, ...record };
|
|
7546
|
-
await fs5.writeFile(
|
|
7547
|
-
paths.sessionFile(record.sessionId),
|
|
7548
|
-
JSON.stringify(full, null, 2) + "\n",
|
|
7549
|
-
{ encoding: "utf8", mode: 384 }
|
|
7550
|
-
);
|
|
7551
|
-
}
|
|
7552
|
-
async read(sessionId) {
|
|
7553
|
-
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
7554
|
-
return void 0;
|
|
7555
|
-
}
|
|
7556
|
-
let raw;
|
|
7557
|
-
try {
|
|
7558
|
-
raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
|
|
7559
|
-
} catch (err) {
|
|
7560
|
-
const e = err;
|
|
7561
|
-
if (e.code === "ENOENT") {
|
|
7562
|
-
return void 0;
|
|
7563
|
-
}
|
|
7564
|
-
throw err;
|
|
7565
|
-
}
|
|
7566
|
-
try {
|
|
7567
|
-
return SessionRecord.parse(JSON.parse(raw));
|
|
7568
|
-
} catch {
|
|
7569
|
-
return void 0;
|
|
7570
|
-
}
|
|
7571
|
-
}
|
|
7572
|
-
async delete(sessionId) {
|
|
7573
|
-
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
7574
|
-
return;
|
|
7575
|
-
}
|
|
7576
|
-
try {
|
|
7577
|
-
await fs5.unlink(paths.sessionFile(sessionId));
|
|
7578
|
-
} catch (err) {
|
|
7579
|
-
const e = err;
|
|
7580
|
-
if (e.code !== "ENOENT") {
|
|
7581
|
-
throw err;
|
|
7582
|
-
}
|
|
7583
|
-
}
|
|
7584
|
-
try {
|
|
7585
|
-
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
7586
|
-
} catch (err) {
|
|
7587
|
-
const e = err;
|
|
7588
|
-
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
7589
|
-
throw err;
|
|
7590
|
-
}
|
|
7591
|
-
}
|
|
7592
|
-
}
|
|
7593
|
-
// Find a persisted session by lineageId. Used by SessionManager.import
|
|
7594
|
-
// to detect bundles that have already been imported (lineageId match)
|
|
7595
|
-
// so we can either error out or, with replace:true, overwrite.
|
|
7596
|
-
// Returns undefined if no record has that lineageId. Records that
|
|
7597
|
-
// pre-date the lineageId field simply don't match — which is
|
|
7598
|
-
// correct: they were never exported, so no incoming bundle can
|
|
7599
|
-
// legitimately claim their lineage.
|
|
7600
|
-
async findByLineageId(lineageId) {
|
|
7601
|
-
if (lineageId.length === 0) {
|
|
7602
|
-
return void 0;
|
|
7603
|
-
}
|
|
7604
|
-
const all = await this.list().catch(() => []);
|
|
7605
|
-
for (const record of all) {
|
|
7606
|
-
if (record.lineageId === lineageId) {
|
|
7607
|
-
return record;
|
|
7608
|
-
}
|
|
7609
|
-
}
|
|
7610
|
-
return void 0;
|
|
7611
|
-
}
|
|
7612
|
-
async list() {
|
|
7613
|
-
let entries;
|
|
7614
|
-
try {
|
|
7615
|
-
entries = await fs5.readdir(paths.sessionsDir());
|
|
7616
|
-
} catch (err) {
|
|
7617
|
-
const e = err;
|
|
7618
|
-
if (e.code === "ENOENT") {
|
|
7619
|
-
return [];
|
|
7620
|
-
}
|
|
7621
|
-
throw err;
|
|
7622
|
-
}
|
|
7623
|
-
const records = [];
|
|
7624
|
-
for (const entry of entries) {
|
|
7625
|
-
const record = await this.read(entry);
|
|
7626
|
-
if (record) {
|
|
7627
|
-
records.push(record);
|
|
7628
|
-
}
|
|
7629
|
-
}
|
|
7630
|
-
return records;
|
|
7631
|
-
}
|
|
7632
|
-
};
|
|
7633
|
-
function recordFromMemorySession(args) {
|
|
7634
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7635
|
-
return {
|
|
7636
|
-
sessionId: args.sessionId,
|
|
7637
|
-
lineageId: args.lineageId,
|
|
7638
|
-
upstreamSessionId: args.upstreamSessionId,
|
|
7639
|
-
importedFromSessionId: args.importedFromSessionId,
|
|
7640
|
-
agentId: args.agentId,
|
|
7641
|
-
cwd: args.cwd,
|
|
7642
|
-
title: args.title,
|
|
7643
|
-
agentArgs: args.agentArgs,
|
|
7644
|
-
currentModel: args.currentModel,
|
|
7645
|
-
currentMode: args.currentMode,
|
|
7646
|
-
currentUsage: args.currentUsage,
|
|
7647
|
-
agentCommands: args.agentCommands,
|
|
7648
|
-
createdAt: args.createdAt ?? now,
|
|
7649
|
-
updatedAt: args.updatedAt ?? now
|
|
7650
|
-
};
|
|
7651
|
-
}
|
|
9217
|
+
init_session_store();
|
|
7652
9218
|
|
|
7653
9219
|
// src/core/history-store.ts
|
|
7654
9220
|
init_paths();
|
|
7655
9221
|
import * as fs6 from "fs/promises";
|
|
7656
9222
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
7657
|
-
var
|
|
9223
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
7658
9224
|
var HistoryStore = class {
|
|
7659
9225
|
// Serialize writes per session id so appends and rewrites don't
|
|
7660
9226
|
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
7661
9227
|
// failed append doesn't poison every subsequent write.
|
|
7662
9228
|
writeQueues = /* @__PURE__ */ new Map();
|
|
9229
|
+
maxEntries;
|
|
9230
|
+
constructor(options = {}) {
|
|
9231
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
9232
|
+
}
|
|
7663
9233
|
async append(sessionId, entry) {
|
|
7664
9234
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
7665
9235
|
return;
|
|
@@ -7761,8 +9331,8 @@ var HistoryStore = class {
|
|
|
7761
9331
|
recordedAt: obj.recordedAt
|
|
7762
9332
|
});
|
|
7763
9333
|
}
|
|
7764
|
-
if (out.length >
|
|
7765
|
-
return out.slice(-
|
|
9334
|
+
if (out.length > this.maxEntries) {
|
|
9335
|
+
return out.slice(-this.maxEntries);
|
|
7766
9336
|
}
|
|
7767
9337
|
return out;
|
|
7768
9338
|
}
|
|
@@ -7807,6 +9377,7 @@ var HistoryStore = class {
|
|
|
7807
9377
|
init_paths();
|
|
7808
9378
|
init_history();
|
|
7809
9379
|
init_types();
|
|
9380
|
+
init_hydra_version();
|
|
7810
9381
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7811
9382
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
7812
9383
|
var SessionManager = class {
|
|
@@ -7814,7 +9385,8 @@ var SessionManager = class {
|
|
|
7814
9385
|
this.registry = registry;
|
|
7815
9386
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
7816
9387
|
this.store = store ?? new SessionStore();
|
|
7817
|
-
this.
|
|
9388
|
+
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
9389
|
+
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
7818
9390
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
7819
9391
|
this.defaultModels = options.defaultModels ?? {};
|
|
7820
9392
|
}
|
|
@@ -7826,6 +9398,7 @@ var SessionManager = class {
|
|
|
7826
9398
|
histories;
|
|
7827
9399
|
idleTimeoutMs;
|
|
7828
9400
|
defaultModels;
|
|
9401
|
+
sessionHistoryMaxEntries;
|
|
7829
9402
|
// Serialize meta.json read-modify-write operations per session id so
|
|
7830
9403
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
7831
9404
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -7849,6 +9422,7 @@ var SessionManager = class {
|
|
|
7849
9422
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7850
9423
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7851
9424
|
historyStore: this.histories,
|
|
9425
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7852
9426
|
currentModel: fresh.initialModel
|
|
7853
9427
|
});
|
|
7854
9428
|
await this.attachManagerHooks(session);
|
|
@@ -7900,11 +9474,16 @@ var SessionManager = class {
|
|
|
7900
9474
|
cwd: params.cwd,
|
|
7901
9475
|
plan
|
|
7902
9476
|
});
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
9477
|
+
try {
|
|
9478
|
+
await agent.connection.request("initialize", {
|
|
9479
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
9480
|
+
clientCapabilities: {},
|
|
9481
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
9482
|
+
});
|
|
9483
|
+
} catch (err) {
|
|
9484
|
+
await agent.kill().catch(() => void 0);
|
|
9485
|
+
throw err;
|
|
9486
|
+
}
|
|
7908
9487
|
let loadResult;
|
|
7909
9488
|
try {
|
|
7910
9489
|
loadResult = await agent.connection.request(
|
|
@@ -7916,10 +9495,12 @@ var SessionManager = class {
|
|
|
7916
9495
|
}
|
|
7917
9496
|
);
|
|
7918
9497
|
} catch (err) {
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
9498
|
+
process.stderr.write(
|
|
9499
|
+
`session/load failed for upstream ${params.upstreamSessionId} on ${params.agentId} (${err.message}); recovering via import-reseed
|
|
9500
|
+
`
|
|
7922
9501
|
);
|
|
9502
|
+
await agent.kill().catch(() => void 0);
|
|
9503
|
+
return this.doResurrectFromImport(params);
|
|
7923
9504
|
}
|
|
7924
9505
|
const session = new Session({
|
|
7925
9506
|
sessionId: params.hydraSessionId,
|
|
@@ -7933,6 +9514,7 @@ var SessionManager = class {
|
|
|
7933
9514
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7934
9515
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7935
9516
|
historyStore: this.histories,
|
|
9517
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7936
9518
|
// Prefer what we previously stored from a current_model_update; if
|
|
7937
9519
|
// we never captured one (e.g. old opencode sessions on disk before
|
|
7938
9520
|
// this fix), fall back to the model the agent ships in its
|
|
@@ -7959,15 +9541,16 @@ var SessionManager = class {
|
|
|
7959
9541
|
// so subsequent resurrects of this session use the normal session/load
|
|
7960
9542
|
// path.
|
|
7961
9543
|
async doResurrectFromImport(params) {
|
|
9544
|
+
const cwd = await this.resolveImportCwd(params.cwd);
|
|
7962
9545
|
const fresh = await this.bootstrapAgent({
|
|
7963
9546
|
agentId: params.agentId,
|
|
7964
|
-
cwd
|
|
9547
|
+
cwd,
|
|
7965
9548
|
agentArgs: params.agentArgs,
|
|
7966
9549
|
mcpServers: []
|
|
7967
9550
|
});
|
|
7968
9551
|
const session = new Session({
|
|
7969
9552
|
sessionId: params.hydraSessionId,
|
|
7970
|
-
cwd
|
|
9553
|
+
cwd,
|
|
7971
9554
|
agentId: params.agentId,
|
|
7972
9555
|
agent: fresh.agent,
|
|
7973
9556
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
@@ -7977,6 +9560,7 @@ var SessionManager = class {
|
|
|
7977
9560
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7978
9561
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7979
9562
|
historyStore: this.histories,
|
|
9563
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7980
9564
|
// Prefer the stored value (set by a previous current_model_update);
|
|
7981
9565
|
// fall back to whatever the agent ships in its session/new response.
|
|
7982
9566
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
@@ -7990,6 +9574,16 @@ var SessionManager = class {
|
|
|
7990
9574
|
void session.seedFromImport().catch(() => void 0);
|
|
7991
9575
|
return session;
|
|
7992
9576
|
}
|
|
9577
|
+
async resolveImportCwd(cwd) {
|
|
9578
|
+
try {
|
|
9579
|
+
const stat4 = await fs9.stat(cwd);
|
|
9580
|
+
if (stat4.isDirectory()) {
|
|
9581
|
+
return cwd;
|
|
9582
|
+
}
|
|
9583
|
+
} catch {
|
|
9584
|
+
}
|
|
9585
|
+
return os2.homedir();
|
|
9586
|
+
}
|
|
7993
9587
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
7994
9588
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
7995
9589
|
// go through the same env / capabilities / error-handling.
|
|
@@ -8010,9 +9604,9 @@ var SessionManager = class {
|
|
|
8010
9604
|
});
|
|
8011
9605
|
try {
|
|
8012
9606
|
await agent.connection.request("initialize", {
|
|
8013
|
-
protocolVersion:
|
|
9607
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
8014
9608
|
clientCapabilities: {},
|
|
8015
|
-
clientInfo: { name: "hydra", version:
|
|
9609
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
8016
9610
|
});
|
|
8017
9611
|
const newResult = await agent.connection.request(
|
|
8018
9612
|
"session/new",
|
|
@@ -8292,7 +9886,8 @@ var SessionManager = class {
|
|
|
8292
9886
|
await this.writeImportedRecord({
|
|
8293
9887
|
sessionId: existing.sessionId,
|
|
8294
9888
|
bundle,
|
|
8295
|
-
preservedCreatedAt: existing.createdAt
|
|
9889
|
+
preservedCreatedAt: existing.createdAt,
|
|
9890
|
+
cwd: opts.cwd
|
|
8296
9891
|
});
|
|
8297
9892
|
return {
|
|
8298
9893
|
sessionId: existing.sessionId,
|
|
@@ -8301,7 +9896,11 @@ var SessionManager = class {
|
|
|
8301
9896
|
};
|
|
8302
9897
|
}
|
|
8303
9898
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
8304
|
-
await this.writeImportedRecord({
|
|
9899
|
+
await this.writeImportedRecord({
|
|
9900
|
+
sessionId: newId,
|
|
9901
|
+
bundle,
|
|
9902
|
+
cwd: opts.cwd
|
|
9903
|
+
});
|
|
8305
9904
|
return {
|
|
8306
9905
|
sessionId: newId,
|
|
8307
9906
|
importedFromSessionId: bundle.session.sessionId,
|
|
@@ -8331,7 +9930,7 @@ var SessionManager = class {
|
|
|
8331
9930
|
upstreamSessionId: "",
|
|
8332
9931
|
importedFromSessionId: args.bundle.session.sessionId,
|
|
8333
9932
|
agentId: args.bundle.session.agentId,
|
|
8334
|
-
cwd: args.bundle.session.cwd,
|
|
9933
|
+
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
8335
9934
|
title: args.bundle.session.title,
|
|
8336
9935
|
currentModel: args.bundle.session.currentModel,
|
|
8337
9936
|
currentMode: args.bundle.session.currentMode,
|
|
@@ -8524,7 +10123,7 @@ function asString(value) {
|
|
|
8524
10123
|
}
|
|
8525
10124
|
async function loadPromptHistorySafely(sessionId) {
|
|
8526
10125
|
try {
|
|
8527
|
-
const raw = await
|
|
10126
|
+
const raw = await fs9.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
8528
10127
|
const out = [];
|
|
8529
10128
|
for (const line of raw.split("\n")) {
|
|
8530
10129
|
if (line.length === 0) {
|
|
@@ -8545,7 +10144,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
8545
10144
|
}
|
|
8546
10145
|
async function historyMtimeIso(sessionId) {
|
|
8547
10146
|
try {
|
|
8548
|
-
const st = await
|
|
10147
|
+
const st = await fs9.stat(paths.historyFile(sessionId));
|
|
8549
10148
|
return new Date(st.mtimeMs).toISOString();
|
|
8550
10149
|
} catch {
|
|
8551
10150
|
return void 0;
|
|
@@ -8554,10 +10153,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
8554
10153
|
|
|
8555
10154
|
// src/core/extensions.ts
|
|
8556
10155
|
init_paths();
|
|
8557
|
-
import { spawn as
|
|
8558
|
-
import * as
|
|
8559
|
-
import * as
|
|
8560
|
-
import * as
|
|
10156
|
+
import { spawn as spawn4 } from "child_process";
|
|
10157
|
+
import * as fs10 from "fs";
|
|
10158
|
+
import * as fsp3 from "fs/promises";
|
|
10159
|
+
import * as path7 from "path";
|
|
8561
10160
|
var RESTART_BASE_MS = 1e3;
|
|
8562
10161
|
var RESTART_CAP_MS = 6e4;
|
|
8563
10162
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -8578,7 +10177,7 @@ var ExtensionManager = class {
|
|
|
8578
10177
|
if (!this.context) {
|
|
8579
10178
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
8580
10179
|
}
|
|
8581
|
-
await
|
|
10180
|
+
await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
|
|
8582
10181
|
await this.reapOrphans();
|
|
8583
10182
|
for (const entry of this.entries.values()) {
|
|
8584
10183
|
if (!entry.config.enabled) {
|
|
@@ -8787,7 +10386,7 @@ var ExtensionManager = class {
|
|
|
8787
10386
|
async reapOrphans() {
|
|
8788
10387
|
let entries;
|
|
8789
10388
|
try {
|
|
8790
|
-
entries = await
|
|
10389
|
+
entries = await fsp3.readdir(paths.extensionsDir());
|
|
8791
10390
|
} catch (err) {
|
|
8792
10391
|
const e = err;
|
|
8793
10392
|
if (e.code === "ENOENT") {
|
|
@@ -8799,10 +10398,10 @@ var ExtensionManager = class {
|
|
|
8799
10398
|
if (!entry.endsWith(".pid")) {
|
|
8800
10399
|
continue;
|
|
8801
10400
|
}
|
|
8802
|
-
const pidPath =
|
|
10401
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
8803
10402
|
let pid;
|
|
8804
10403
|
try {
|
|
8805
|
-
const raw = await
|
|
10404
|
+
const raw = await fsp3.readFile(pidPath, "utf8");
|
|
8806
10405
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
8807
10406
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
8808
10407
|
pid = parsed;
|
|
@@ -8825,7 +10424,7 @@ var ExtensionManager = class {
|
|
|
8825
10424
|
}
|
|
8826
10425
|
}
|
|
8827
10426
|
}
|
|
8828
|
-
await
|
|
10427
|
+
await fsp3.unlink(pidPath).catch(() => void 0);
|
|
8829
10428
|
}
|
|
8830
10429
|
}
|
|
8831
10430
|
spawn(entry, attempt) {
|
|
@@ -8838,7 +10437,7 @@ var ExtensionManager = class {
|
|
|
8838
10437
|
}
|
|
8839
10438
|
const ext = entry.config;
|
|
8840
10439
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
8841
|
-
const logStream =
|
|
10440
|
+
const logStream = fs10.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
8842
10441
|
flags: "a"
|
|
8843
10442
|
});
|
|
8844
10443
|
logStream.write(
|
|
@@ -8866,7 +10465,7 @@ var ExtensionManager = class {
|
|
|
8866
10465
|
const args = [...baseArgs, ...ext.args];
|
|
8867
10466
|
let child;
|
|
8868
10467
|
try {
|
|
8869
|
-
child =
|
|
10468
|
+
child = spawn4(cmd, args, {
|
|
8870
10469
|
env,
|
|
8871
10470
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8872
10471
|
detached: false
|
|
@@ -8888,7 +10487,7 @@ var ExtensionManager = class {
|
|
|
8888
10487
|
}
|
|
8889
10488
|
if (typeof child.pid === "number") {
|
|
8890
10489
|
try {
|
|
8891
|
-
|
|
10490
|
+
fs10.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
8892
10491
|
`, {
|
|
8893
10492
|
encoding: "utf8",
|
|
8894
10493
|
mode: 384
|
|
@@ -8913,7 +10512,7 @@ var ExtensionManager = class {
|
|
|
8913
10512
|
});
|
|
8914
10513
|
child.on("exit", (code, signal) => {
|
|
8915
10514
|
try {
|
|
8916
|
-
|
|
10515
|
+
fs10.unlinkSync(paths.extensionPidFile(ext.name));
|
|
8917
10516
|
} catch {
|
|
8918
10517
|
}
|
|
8919
10518
|
logStream.write(
|
|
@@ -8971,6 +10570,7 @@ function withCode2(err, code) {
|
|
|
8971
10570
|
|
|
8972
10571
|
// src/daemon/server.ts
|
|
8973
10572
|
init_paths();
|
|
10573
|
+
init_hydra_version();
|
|
8974
10574
|
|
|
8975
10575
|
// src/daemon/auth.ts
|
|
8976
10576
|
var BEARER_PREFIX = "Bearer ";
|
|
@@ -9026,78 +10626,10 @@ function constantTimeEqual(a, b) {
|
|
|
9026
10626
|
|
|
9027
10627
|
// src/daemon/routes/sessions.ts
|
|
9028
10628
|
init_config();
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
// src/core/bundle.ts
|
|
9032
|
-
import { z as z5 } from "zod";
|
|
9033
|
-
var HistoryEntrySchema = z5.object({
|
|
9034
|
-
method: z5.string(),
|
|
9035
|
-
params: z5.unknown(),
|
|
9036
|
-
recordedAt: z5.number()
|
|
9037
|
-
});
|
|
9038
|
-
var BundleSession = z5.object({
|
|
9039
|
-
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
9040
|
-
// the local namespace; lineageId is what survives across hops).
|
|
9041
|
-
sessionId: z5.string(),
|
|
9042
|
-
// Required on bundles — the export path backfills if the source
|
|
9043
|
-
// record was written before lineageId existed.
|
|
9044
|
-
lineageId: z5.string(),
|
|
9045
|
-
agentId: z5.string(),
|
|
9046
|
-
cwd: z5.string(),
|
|
9047
|
-
title: z5.string().optional(),
|
|
9048
|
-
currentModel: z5.string().optional(),
|
|
9049
|
-
currentMode: z5.string().optional(),
|
|
9050
|
-
currentUsage: PersistedUsage.optional(),
|
|
9051
|
-
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
9052
|
-
createdAt: z5.string(),
|
|
9053
|
-
updatedAt: z5.string()
|
|
9054
|
-
});
|
|
9055
|
-
var Bundle = z5.object({
|
|
9056
|
-
version: z5.literal(1),
|
|
9057
|
-
exportedAt: z5.string(),
|
|
9058
|
-
exportedFrom: z5.object({
|
|
9059
|
-
hydraVersion: z5.string(),
|
|
9060
|
-
machine: z5.string()
|
|
9061
|
-
}),
|
|
9062
|
-
session: BundleSession,
|
|
9063
|
-
history: z5.array(HistoryEntrySchema),
|
|
9064
|
-
promptHistory: z5.array(z5.string()).optional()
|
|
9065
|
-
});
|
|
9066
|
-
function encodeBundle(params) {
|
|
9067
|
-
const bundle = {
|
|
9068
|
-
version: 1,
|
|
9069
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9070
|
-
exportedFrom: {
|
|
9071
|
-
hydraVersion: params.hydraVersion,
|
|
9072
|
-
machine: params.machine
|
|
9073
|
-
},
|
|
9074
|
-
session: {
|
|
9075
|
-
sessionId: params.record.sessionId,
|
|
9076
|
-
lineageId: params.record.lineageId,
|
|
9077
|
-
agentId: params.record.agentId,
|
|
9078
|
-
cwd: params.record.cwd,
|
|
9079
|
-
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
9080
|
-
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
9081
|
-
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
9082
|
-
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
9083
|
-
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
9084
|
-
createdAt: params.record.createdAt,
|
|
9085
|
-
updatedAt: params.record.updatedAt
|
|
9086
|
-
},
|
|
9087
|
-
history: params.history
|
|
9088
|
-
};
|
|
9089
|
-
if (params.promptHistory !== void 0) {
|
|
9090
|
-
bundle.promptHistory = params.promptHistory;
|
|
9091
|
-
}
|
|
9092
|
-
return bundle;
|
|
9093
|
-
}
|
|
9094
|
-
function decodeBundle(raw) {
|
|
9095
|
-
return Bundle.parse(raw);
|
|
9096
|
-
}
|
|
9097
|
-
|
|
9098
|
-
// src/daemon/routes/sessions.ts
|
|
10629
|
+
init_bundle();
|
|
9099
10630
|
init_types();
|
|
9100
|
-
|
|
10631
|
+
init_hydra_version();
|
|
10632
|
+
import * as os3 from "os";
|
|
9101
10633
|
function registerSessionRoutes(app, manager, defaults) {
|
|
9102
10634
|
app.get("/v1/sessions", async (request) => {
|
|
9103
10635
|
const query = request.query;
|
|
@@ -9168,7 +10700,7 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9168
10700
|
history: exported.history,
|
|
9169
10701
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
9170
10702
|
hydraVersion: HYDRA_VERSION,
|
|
9171
|
-
machine:
|
|
10703
|
+
machine: os3.hostname()
|
|
9172
10704
|
});
|
|
9173
10705
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
9174
10706
|
reply.header(
|
|
@@ -9183,6 +10715,14 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9183
10715
|
reply.code(400).send({ error: "missing bundle" });
|
|
9184
10716
|
return;
|
|
9185
10717
|
}
|
|
10718
|
+
let cwdOverride;
|
|
10719
|
+
if (body.cwd !== void 0) {
|
|
10720
|
+
if (typeof body.cwd !== "string" || body.cwd.length === 0) {
|
|
10721
|
+
reply.code(400).send({ error: "cwd must be a non-empty string" });
|
|
10722
|
+
return;
|
|
10723
|
+
}
|
|
10724
|
+
cwdOverride = body.cwd;
|
|
10725
|
+
}
|
|
9186
10726
|
let bundle;
|
|
9187
10727
|
try {
|
|
9188
10728
|
bundle = decodeBundle(body.bundle);
|
|
@@ -9195,7 +10735,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9195
10735
|
}
|
|
9196
10736
|
try {
|
|
9197
10737
|
const result = await manager.importBundle(bundle, {
|
|
9198
|
-
replace: body.replace === true
|
|
10738
|
+
replace: body.replace === true,
|
|
10739
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
9199
10740
|
});
|
|
9200
10741
|
reply.code(201).send(result);
|
|
9201
10742
|
} catch (err) {
|
|
@@ -9436,8 +10977,7 @@ init_connection();
|
|
|
9436
10977
|
init_ws_stream();
|
|
9437
10978
|
init_types();
|
|
9438
10979
|
import { nanoid as nanoid2 } from "nanoid";
|
|
9439
|
-
|
|
9440
|
-
var HYDRA_PROTOCOL_VERSION = 1;
|
|
10980
|
+
init_hydra_version();
|
|
9441
10981
|
function registerAcpWsEndpoint(app, deps) {
|
|
9442
10982
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
9443
10983
|
const token = tokenFromUpgradeRequest({
|
|
@@ -9700,8 +11240,8 @@ function buildResponseMeta(session) {
|
|
|
9700
11240
|
}
|
|
9701
11241
|
function buildInitializeResult() {
|
|
9702
11242
|
return {
|
|
9703
|
-
protocolVersion:
|
|
9704
|
-
agentInfo: { name: "hydra", version:
|
|
11243
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
11244
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION },
|
|
9705
11245
|
agentCapabilities: {
|
|
9706
11246
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
9707
11247
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -9740,14 +11280,13 @@ function bindClientToSession(connection, session, state, clientInfo) {
|
|
|
9740
11280
|
}
|
|
9741
11281
|
|
|
9742
11282
|
// src/daemon/server.ts
|
|
9743
|
-
var HYDRA_VERSION3 = "0.1.0";
|
|
9744
11283
|
async function startDaemon(config) {
|
|
9745
11284
|
ensureLoopbackOrTls(config);
|
|
9746
11285
|
const httpsOptions = config.daemon.tls ? {
|
|
9747
|
-
key: await
|
|
9748
|
-
cert: await
|
|
11286
|
+
key: await fsp4.readFile(config.daemon.tls.key),
|
|
11287
|
+
cert: await fsp4.readFile(config.daemon.tls.cert)
|
|
9749
11288
|
} : void 0;
|
|
9750
|
-
await
|
|
11289
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
9751
11290
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
9752
11291
|
config.daemon.logLevel
|
|
9753
11292
|
);
|
|
@@ -9756,12 +11295,18 @@ async function startDaemon(config) {
|
|
|
9756
11295
|
level: config.daemon.logLevel,
|
|
9757
11296
|
stream: logStream
|
|
9758
11297
|
},
|
|
9759
|
-
https: httpsOptions ?? null
|
|
11298
|
+
https: httpsOptions ?? null,
|
|
11299
|
+
// Session bundles can be large (full history + tool output);
|
|
11300
|
+
// the 1MB Fastify default rejects ordinary imports.
|
|
11301
|
+
bodyLimit: 256 * 1024 * 1024
|
|
9760
11302
|
});
|
|
9761
11303
|
await app.register(websocketPlugin);
|
|
9762
11304
|
setBinaryInstallLogger((msg) => {
|
|
9763
11305
|
app.log.info(msg);
|
|
9764
11306
|
});
|
|
11307
|
+
setNpmInstallLogger((msg) => {
|
|
11308
|
+
app.log.info(msg);
|
|
11309
|
+
});
|
|
9765
11310
|
const auth = bearerAuth({ config });
|
|
9766
11311
|
app.addHook("onRequest", async (request, reply) => {
|
|
9767
11312
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -9773,12 +11318,14 @@ async function startDaemon(config) {
|
|
|
9773
11318
|
await auth(request, reply);
|
|
9774
11319
|
});
|
|
9775
11320
|
const registry = new Registry(config);
|
|
9776
|
-
const
|
|
11321
|
+
const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
|
|
11322
|
+
const manager = new SessionManager(registry, spawner, void 0, {
|
|
9777
11323
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
9778
|
-
defaultModels: config.defaultModels
|
|
11324
|
+
defaultModels: config.defaultModels,
|
|
11325
|
+
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
|
|
9779
11326
|
});
|
|
9780
11327
|
const extensions = new ExtensionManager(extensionList(config));
|
|
9781
|
-
registerHealthRoutes(app,
|
|
11328
|
+
registerHealthRoutes(app, HYDRA_VERSION);
|
|
9782
11329
|
registerSessionRoutes(app, manager, {
|
|
9783
11330
|
agentId: config.defaultAgent,
|
|
9784
11331
|
cwd: config.defaultCwd
|
|
@@ -9797,8 +11344,8 @@ async function startDaemon(config) {
|
|
|
9797
11344
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
9798
11345
|
const address = app.server.address();
|
|
9799
11346
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
9800
|
-
await
|
|
9801
|
-
await
|
|
11347
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
11348
|
+
await fsp4.writeFile(
|
|
9802
11349
|
paths.pidFile(),
|
|
9803
11350
|
JSON.stringify({
|
|
9804
11351
|
pid: process.pid,
|
|
@@ -9824,9 +11371,10 @@ async function startDaemon(config) {
|
|
|
9824
11371
|
await manager.closeAll();
|
|
9825
11372
|
await manager.flushMetaWrites();
|
|
9826
11373
|
setBinaryInstallLogger(null);
|
|
11374
|
+
setNpmInstallLogger(null);
|
|
9827
11375
|
await app.close();
|
|
9828
11376
|
try {
|
|
9829
|
-
|
|
11377
|
+
fs11.unlinkSync(paths.pidFile());
|
|
9830
11378
|
} catch {
|
|
9831
11379
|
}
|
|
9832
11380
|
try {
|
|
@@ -9865,13 +11413,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
9865
11413
|
init_daemon_bootstrap();
|
|
9866
11414
|
|
|
9867
11415
|
// src/cli/commands/log-tail.ts
|
|
9868
|
-
import * as
|
|
9869
|
-
import * as
|
|
11416
|
+
import * as fs12 from "fs";
|
|
11417
|
+
import * as fsp5 from "fs/promises";
|
|
9870
11418
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
9871
11419
|
const opts = parseLogTailFlags(argv);
|
|
9872
|
-
let
|
|
11420
|
+
let stat4;
|
|
9873
11421
|
try {
|
|
9874
|
-
|
|
11422
|
+
stat4 = await fsp5.stat(logPath);
|
|
9875
11423
|
} catch (err) {
|
|
9876
11424
|
const e = err;
|
|
9877
11425
|
if (e.code === "ENOENT") {
|
|
@@ -9882,14 +11430,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9882
11430
|
}
|
|
9883
11431
|
throw err;
|
|
9884
11432
|
}
|
|
9885
|
-
let position = await printTail(logPath,
|
|
11433
|
+
let position = await printTail(logPath, stat4.size, opts.tail);
|
|
9886
11434
|
if (!opts.follow) {
|
|
9887
11435
|
return;
|
|
9888
11436
|
}
|
|
9889
11437
|
process.stdout.write(`-- following ${logPath} --
|
|
9890
11438
|
`);
|
|
9891
11439
|
let pending = false;
|
|
9892
|
-
const watcher =
|
|
11440
|
+
const watcher = fs12.watch(logPath, () => {
|
|
9893
11441
|
if (pending) {
|
|
9894
11442
|
return;
|
|
9895
11443
|
}
|
|
@@ -9897,14 +11445,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
9897
11445
|
setImmediate(async () => {
|
|
9898
11446
|
pending = false;
|
|
9899
11447
|
try {
|
|
9900
|
-
const s = await
|
|
11448
|
+
const s = await fsp5.stat(logPath);
|
|
9901
11449
|
if (s.size <= position) {
|
|
9902
11450
|
if (s.size < position) {
|
|
9903
11451
|
position = s.size;
|
|
9904
11452
|
}
|
|
9905
11453
|
return;
|
|
9906
11454
|
}
|
|
9907
|
-
const fd = await
|
|
11455
|
+
const fd = await fsp5.open(logPath, "r");
|
|
9908
11456
|
try {
|
|
9909
11457
|
const buf = Buffer.alloc(s.size - position);
|
|
9910
11458
|
await fd.read(buf, 0, buf.length, position);
|
|
@@ -9931,7 +11479,7 @@ async function printTail(logPath, fileSize, lines) {
|
|
|
9931
11479
|
return fileSize;
|
|
9932
11480
|
}
|
|
9933
11481
|
const CHUNK = 64 * 1024;
|
|
9934
|
-
const fd = await
|
|
11482
|
+
const fd = await fsp5.open(logPath, "r");
|
|
9935
11483
|
try {
|
|
9936
11484
|
let position = fileSize;
|
|
9937
11485
|
let collected = "";
|
|
@@ -10100,7 +11648,7 @@ async function runDaemonStatus() {
|
|
|
10100
11648
|
}
|
|
10101
11649
|
async function readPidFile() {
|
|
10102
11650
|
try {
|
|
10103
|
-
const raw = await
|
|
11651
|
+
const raw = await fsp6.readFile(paths.pidFile(), "utf8");
|
|
10104
11652
|
return JSON.parse(raw);
|
|
10105
11653
|
} catch (err) {
|
|
10106
11654
|
const e = err;
|
|
@@ -10125,7 +11673,7 @@ init_sessions();
|
|
|
10125
11673
|
// src/cli/commands/extensions.ts
|
|
10126
11674
|
init_config();
|
|
10127
11675
|
init_paths();
|
|
10128
|
-
import * as
|
|
11676
|
+
import * as fsp7 from "fs/promises";
|
|
10129
11677
|
init_sessions();
|
|
10130
11678
|
async function runExtensionsList() {
|
|
10131
11679
|
const config = await loadConfig();
|
|
@@ -10321,11 +11869,11 @@ async function runExtensionsRemove(name) {
|
|
|
10321
11869
|
}
|
|
10322
11870
|
}
|
|
10323
11871
|
async function readRawConfig() {
|
|
10324
|
-
const raw = await
|
|
11872
|
+
const raw = await fsp7.readFile(paths.config(), "utf8");
|
|
10325
11873
|
return JSON.parse(raw);
|
|
10326
11874
|
}
|
|
10327
11875
|
async function writeRawConfig(raw) {
|
|
10328
|
-
await
|
|
11876
|
+
await fsp7.writeFile(
|
|
10329
11877
|
paths.config(),
|
|
10330
11878
|
JSON.stringify(raw, null, 2) + "\n",
|
|
10331
11879
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -11090,8 +12638,8 @@ async function main() {
|
|
|
11090
12638
|
await runSessionsKill(positional[2]);
|
|
11091
12639
|
return;
|
|
11092
12640
|
}
|
|
11093
|
-
if (sub === "
|
|
11094
|
-
await
|
|
12641
|
+
if (sub === "remove") {
|
|
12642
|
+
await runSessionsRemove(positional[2]);
|
|
11095
12643
|
return;
|
|
11096
12644
|
}
|
|
11097
12645
|
if (sub === "export") {
|
|
@@ -11100,8 +12648,11 @@ async function main() {
|
|
|
11100
12648
|
return;
|
|
11101
12649
|
}
|
|
11102
12650
|
if (sub === "import") {
|
|
12651
|
+
const cwd = resolveOption(flags, "cwd");
|
|
11103
12652
|
await runSessionsImport(positional[2], {
|
|
11104
|
-
replace: flags.replace === true
|
|
12653
|
+
replace: flags.replace === true,
|
|
12654
|
+
info: flags.info === true,
|
|
12655
|
+
...cwd !== void 0 ? { cwd } : {}
|
|
11105
12656
|
});
|
|
11106
12657
|
return;
|
|
11107
12658
|
}
|
|
@@ -11204,9 +12755,9 @@ async function dispatchTui(flags, base) {
|
|
|
11204
12755
|
}
|
|
11205
12756
|
function readVersion() {
|
|
11206
12757
|
try {
|
|
11207
|
-
const here =
|
|
12758
|
+
const here = dirname6(fileURLToPath2(import.meta.url));
|
|
11208
12759
|
const pkg = JSON.parse(
|
|
11209
|
-
|
|
12760
|
+
readFileSync2(resolve4(here, "../package.json"), "utf8")
|
|
11210
12761
|
);
|
|
11211
12762
|
return pkg.version ?? "unknown";
|
|
11212
12763
|
} catch {
|
|
@@ -11233,11 +12784,11 @@ function printHelp() {
|
|
|
11233
12784
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
11234
12785
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
11235
12786
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
11236
|
-
" hydra-acp sessions
|
|
12787
|
+
" hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
|
|
11237
12788
|
" hydra-acp sessions export <id> [--out <file>|.]",
|
|
11238
12789
|
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
11239
|
-
" hydra-acp sessions import <file>|- [--replace]",
|
|
11240
|
-
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live)",
|
|
12790
|
+
" hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
|
|
12791
|
+
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live); --cwd overrides the bundle's recorded working directory; --info prints the bundle's meta without importing",
|
|
11241
12792
|
" hydra-acp extensions list List configured extensions and live state",
|
|
11242
12793
|
" hydra-acp extensions add <name> [opts] Add an extension to config",
|
|
11243
12794
|
" hydra-acp extensions remove <name> Remove an extension from config",
|