@hydra-acp/cli 0.1.7 → 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 +1 -1
- package/dist/cli.js +1701 -483
- package/dist/index.d.ts +59 -4
- package/dist/index.js +347 -75
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,19 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/core/paths.ts
|
|
13
13
|
import * as path from "path";
|
|
14
14
|
import * as os from "os";
|
|
15
|
+
function shortenHomePath(p) {
|
|
16
|
+
const home = os.homedir();
|
|
17
|
+
if (!home) {
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
if (p === home) {
|
|
21
|
+
return "~";
|
|
22
|
+
}
|
|
23
|
+
if (p.startsWith(home + "/")) {
|
|
24
|
+
return "~" + p.slice(home.length);
|
|
25
|
+
}
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
15
28
|
function hydraHome() {
|
|
16
29
|
const override = process.env[ROOT_ENV];
|
|
17
30
|
if (override && override.length > 0) {
|
|
@@ -45,6 +58,18 @@ var init_paths = __esm({
|
|
|
45
58
|
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
46
59
|
// shows which platforms have ever installed anything.
|
|
47
60
|
agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
|
|
61
|
+
// npm install cache for npx-distributed agents. The trailing
|
|
62
|
+
// node<ABI> segment keys on process.versions.modules so a Node
|
|
63
|
+
// major bump (different ABI → native modules incompatible) yields
|
|
64
|
+
// a fresh install rather than failing at require() time.
|
|
65
|
+
agentNpmInstallDir: (id, platformKey, version) => path.join(
|
|
66
|
+
hydraHome(),
|
|
67
|
+
"agents",
|
|
68
|
+
platformKey,
|
|
69
|
+
id,
|
|
70
|
+
version,
|
|
71
|
+
`node${process.versions.modules}`
|
|
72
|
+
),
|
|
48
73
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
49
74
|
// One directory per session id under sessions/. Co-locates the
|
|
50
75
|
// session record, its transcript, and any future per-session state
|
|
@@ -149,6 +174,9 @@ async function loadConfig() {
|
|
|
149
174
|
daemon.authToken = token;
|
|
150
175
|
return HydraConfig.parse(raw);
|
|
151
176
|
}
|
|
177
|
+
async function loadConfigReadOnly() {
|
|
178
|
+
return HydraConfigReadOnly.parse(await readConfigFile());
|
|
179
|
+
}
|
|
152
180
|
async function ensureConfig() {
|
|
153
181
|
if (!await loadAuthToken()) {
|
|
154
182
|
const token = generateAuthToken();
|
|
@@ -181,7 +209,7 @@ function expandHome(p) {
|
|
|
181
209
|
}
|
|
182
210
|
return p;
|
|
183
211
|
}
|
|
184
|
-
var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig;
|
|
212
|
+
var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig, HydraConfigReadOnly;
|
|
185
213
|
var init_config = __esm({
|
|
186
214
|
"src/core/config.ts"() {
|
|
187
215
|
"use strict";
|
|
@@ -197,7 +225,16 @@ var init_config = __esm({
|
|
|
197
225
|
authToken: z.string().min(16),
|
|
198
226
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
199
227
|
tls: TlsConfig.optional(),
|
|
200
|
-
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
|
|
228
|
+
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
|
|
229
|
+
// Cap on entries kept in a session's on-disk replay log (history.jsonl).
|
|
230
|
+
// Compaction trims to this many on a periodic basis; reads also slice
|
|
231
|
+
// to the tail at this length as a defensive measure against older
|
|
232
|
+
// daemons that may have written unbounded files.
|
|
233
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e3),
|
|
234
|
+
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
235
|
+
// daemon can include it in the diagnostic message when a spawn fails.
|
|
236
|
+
// Bump if your agents emit large tracebacks you want surfaced.
|
|
237
|
+
agentStderrTailBytes: z.number().int().positive().default(4096)
|
|
201
238
|
});
|
|
202
239
|
RegistryConfig = z.object({
|
|
203
240
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -220,7 +257,14 @@ var init_config = __esm({
|
|
|
220
257
|
// text selection requires shift+drag to bypass mouse reporting. Set
|
|
221
258
|
// false to disable capture — wheel scrollback stops working, but
|
|
222
259
|
// plain click-drag selects text via the terminal emulator.
|
|
223
|
-
mouse: z.boolean().default(true)
|
|
260
|
+
mouse: z.boolean().default(true),
|
|
261
|
+
// Size at which the TUI's session/update debug log (tui.log) rotates
|
|
262
|
+
// to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
|
|
263
|
+
logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
|
|
264
|
+
// Width cap on the cwd column in the `sessions list` output and the
|
|
265
|
+
// TUI picker. Set higher if you keep deeply-nested working directories
|
|
266
|
+
// and want them visible; the elastic title column shrinks to make room.
|
|
267
|
+
cwdColumnMaxWidth: z.number().int().positive().default(24)
|
|
224
268
|
});
|
|
225
269
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
226
270
|
ExtensionBody = z.object({
|
|
@@ -256,9 +300,14 @@ var init_config = __esm({
|
|
|
256
300
|
tui: TuiConfig.default({
|
|
257
301
|
repaintThrottleMs: 1e3,
|
|
258
302
|
maxScrollbackLines: 1e4,
|
|
259
|
-
mouse: true
|
|
303
|
+
mouse: true,
|
|
304
|
+
logMaxBytes: 5 * 1024 * 1024,
|
|
305
|
+
cwdColumnMaxWidth: 24
|
|
260
306
|
})
|
|
261
307
|
});
|
|
308
|
+
HydraConfigReadOnly = HydraConfig.extend({
|
|
309
|
+
daemon: DaemonConfig.omit({ authToken: true })
|
|
310
|
+
});
|
|
262
311
|
}
|
|
263
312
|
});
|
|
264
313
|
|
|
@@ -332,10 +381,11 @@ function extractHydraMeta(meta) {
|
|
|
332
381
|
function mergeMeta(passthrough, ours) {
|
|
333
382
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
334
383
|
}
|
|
335
|
-
var JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
384
|
+
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
|
|
336
385
|
var init_types = __esm({
|
|
337
386
|
"src/acp/types.ts"() {
|
|
338
387
|
"use strict";
|
|
388
|
+
ACP_PROTOCOL_VERSION = 1;
|
|
339
389
|
JsonRpcErrorCodes = {
|
|
340
390
|
ParseError: -32700,
|
|
341
391
|
InvalidRequest: -32600,
|
|
@@ -528,6 +578,13 @@ var init_connection = __esm({
|
|
|
528
578
|
}
|
|
529
579
|
await this.stream.close();
|
|
530
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
|
+
}
|
|
531
588
|
handleIncoming(message) {
|
|
532
589
|
if ("method" in message) {
|
|
533
590
|
if ("id" in message && message.id !== void 0) {
|
|
@@ -739,7 +796,7 @@ function firstLine(text, max) {
|
|
|
739
796
|
}
|
|
740
797
|
return void 0;
|
|
741
798
|
}
|
|
742
|
-
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;
|
|
743
800
|
var init_session = __esm({
|
|
744
801
|
"src/core/session.ts"() {
|
|
745
802
|
"use strict";
|
|
@@ -748,8 +805,7 @@ var init_session = __esm({
|
|
|
748
805
|
HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
749
806
|
generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
750
807
|
HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
751
|
-
|
|
752
|
-
COMPACT_EVERY = 200;
|
|
808
|
+
DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
753
809
|
Session = class {
|
|
754
810
|
sessionId;
|
|
755
811
|
cwd;
|
|
@@ -791,11 +847,13 @@ var init_session = __esm({
|
|
|
791
847
|
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
792
848
|
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
793
849
|
promptStartedAt;
|
|
794
|
-
// Counts appends since the last compaction. When it hits
|
|
850
|
+
// Counts appends since the last compaction. When it hits compactEvery
|
|
795
851
|
// we ask the history store to trim the file to the most recent
|
|
796
|
-
//
|
|
852
|
+
// historyMaxEntries. Keeps file growth bounded without per-append
|
|
797
853
|
// file-size checks.
|
|
798
854
|
appendCount = 0;
|
|
855
|
+
historyMaxEntries;
|
|
856
|
+
compactEvery;
|
|
799
857
|
// Permission requests that have been broadcast to one or more
|
|
800
858
|
// clients but have not yet resolved. Replayed to clients that
|
|
801
859
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -852,6 +910,8 @@ var init_session = __esm({
|
|
|
852
910
|
this.firstPromptSeeded = true;
|
|
853
911
|
}
|
|
854
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));
|
|
855
915
|
this.updatedAt = Date.now();
|
|
856
916
|
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
857
917
|
this.lastRecordedAt = this.updatedAt;
|
|
@@ -1690,9 +1750,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1690
1750
|
if (this.historyStore) {
|
|
1691
1751
|
const store = this.historyStore;
|
|
1692
1752
|
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
1693
|
-
if (this.appendCount >=
|
|
1753
|
+
if (this.appendCount >= this.compactEvery) {
|
|
1694
1754
|
this.appendCount = 0;
|
|
1695
|
-
void store.compact(this.sessionId,
|
|
1755
|
+
void store.compact(this.sessionId, this.historyMaxEntries).catch(
|
|
1696
1756
|
() => void 0
|
|
1697
1757
|
);
|
|
1698
1758
|
}
|
|
@@ -1809,9 +1869,187 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1809
1869
|
}
|
|
1810
1870
|
});
|
|
1811
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
|
+
|
|
1812
2050
|
// src/tui/history.ts
|
|
1813
2051
|
import { promises as fs7 } from "fs";
|
|
1814
|
-
import * as
|
|
2052
|
+
import * as path5 from "path";
|
|
1815
2053
|
async function loadHistory(file) {
|
|
1816
2054
|
let text;
|
|
1817
2055
|
try {
|
|
@@ -1855,7 +2093,7 @@ function appendEntry(history, entry) {
|
|
|
1855
2093
|
return out;
|
|
1856
2094
|
}
|
|
1857
2095
|
async function saveHistory(file, history) {
|
|
1858
|
-
await fs7.mkdir(
|
|
2096
|
+
await fs7.mkdir(path5.dirname(file), { recursive: true });
|
|
1859
2097
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1860
2098
|
await fs7.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1861
2099
|
}
|
|
@@ -1867,6 +2105,113 @@ var init_history = __esm({
|
|
|
1867
2105
|
}
|
|
1868
2106
|
});
|
|
1869
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
|
+
|
|
1870
2215
|
// src/acp/ws-stream.ts
|
|
1871
2216
|
function wsToMessageStream(ws) {
|
|
1872
2217
|
const messageHandlers = [];
|
|
@@ -1945,7 +2290,7 @@ var init_ws_stream = __esm({
|
|
|
1945
2290
|
});
|
|
1946
2291
|
|
|
1947
2292
|
// src/core/daemon-bootstrap.ts
|
|
1948
|
-
import { spawn as
|
|
2293
|
+
import { spawn as spawn5 } from "child_process";
|
|
1949
2294
|
import { setTimeout as sleep } from "timers/promises";
|
|
1950
2295
|
async function ensureDaemonReachable(config) {
|
|
1951
2296
|
if (await pingHealth(config)) {
|
|
@@ -1972,7 +2317,7 @@ function spawnDaemonDetached() {
|
|
|
1972
2317
|
if (!cliPath) {
|
|
1973
2318
|
throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
|
|
1974
2319
|
}
|
|
1975
|
-
const child =
|
|
2320
|
+
const child = spawn5(
|
|
1976
2321
|
process.execPath,
|
|
1977
2322
|
[cliPath, "daemon", "start", "--foreground"],
|
|
1978
2323
|
{
|
|
@@ -2020,8 +2365,8 @@ function formatAgentWithModel(agentId, model) {
|
|
|
2020
2365
|
}
|
|
2021
2366
|
return `${agent}${AGENT_MODEL_SEP}${short}`;
|
|
2022
2367
|
}
|
|
2023
|
-
function formatAgentCell(agentId,
|
|
2024
|
-
const base =
|
|
2368
|
+
function formatAgentCell(agentId, usage) {
|
|
2369
|
+
const base = agentId ?? "?";
|
|
2025
2370
|
if (!usage || typeof usage.costAmount !== "number") {
|
|
2026
2371
|
return base;
|
|
2027
2372
|
}
|
|
@@ -2058,10 +2403,10 @@ function toRow(s, now = Date.now()) {
|
|
|
2058
2403
|
session: stripHydraSessionPrefix(s.sessionId),
|
|
2059
2404
|
upstream: s.upstreamSessionId ?? "-",
|
|
2060
2405
|
state: formatState(s.status, s.attachedClients),
|
|
2061
|
-
agent: formatAgentCell(s.agentId, s.
|
|
2406
|
+
agent: formatAgentCell(s.agentId, s.currentUsage),
|
|
2062
2407
|
age: formatRelativeAge(s.updatedAt, now),
|
|
2063
2408
|
title: s.title ?? "-",
|
|
2064
|
-
cwd: s.cwd
|
|
2409
|
+
cwd: shortenHomePath(s.cwd)
|
|
2065
2410
|
};
|
|
2066
2411
|
}
|
|
2067
2412
|
function formatState(status, clients) {
|
|
@@ -2077,6 +2422,7 @@ function computeWidths(rows) {
|
|
|
2077
2422
|
state: maxLen(HEADER.state, rows.map((r) => r.state)),
|
|
2078
2423
|
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
2079
2424
|
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
2425
|
+
cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
|
|
2080
2426
|
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
2081
2427
|
};
|
|
2082
2428
|
}
|
|
@@ -2125,7 +2471,7 @@ function maxLen(headerCell, values) {
|
|
|
2125
2471
|
}
|
|
2126
2472
|
return max;
|
|
2127
2473
|
}
|
|
2128
|
-
function formatRow(r, w, maxWidth) {
|
|
2474
|
+
function formatRow(r, w, maxWidth, cwdMaxWidth = DEFAULT_CWD_MAX_WIDTH) {
|
|
2129
2475
|
const fixed = [
|
|
2130
2476
|
r.session.padEnd(w.session),
|
|
2131
2477
|
r.upstream.padEnd(w.upstream),
|
|
@@ -2134,20 +2480,18 @@ function formatRow(r, w, maxWidth) {
|
|
|
2134
2480
|
r.age.padStart(w.age)
|
|
2135
2481
|
].join(SEP);
|
|
2136
2482
|
if (maxWidth === void 0) {
|
|
2137
|
-
return [fixed, r.
|
|
2483
|
+
return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
|
|
2138
2484
|
}
|
|
2139
|
-
const titleCap = Math.min(w.title, TITLE_MAX_WIDTH);
|
|
2140
2485
|
const budget = maxWidth - fixed.length - SEP.length;
|
|
2141
2486
|
if (budget <= 0) {
|
|
2142
2487
|
return fixed.slice(0, maxWidth);
|
|
2143
2488
|
}
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
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);
|
|
2151
2495
|
}
|
|
2152
2496
|
function truncateRight(s, max) {
|
|
2153
2497
|
if (max <= 0) {
|
|
@@ -2175,11 +2519,12 @@ function truncateMiddle(s, max) {
|
|
|
2175
2519
|
const tail = max - 1 - head;
|
|
2176
2520
|
return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
|
|
2177
2521
|
}
|
|
2178
|
-
var HEADER, SEP,
|
|
2522
|
+
var HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
|
|
2179
2523
|
var init_session_row = __esm({
|
|
2180
2524
|
"src/cli/session-row.ts"() {
|
|
2181
2525
|
"use strict";
|
|
2182
2526
|
init_agent_display();
|
|
2527
|
+
init_paths();
|
|
2183
2528
|
init_session();
|
|
2184
2529
|
HEADER = {
|
|
2185
2530
|
session: "SESSION",
|
|
@@ -2191,14 +2536,13 @@ var init_session_row = __esm({
|
|
|
2191
2536
|
cwd: "CWD"
|
|
2192
2537
|
};
|
|
2193
2538
|
SEP = " ";
|
|
2194
|
-
|
|
2195
|
-
TITLE_MAX_WIDTH = 40;
|
|
2539
|
+
DEFAULT_CWD_MAX_WIDTH = 24;
|
|
2196
2540
|
}
|
|
2197
2541
|
});
|
|
2198
2542
|
|
|
2199
2543
|
// src/cli/commands/sessions.ts
|
|
2200
|
-
import * as
|
|
2201
|
-
import * as
|
|
2544
|
+
import * as fs13 from "fs/promises";
|
|
2545
|
+
import * as path8 from "path";
|
|
2202
2546
|
async function runSessionsList(opts = {}) {
|
|
2203
2547
|
const config = await loadConfig();
|
|
2204
2548
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -2237,9 +2581,10 @@ async function runSessionsList(opts = {}) {
|
|
|
2237
2581
|
const rows = visible.map((s) => toRow(s, now));
|
|
2238
2582
|
const widths = computeWidths(rows);
|
|
2239
2583
|
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2240
|
-
|
|
2584
|
+
const cwdMax = config.tui.cwdColumnMaxWidth;
|
|
2585
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
|
|
2241
2586
|
for (const r of rows) {
|
|
2242
|
-
process.stdout.write(formatRow(r, widths, maxWidth) + "\n");
|
|
2587
|
+
process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
|
|
2243
2588
|
}
|
|
2244
2589
|
if (truncated > 0) {
|
|
2245
2590
|
process.stdout.write(
|
|
@@ -2268,9 +2613,9 @@ async function runSessionsKill(id) {
|
|
|
2268
2613
|
process.stdout.write(`Killed ${id}
|
|
2269
2614
|
`);
|
|
2270
2615
|
}
|
|
2271
|
-
async function
|
|
2616
|
+
async function runSessionsRemove(id) {
|
|
2272
2617
|
if (!id) {
|
|
2273
|
-
process.stderr.write("Usage: hydra-acp sessions
|
|
2618
|
+
process.stderr.write("Usage: hydra-acp sessions remove <session-id>\n");
|
|
2274
2619
|
process.exit(2);
|
|
2275
2620
|
}
|
|
2276
2621
|
const config = await loadConfig();
|
|
@@ -2317,23 +2662,40 @@ async function runSessionsExport(id, outPath) {
|
|
|
2317
2662
|
return;
|
|
2318
2663
|
}
|
|
2319
2664
|
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
2320
|
-
await
|
|
2321
|
-
await
|
|
2665
|
+
await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
|
|
2666
|
+
await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2322
2667
|
process.stdout.write(`Wrote ${resolved}
|
|
2323
2668
|
`);
|
|
2324
2669
|
}
|
|
2325
2670
|
async function runSessionsImport(file, opts = {}) {
|
|
2326
2671
|
if (!file) {
|
|
2327
2672
|
process.stderr.write(
|
|
2328
|
-
"Usage: hydra-acp sessions import <file>|- [--replace]\n"
|
|
2673
|
+
"Usage: hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]\n"
|
|
2329
2674
|
);
|
|
2330
2675
|
process.exit(2);
|
|
2331
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
|
+
}
|
|
2332
2694
|
let body;
|
|
2333
2695
|
if (file === "-") {
|
|
2334
2696
|
body = await readStdin();
|
|
2335
2697
|
} else {
|
|
2336
|
-
body = await
|
|
2698
|
+
body = await fs13.readFile(file, "utf8");
|
|
2337
2699
|
}
|
|
2338
2700
|
let bundle;
|
|
2339
2701
|
try {
|
|
@@ -2343,6 +2705,11 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2343
2705
|
`);
|
|
2344
2706
|
process.exit(1);
|
|
2345
2707
|
}
|
|
2708
|
+
if (opts.info === true) {
|
|
2709
|
+
const inspectConfig = await loadConfigReadOnly();
|
|
2710
|
+
printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2346
2713
|
const config = await loadConfig();
|
|
2347
2714
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2348
2715
|
const response = await fetch(`${baseUrl}/v1/sessions/import`, {
|
|
@@ -2351,7 +2718,11 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2351
2718
|
"Content-Type": "application/json",
|
|
2352
2719
|
Authorization: `Bearer ${config.daemon.authToken}`
|
|
2353
2720
|
},
|
|
2354
|
-
body: JSON.stringify({
|
|
2721
|
+
body: JSON.stringify({
|
|
2722
|
+
bundle,
|
|
2723
|
+
replace: opts.replace === true,
|
|
2724
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
2725
|
+
})
|
|
2355
2726
|
});
|
|
2356
2727
|
if (response.status === 409) {
|
|
2357
2728
|
const detail = await response.json().catch(() => ({}));
|
|
@@ -2374,6 +2745,42 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2374
2745
|
`
|
|
2375
2746
|
);
|
|
2376
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
|
+
}
|
|
2377
2784
|
async function readStdin() {
|
|
2378
2785
|
const chunks = [];
|
|
2379
2786
|
for await (const chunk of process.stdin) {
|
|
@@ -2400,6 +2807,7 @@ var init_sessions = __esm({
|
|
|
2400
2807
|
"src/cli/commands/sessions.ts"() {
|
|
2401
2808
|
"use strict";
|
|
2402
2809
|
init_config();
|
|
2810
|
+
init_bundle();
|
|
2403
2811
|
init_session_row();
|
|
2404
2812
|
}
|
|
2405
2813
|
});
|
|
@@ -2734,12 +3142,15 @@ async function pickSession(term, opts) {
|
|
|
2734
3142
|
return b.updatedAt.localeCompare(a.updatedAt);
|
|
2735
3143
|
});
|
|
2736
3144
|
};
|
|
2737
|
-
let
|
|
3145
|
+
let allSessions = sortSessions(opts.sessions);
|
|
3146
|
+
let visible = allSessions;
|
|
2738
3147
|
let rows = visible.map((s) => toRow(s, Date.now()));
|
|
2739
3148
|
let widths = computeWidths(rows);
|
|
2740
3149
|
let total = 1 + visible.length;
|
|
2741
3150
|
let selectedIdx = 0;
|
|
2742
3151
|
let scrollOffset = 0;
|
|
3152
|
+
let searchActive = false;
|
|
3153
|
+
let searchTerm = "";
|
|
2743
3154
|
let mode = "normal";
|
|
2744
3155
|
let pendingAction = null;
|
|
2745
3156
|
let transientStatus = null;
|
|
@@ -2750,6 +3161,7 @@ async function pickSession(term, opts) {
|
|
|
2750
3161
|
let headerLine = "";
|
|
2751
3162
|
let sessionLines = [];
|
|
2752
3163
|
let startRow = 1;
|
|
3164
|
+
const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
|
|
2753
3165
|
const computeLayout = () => {
|
|
2754
3166
|
termHeight = readTermHeight(term);
|
|
2755
3167
|
termWidth = readTermWidth(term);
|
|
@@ -2757,8 +3169,8 @@ async function pickSession(term, opts) {
|
|
|
2757
3169
|
viewportSize = Math.min(visible.length, maxViewportRows);
|
|
2758
3170
|
const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
|
|
2759
3171
|
newSessionLabel = formatNewSessionLabel(opts.cwd, rowMaxWidth);
|
|
2760
|
-
headerLine = formatRow(HEADER, widths, rowMaxWidth);
|
|
2761
|
-
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));
|
|
2762
3174
|
};
|
|
2763
3175
|
const rebuildRows = () => {
|
|
2764
3176
|
rows = visible.map((s) => toRow(s, Date.now()));
|
|
@@ -2766,6 +3178,24 @@ async function pickSession(term, opts) {
|
|
|
2766
3178
|
total = 1 + visible.length;
|
|
2767
3179
|
computeLayout();
|
|
2768
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
|
+
};
|
|
2769
3199
|
const adjustScroll = () => {
|
|
2770
3200
|
if (selectedIdx === 0) {
|
|
2771
3201
|
return;
|
|
@@ -2828,6 +3258,13 @@ async function pickSession(term, opts) {
|
|
|
2828
3258
|
term.dim.noFormat(` ${transientStatus}`);
|
|
2829
3259
|
return;
|
|
2830
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
|
+
}
|
|
2831
3268
|
term.dim.noFormat(formatIndicator());
|
|
2832
3269
|
};
|
|
2833
3270
|
const indicatorRow = () => startRow + 3 + viewportSize;
|
|
@@ -2894,8 +3331,8 @@ async function pickSession(term, opts) {
|
|
|
2894
3331
|
const refresh = async (preferredId) => {
|
|
2895
3332
|
try {
|
|
2896
3333
|
const next = await listSessions(opts.config);
|
|
2897
|
-
|
|
2898
|
-
|
|
3334
|
+
allSessions = sortSessions(next);
|
|
3335
|
+
applyFilter();
|
|
2899
3336
|
if (preferredId !== void 0) {
|
|
2900
3337
|
const idx = visible.findIndex((s) => s.sessionId === preferredId);
|
|
2901
3338
|
if (idx >= 0) {
|
|
@@ -2992,7 +3429,37 @@ async function pickSession(term, opts) {
|
|
|
2992
3429
|
return;
|
|
2993
3430
|
}
|
|
2994
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
|
+
}
|
|
2995
3455
|
if (data?.isCharacter) {
|
|
3456
|
+
if (name === "/") {
|
|
3457
|
+
searchActive = true;
|
|
3458
|
+
searchTerm = "";
|
|
3459
|
+
applyFilter();
|
|
3460
|
+
renderFromScratch();
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
2996
3463
|
if (name === "r" || name === "R") {
|
|
2997
3464
|
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
2998
3465
|
void refresh(currentId);
|
|
@@ -3097,13 +3564,34 @@ function readTermWidth(term) {
|
|
|
3097
3564
|
function formatNewSessionLabel(cwd, maxWidth) {
|
|
3098
3565
|
const prefix = "+ New session in ";
|
|
3099
3566
|
const budget = Math.max(1, maxWidth - prefix.length);
|
|
3100
|
-
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;
|
|
3101
3588
|
}
|
|
3102
3589
|
var ROW_PREFIX_WIDTH;
|
|
3103
3590
|
var init_picker = __esm({
|
|
3104
3591
|
"src/tui/picker.ts"() {
|
|
3105
3592
|
"use strict";
|
|
3106
3593
|
init_session_row();
|
|
3594
|
+
init_paths();
|
|
3107
3595
|
init_session();
|
|
3108
3596
|
init_discovery();
|
|
3109
3597
|
ROW_PREFIX_WIDTH = 2;
|
|
@@ -3111,14 +3599,14 @@ var init_picker = __esm({
|
|
|
3111
3599
|
});
|
|
3112
3600
|
|
|
3113
3601
|
// src/tui/screen.ts
|
|
3114
|
-
import os3 from "os";
|
|
3115
3602
|
import stringWidth from "string-width";
|
|
3116
3603
|
import wrapAnsi from "wrap-ansi";
|
|
3117
|
-
function formattedLineSig(zone, width, line) {
|
|
3604
|
+
function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null) {
|
|
3605
|
+
const active = activeCol === null ? "" : `a${activeCol}`;
|
|
3118
3606
|
if (!line) {
|
|
3119
|
-
return `${zone}|${width}|empty`;
|
|
3607
|
+
return `${zone}|${width}|empty|${highlight2 ?? ""}|${active}`;
|
|
3120
3608
|
}
|
|
3121
|
-
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}`;
|
|
3122
3610
|
}
|
|
3123
3611
|
function computePromptVisualRows(buffer, room) {
|
|
3124
3612
|
const rows = [];
|
|
@@ -3130,9 +3618,24 @@ function computePromptVisualRows(buffer, room) {
|
|
|
3130
3618
|
}
|
|
3131
3619
|
let pos = 0;
|
|
3132
3620
|
while (pos < line.length) {
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
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;
|
|
3136
3639
|
}
|
|
3137
3640
|
}
|
|
3138
3641
|
if (rows.length === 0) {
|
|
@@ -3180,6 +3683,34 @@ function computePromptLayout(visualRows, state, maxRows) {
|
|
|
3180
3683
|
}
|
|
3181
3684
|
return { cursorVisualRow, cursorVisualCol, windowStart, rendered };
|
|
3182
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
|
+
}
|
|
3183
3714
|
function writeStyled(term, text, style) {
|
|
3184
3715
|
if (text.length === 0) {
|
|
3185
3716
|
return;
|
|
@@ -3242,6 +3773,12 @@ function writeStyled(term, text, style) {
|
|
|
3242
3773
|
case "heading-3":
|
|
3243
3774
|
term.bold.noFormat(text);
|
|
3244
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;
|
|
3245
3782
|
default:
|
|
3246
3783
|
term.noFormat(text);
|
|
3247
3784
|
}
|
|
@@ -3255,17 +3792,80 @@ function wrapAnsiBody(text, width) {
|
|
|
3255
3792
|
}
|
|
3256
3793
|
return wrapAnsi(text, width, { hard: true, trim: false }).split("\n");
|
|
3257
3794
|
}
|
|
3258
|
-
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 = {}) {
|
|
3259
3858
|
if (width <= 0) {
|
|
3260
3859
|
return [text];
|
|
3261
3860
|
}
|
|
3262
3861
|
if (text.length === 0) {
|
|
3263
3862
|
return [""];
|
|
3264
3863
|
}
|
|
3265
|
-
|
|
3864
|
+
const stripMarkup = opts.stripMarkup === true && hasTkMarkup(text);
|
|
3865
|
+
if (!stripMarkup && !NON_ASCII.test(text)) {
|
|
3266
3866
|
return wrapAscii(text, width);
|
|
3267
3867
|
}
|
|
3268
|
-
return wrapVisible(text, width);
|
|
3868
|
+
return wrapVisible(text, width, stripMarkup);
|
|
3269
3869
|
}
|
|
3270
3870
|
function wrapAscii(text, width) {
|
|
3271
3871
|
const out = [];
|
|
@@ -3290,32 +3890,33 @@ function wrapAscii(text, width) {
|
|
|
3290
3890
|
out.push(remaining);
|
|
3291
3891
|
return out;
|
|
3292
3892
|
}
|
|
3293
|
-
function wrapVisible(text, width) {
|
|
3893
|
+
function wrapVisible(text, width, stripMarkup) {
|
|
3294
3894
|
const out = [];
|
|
3295
|
-
const
|
|
3296
|
-
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3297
|
-
graphemes.push({ seg: segment, w: stringWidth(segment) });
|
|
3298
|
-
}
|
|
3895
|
+
const segments = stripMarkup ? [...segmentForWidth(text)] : graphemeSegments(text);
|
|
3299
3896
|
let i = 0;
|
|
3300
|
-
while (i <
|
|
3897
|
+
while (i < segments.length) {
|
|
3301
3898
|
let chunk = "";
|
|
3302
3899
|
let chunkW = 0;
|
|
3303
3900
|
let lastSpaceI = -1;
|
|
3304
3901
|
let chunkAtLastSpace = "";
|
|
3305
|
-
while (i <
|
|
3306
|
-
const
|
|
3307
|
-
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
|
+
}
|
|
3308
3909
|
break;
|
|
3309
3910
|
}
|
|
3310
|
-
if (
|
|
3911
|
+
if (s.text === " " && s.width === 1) {
|
|
3311
3912
|
lastSpaceI = i;
|
|
3312
3913
|
chunkAtLastSpace = chunk;
|
|
3313
3914
|
}
|
|
3314
|
-
chunk +=
|
|
3315
|
-
chunkW +=
|
|
3915
|
+
chunk += s.text;
|
|
3916
|
+
chunkW += s.width;
|
|
3316
3917
|
i += 1;
|
|
3317
3918
|
}
|
|
3318
|
-
if (i >=
|
|
3919
|
+
if (i >= segments.length) {
|
|
3319
3920
|
out.push(chunk);
|
|
3320
3921
|
break;
|
|
3321
3922
|
}
|
|
@@ -3323,7 +3924,7 @@ function wrapVisible(text, width) {
|
|
|
3323
3924
|
out.push(chunkAtLastSpace);
|
|
3324
3925
|
i = lastSpaceI + 1;
|
|
3325
3926
|
} else if (chunk.length === 0) {
|
|
3326
|
-
out.push(
|
|
3927
|
+
out.push(segments[i].text);
|
|
3327
3928
|
i += 1;
|
|
3328
3929
|
} else {
|
|
3329
3930
|
out.push(chunk);
|
|
@@ -3331,34 +3932,43 @@ function wrapVisible(text, width) {
|
|
|
3331
3932
|
}
|
|
3332
3933
|
return out;
|
|
3333
3934
|
}
|
|
3334
|
-
function
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
}
|
|
3339
|
-
if (p === home) {
|
|
3340
|
-
return "~";
|
|
3341
|
-
}
|
|
3342
|
-
if (p.startsWith(home + "/")) {
|
|
3343
|
-
return "~" + p.slice(home.length);
|
|
3935
|
+
function graphemeSegments(text) {
|
|
3936
|
+
const out = [];
|
|
3937
|
+
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3938
|
+
out.push({ text: segment, width: stringWidth(segment) });
|
|
3344
3939
|
}
|
|
3345
|
-
return
|
|
3940
|
+
return out;
|
|
3346
3941
|
}
|
|
3347
|
-
function truncate(text, max) {
|
|
3942
|
+
function truncate(text, max, opts = {}) {
|
|
3348
3943
|
if (max <= 0) {
|
|
3349
3944
|
return "";
|
|
3350
3945
|
}
|
|
3351
|
-
|
|
3946
|
+
const stripMarkup = opts.stripMarkup === true && hasTkMarkup(text);
|
|
3947
|
+
if (!stripMarkup && text.length <= max && !NON_ASCII.test(text)) {
|
|
3352
3948
|
return text;
|
|
3353
3949
|
}
|
|
3354
|
-
|
|
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
|
+
}
|
|
3355
3965
|
if (visible <= max) {
|
|
3356
3966
|
return text;
|
|
3357
3967
|
}
|
|
3358
3968
|
if (max <= 1) {
|
|
3359
|
-
return
|
|
3969
|
+
return takeFromSegments(segments, max);
|
|
3360
3970
|
}
|
|
3361
|
-
return
|
|
3971
|
+
return takeFromSegments(segments, max - 1) + "\u2026";
|
|
3362
3972
|
}
|
|
3363
3973
|
function takeByWidth(text, budget) {
|
|
3364
3974
|
if (budget <= 0) {
|
|
@@ -3371,8 +3981,23 @@ function takeByWidth(text, budget) {
|
|
|
3371
3981
|
if (used + w > budget) {
|
|
3372
3982
|
break;
|
|
3373
3983
|
}
|
|
3374
|
-
out += segment;
|
|
3375
|
-
used += w;
|
|
3984
|
+
out += segment;
|
|
3985
|
+
used += w;
|
|
3986
|
+
}
|
|
3987
|
+
return out;
|
|
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;
|
|
3376
4001
|
}
|
|
3377
4002
|
return out;
|
|
3378
4003
|
}
|
|
@@ -3472,6 +4097,10 @@ function mapKeyName(name) {
|
|
|
3472
4097
|
return "ctrl-o";
|
|
3473
4098
|
case "CTRL_P":
|
|
3474
4099
|
return "ctrl-p";
|
|
4100
|
+
case "CTRL_R":
|
|
4101
|
+
return "ctrl-r";
|
|
4102
|
+
case "CTRL_S":
|
|
4103
|
+
return "ctrl-s";
|
|
3475
4104
|
case "CTRL_U":
|
|
3476
4105
|
return "ctrl-u";
|
|
3477
4106
|
case "CTRL_W":
|
|
@@ -3484,11 +4113,12 @@ function mapKeyName(name) {
|
|
|
3484
4113
|
return null;
|
|
3485
4114
|
}
|
|
3486
4115
|
}
|
|
3487
|
-
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, shortId;
|
|
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;
|
|
3488
4117
|
var init_screen = __esm({
|
|
3489
4118
|
"src/tui/screen.ts"() {
|
|
3490
4119
|
"use strict";
|
|
3491
4120
|
init_agent_display();
|
|
4121
|
+
init_paths();
|
|
3492
4122
|
init_session();
|
|
3493
4123
|
HEADER_ROWS = 2;
|
|
3494
4124
|
BANNER_ROWS = 1;
|
|
@@ -3532,6 +4162,12 @@ var init_screen = __esm({
|
|
|
3532
4162
|
lineIds = /* @__PURE__ */ new WeakMap();
|
|
3533
4163
|
wrapCache = /* @__PURE__ */ new Map();
|
|
3534
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();
|
|
3535
4171
|
// Per-row signature of what was painted to each terminal row on the
|
|
3536
4172
|
// previous repaint. drawX methods funnel through paintRow(), which
|
|
3537
4173
|
// skips the moveTo+eraseLineAfter+write sequence when the new
|
|
@@ -3549,10 +4185,30 @@ var init_screen = __esm({
|
|
|
3549
4185
|
// above the bottom. Mouse wheel and PgUp/PgDn adjust this; new content
|
|
3550
4186
|
// pushes the view down naturally when at 0.
|
|
3551
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;
|
|
3552
4208
|
banner = {
|
|
3553
4209
|
status: "ready",
|
|
3554
4210
|
planMode: false,
|
|
3555
|
-
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",
|
|
3556
4212
|
queued: 0
|
|
3557
4213
|
};
|
|
3558
4214
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -3615,6 +4271,10 @@ var init_screen = __esm({
|
|
|
3615
4271
|
return;
|
|
3616
4272
|
}
|
|
3617
4273
|
this.started = false;
|
|
4274
|
+
if (this.bannerNotificationTimer) {
|
|
4275
|
+
clearTimeout(this.bannerNotificationTimer);
|
|
4276
|
+
this.bannerNotificationTimer = null;
|
|
4277
|
+
}
|
|
3618
4278
|
this.uninstallBracketedPaste();
|
|
3619
4279
|
this.term.off("key", this.keyHandler);
|
|
3620
4280
|
if (this.mouseEnabled) {
|
|
@@ -3889,6 +4549,58 @@ var init_screen = __esm({
|
|
|
3889
4549
|
this.drawBanner();
|
|
3890
4550
|
this.placeCursor();
|
|
3891
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
|
+
}
|
|
3892
4604
|
clearScrollback() {
|
|
3893
4605
|
this.lines = [];
|
|
3894
4606
|
this.keyedBlocks.clear();
|
|
@@ -4070,6 +4782,9 @@ var init_screen = __esm({
|
|
|
4070
4782
|
if (delta === 0) {
|
|
4071
4783
|
return;
|
|
4072
4784
|
}
|
|
4785
|
+
if (this.scrollbackSearch !== null) {
|
|
4786
|
+
this.acceptScrollbackSearch();
|
|
4787
|
+
}
|
|
4073
4788
|
const max = this.maxScrollOffset();
|
|
4074
4789
|
const next = Math.min(max, Math.max(0, this.scrollOffset + delta));
|
|
4075
4790
|
if (next === this.scrollOffset) {
|
|
@@ -4079,6 +4794,9 @@ var init_screen = __esm({
|
|
|
4079
4794
|
this.repaint();
|
|
4080
4795
|
}
|
|
4081
4796
|
scrollToBottom() {
|
|
4797
|
+
if (this.scrollbackSearch !== null) {
|
|
4798
|
+
this.acceptScrollbackSearch();
|
|
4799
|
+
}
|
|
4082
4800
|
if (this.scrollOffset === 0) {
|
|
4083
4801
|
return;
|
|
4084
4802
|
}
|
|
@@ -4086,6 +4804,9 @@ var init_screen = __esm({
|
|
|
4086
4804
|
this.repaint();
|
|
4087
4805
|
}
|
|
4088
4806
|
scrollToTop() {
|
|
4807
|
+
if (this.scrollbackSearch !== null) {
|
|
4808
|
+
this.acceptScrollbackSearch();
|
|
4809
|
+
}
|
|
4089
4810
|
const max = this.maxScrollOffset();
|
|
4090
4811
|
if (this.scrollOffset === max) {
|
|
4091
4812
|
return;
|
|
@@ -4093,6 +4814,221 @@ var init_screen = __esm({
|
|
|
4093
4814
|
this.scrollOffset = max;
|
|
4094
4815
|
this.repaint();
|
|
4095
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
|
+
}
|
|
4096
5032
|
scrollPageSize() {
|
|
4097
5033
|
return Math.max(1, this.scrollbackVisibleRows() - 2);
|
|
4098
5034
|
}
|
|
@@ -4215,8 +5151,8 @@ var init_screen = __esm({
|
|
|
4215
5151
|
}
|
|
4216
5152
|
if (usage) {
|
|
4217
5153
|
const col = Math.max(1, w - usage.length + 1);
|
|
4218
|
-
this.term.moveTo(col, 1);
|
|
4219
|
-
this.term.dim(usage);
|
|
5154
|
+
this.term.moveTo(col, 1).eraseLineAfter();
|
|
5155
|
+
this.term.dim.noFormat(usage);
|
|
4220
5156
|
}
|
|
4221
5157
|
});
|
|
4222
5158
|
}
|
|
@@ -4247,14 +5183,23 @@ var init_screen = __esm({
|
|
|
4247
5183
|
const start = Math.max(0, end - visibleRows);
|
|
4248
5184
|
const slice = wrapped.slice(start, end);
|
|
4249
5185
|
const padTop = Math.max(0, visibleRows - slice.length);
|
|
5186
|
+
const matchInfo = this.currentMatchInfo();
|
|
5187
|
+
const activeLength = matchInfo?.length ?? 0;
|
|
4250
5188
|
for (let i = 0; i < visibleRows; i++) {
|
|
4251
5189
|
const row = top + i;
|
|
4252
5190
|
const sliceIdx = i - padTop;
|
|
4253
5191
|
const line = sliceIdx >= 0 ? slice[sliceIdx] : void 0;
|
|
4254
|
-
const
|
|
5192
|
+
const activeCol = this.activeMatchCol(line, matchInfo);
|
|
5193
|
+
const sig = formattedLineSig(
|
|
5194
|
+
"sb",
|
|
5195
|
+
w,
|
|
5196
|
+
line,
|
|
5197
|
+
this.scrollbackHighlight,
|
|
5198
|
+
activeCol
|
|
5199
|
+
);
|
|
4255
5200
|
this.paintRow(row, sig, () => {
|
|
4256
5201
|
if (line) {
|
|
4257
|
-
this.writeFormattedLine(line, w);
|
|
5202
|
+
this.writeFormattedLine(line, w, activeCol, activeLength);
|
|
4258
5203
|
}
|
|
4259
5204
|
});
|
|
4260
5205
|
}
|
|
@@ -4454,7 +5399,9 @@ var init_screen = __esm({
|
|
|
4454
5399
|
const row = this.term.height;
|
|
4455
5400
|
const w = this.term.width;
|
|
4456
5401
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
4457
|
-
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;
|
|
4458
5405
|
this.paintRow(row, sig, () => {
|
|
4459
5406
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
4460
5407
|
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
@@ -4481,6 +5428,16 @@ var init_screen = __esm({
|
|
|
4481
5428
|
this.term.dim(planLabel);
|
|
4482
5429
|
}
|
|
4483
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
|
+
}
|
|
4484
5441
|
});
|
|
4485
5442
|
}
|
|
4486
5443
|
placeCursor() {
|
|
@@ -4496,6 +5453,11 @@ var init_screen = __esm({
|
|
|
4496
5453
|
this.term.moveTo(2, top2);
|
|
4497
5454
|
return;
|
|
4498
5455
|
}
|
|
5456
|
+
if (this.scrollbackSearch) {
|
|
5457
|
+
this.term.hideCursor(true);
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
this.term.hideCursor(false);
|
|
4499
5461
|
const w = this.term.width;
|
|
4500
5462
|
const room = Math.max(1, w - 2);
|
|
4501
5463
|
const state = this.dispatcher.state();
|
|
@@ -4582,8 +5544,10 @@ var init_screen = __esm({
|
|
|
4582
5544
|
}
|
|
4583
5545
|
const prefix = line.prefix ?? "";
|
|
4584
5546
|
const room = Math.max(1, width - prefix.length);
|
|
4585
|
-
const
|
|
5547
|
+
const stripMarkup = line.bodyStyle === "agent";
|
|
5548
|
+
const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room, { stripMarkup });
|
|
4586
5549
|
const wrapped = [];
|
|
5550
|
+
let scanPos = 0;
|
|
4587
5551
|
for (let i = 0; i < chunks.length; i++) {
|
|
4588
5552
|
const chunk = chunks[i] ?? "";
|
|
4589
5553
|
const wrappedLine = {
|
|
@@ -4602,6 +5566,15 @@ var init_screen = __esm({
|
|
|
4602
5566
|
if (line.ansi) {
|
|
4603
5567
|
wrappedLine.ansi = true;
|
|
4604
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
|
+
}
|
|
4605
5578
|
wrapped.push(wrappedLine);
|
|
4606
5579
|
}
|
|
4607
5580
|
if (id !== void 0) {
|
|
@@ -4609,13 +5582,25 @@ var init_screen = __esm({
|
|
|
4609
5582
|
}
|
|
4610
5583
|
return wrapped;
|
|
4611
5584
|
}
|
|
4612
|
-
writeFormattedLine(line, width) {
|
|
5585
|
+
writeFormattedLine(line, width, activeMatchCol = null, activeMatchLength = 0) {
|
|
4613
5586
|
if (line.prefix) {
|
|
4614
5587
|
writeStyled(this.term, line.prefix, line.prefixStyle ?? line.bodyStyle);
|
|
4615
5588
|
}
|
|
4616
5589
|
const remaining = Math.max(0, width - (line.prefix?.length ?? 0));
|
|
4617
|
-
const
|
|
4618
|
-
|
|
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
|
+
}
|
|
4619
5604
|
if (line.fillRow) {
|
|
4620
5605
|
const visible = line.ansi ? stringWidth(bodyText) : bodyText.length;
|
|
4621
5606
|
const pad = remaining - visible;
|
|
@@ -4630,6 +5615,7 @@ var init_screen = __esm({
|
|
|
4630
5615
|
};
|
|
4631
5616
|
NON_ASCII = /[^\x20-\x7e]/;
|
|
4632
5617
|
SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
5618
|
+
TK_MARKUP_STYLE_CHAR = /[a-zA-Z+\-:_!#/]/;
|
|
4633
5619
|
shortId = stripHydraSessionPrefix;
|
|
4634
5620
|
}
|
|
4635
5621
|
});
|
|
@@ -4653,6 +5639,14 @@ var init_input = __esm({
|
|
|
4653
5639
|
queueIndex = -1;
|
|
4654
5640
|
savedDraft = null;
|
|
4655
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;
|
|
4656
5650
|
// Waiting queue snapshot (excludes the in-flight head). Newest item lives
|
|
4657
5651
|
// at the end so Up walks the array right-to-left.
|
|
4658
5652
|
queue = [];
|
|
@@ -4672,7 +5666,8 @@ var init_input = __esm({
|
|
|
4672
5666
|
col: this.col,
|
|
4673
5667
|
planMode: this.planMode,
|
|
4674
5668
|
historyIndex: this.historyIndex,
|
|
4675
|
-
queueIndex: this.queueIndex
|
|
5669
|
+
queueIndex: this.queueIndex,
|
|
5670
|
+
historySearchQuery: this.historySearch?.query ?? null
|
|
4676
5671
|
};
|
|
4677
5672
|
}
|
|
4678
5673
|
setTurnRunning(running) {
|
|
@@ -4682,6 +5677,7 @@ var init_input = __esm({
|
|
|
4682
5677
|
this.history = [...history];
|
|
4683
5678
|
this.historyIndex = -1;
|
|
4684
5679
|
this.savedDraft = null;
|
|
5680
|
+
this.historySearch = null;
|
|
4685
5681
|
}
|
|
4686
5682
|
// Snapshot of the waiting queue (head excluded). Called by the app after
|
|
4687
5683
|
// every queue mutation so Up/Down can walk a fresh view. queueIndex is
|
|
@@ -4710,8 +5706,44 @@ var init_input = __esm({
|
|
|
4710
5706
|
this.historyIndex = -1;
|
|
4711
5707
|
this.queueIndex = -1;
|
|
4712
5708
|
this.savedDraft = null;
|
|
5709
|
+
this.historySearch = null;
|
|
4713
5710
|
}
|
|
4714
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
|
+
}
|
|
4715
5747
|
if (event.type === "char") {
|
|
4716
5748
|
this.insertChar(event.ch);
|
|
4717
5749
|
return [];
|
|
@@ -4789,6 +5821,10 @@ var init_input = __esm({
|
|
|
4789
5821
|
return [{ type: "redraw" }];
|
|
4790
5822
|
case "ctrl-p":
|
|
4791
5823
|
return [{ type: "switch-session" }];
|
|
5824
|
+
case "ctrl-r":
|
|
5825
|
+
return this.startHistorySearch();
|
|
5826
|
+
case "ctrl-s":
|
|
5827
|
+
return [];
|
|
4792
5828
|
case "ctrl-u":
|
|
4793
5829
|
this.killLine();
|
|
4794
5830
|
return [];
|
|
@@ -4824,6 +5860,7 @@ var init_input = __esm({
|
|
|
4824
5860
|
this.historyIndex = -1;
|
|
4825
5861
|
this.queueIndex = -1;
|
|
4826
5862
|
this.savedDraft = null;
|
|
5863
|
+
this.historySearch = null;
|
|
4827
5864
|
}
|
|
4828
5865
|
insertChar(ch) {
|
|
4829
5866
|
if (ch.length === 0) {
|
|
@@ -5051,6 +6088,143 @@ var init_input = __esm({
|
|
|
5051
6088
|
this.clearBuffer();
|
|
5052
6089
|
}
|
|
5053
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] ?? "");
|
|
6158
|
+
return [];
|
|
6159
|
+
}
|
|
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
|
+
}
|
|
5054
6228
|
loadEntry(text) {
|
|
5055
6229
|
this.buffer = text.split("\n");
|
|
5056
6230
|
if (this.buffer.length === 0) {
|
|
@@ -5790,6 +6964,7 @@ import { nanoid as nanoid3 } from "nanoid";
|
|
|
5790
6964
|
import termkit from "terminal-kit";
|
|
5791
6965
|
async function runTuiApp(opts) {
|
|
5792
6966
|
const config = await ensureConfig();
|
|
6967
|
+
logMaxBytes = config.tui.logMaxBytes;
|
|
5793
6968
|
await ensureDaemonReachable(config);
|
|
5794
6969
|
const term = termkit.terminal;
|
|
5795
6970
|
const exitHint = {};
|
|
@@ -5810,7 +6985,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5810
6985
|
process.exit(0);
|
|
5811
6986
|
}
|
|
5812
6987
|
const launchLabel = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
5813
|
-
term.
|
|
6988
|
+
term.brightYellow(launchLabel)("\n");
|
|
5814
6989
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
5815
6990
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
5816
6991
|
const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
|
|
@@ -5997,12 +7172,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
5997
7172
|
let agentInfoName;
|
|
5998
7173
|
try {
|
|
5999
7174
|
const initResult = await conn.request("initialize", {
|
|
6000
|
-
protocolVersion:
|
|
7175
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
6001
7176
|
clientCapabilities: {
|
|
6002
7177
|
fs: { readTextFile: false, writeTextFile: false },
|
|
6003
7178
|
terminal: false
|
|
6004
7179
|
},
|
|
6005
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
7180
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
6006
7181
|
});
|
|
6007
7182
|
agentInfoName = initResult?.agentInfo?.name;
|
|
6008
7183
|
} catch {
|
|
@@ -6051,7 +7226,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6051
7226
|
const attached = await conn.request("session/attach", {
|
|
6052
7227
|
sessionId: ctx.sessionId,
|
|
6053
7228
|
historyPolicy: "full",
|
|
6054
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
7229
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
6055
7230
|
});
|
|
6056
7231
|
resolvedSessionId = attached.sessionId;
|
|
6057
7232
|
exitHint.sessionId = resolvedSessionId;
|
|
@@ -6096,6 +7271,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6096
7271
|
if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
|
|
6097
7272
|
continue;
|
|
6098
7273
|
}
|
|
7274
|
+
if (tryHandleScrollbackSearchKey(ev)) {
|
|
7275
|
+
continue;
|
|
7276
|
+
}
|
|
6099
7277
|
if (tryHandleCompletionKey(ev)) {
|
|
6100
7278
|
continue;
|
|
6101
7279
|
}
|
|
@@ -6105,6 +7283,9 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6105
7283
|
}
|
|
6106
7284
|
}
|
|
6107
7285
|
refreshCompletions();
|
|
7286
|
+
screen.setBannerSearchIndicator(
|
|
7287
|
+
dispatcher.state().historySearchQuery
|
|
7288
|
+
);
|
|
6108
7289
|
screen.refreshPrompt();
|
|
6109
7290
|
}
|
|
6110
7291
|
});
|
|
@@ -6167,6 +7348,55 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6167
7348
|
dispatcher.replaceFirstLine(next);
|
|
6168
7349
|
return true;
|
|
6169
7350
|
};
|
|
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;
|
|
7357
|
+
}
|
|
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;
|
|
7396
|
+
}
|
|
7397
|
+
}
|
|
7398
|
+
return true;
|
|
7399
|
+
};
|
|
6170
7400
|
const tryHandlePermissionKey = (ev) => {
|
|
6171
7401
|
if (!pendingPermission) {
|
|
6172
7402
|
return false;
|
|
@@ -6437,6 +7667,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6437
7667
|
toolsExpanded = !toolsExpanded;
|
|
6438
7668
|
renderToolsBlock();
|
|
6439
7669
|
return;
|
|
7670
|
+
case "escalate-search":
|
|
7671
|
+
screen.enterScrollbackSearch();
|
|
7672
|
+
screen.updateScrollbackSearchTerm(effect.query);
|
|
7673
|
+
return;
|
|
6440
7674
|
}
|
|
6441
7675
|
};
|
|
6442
7676
|
const promptQueue = [];
|
|
@@ -6482,6 +7716,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6482
7716
|
toolCallOrder.length = 0;
|
|
6483
7717
|
toolsBlockStartedAt = null;
|
|
6484
7718
|
toolsBlockEndedAt = null;
|
|
7719
|
+
toolsBlockStopReason = null;
|
|
6485
7720
|
toolsExpanded = false;
|
|
6486
7721
|
screen.clearScrollback();
|
|
6487
7722
|
return true;
|
|
@@ -6685,6 +7920,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6685
7920
|
let toolsExpanded = false;
|
|
6686
7921
|
let toolsBlockStartedAt = null;
|
|
6687
7922
|
let toolsBlockEndedAt = null;
|
|
7923
|
+
let toolsBlockStopReason = null;
|
|
6688
7924
|
const TOOLS_COLLAPSED_LIMIT = 5;
|
|
6689
7925
|
let agentBuffer = "";
|
|
6690
7926
|
let agentKey = null;
|
|
@@ -6726,12 +7962,17 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6726
7962
|
const inProgress = toolsBlockEndedAt === null;
|
|
6727
7963
|
const end = toolsBlockEndedAt ?? Date.now();
|
|
6728
7964
|
const elapsed = end - toolsBlockStartedAt;
|
|
7965
|
+
const stoppedReason = !inProgress && toolsBlockStopReason !== null && toolsBlockStopReason !== "end_turn" ? toolsBlockStopReason : null;
|
|
6729
7966
|
let summary;
|
|
6730
7967
|
if (total === 0) {
|
|
6731
|
-
|
|
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
|
+
}
|
|
6732
7973
|
} else {
|
|
6733
7974
|
const noun = total === 1 ? "tool" : "tools";
|
|
6734
|
-
const timing = inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
7975
|
+
const timing = stoppedReason !== null ? `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}` : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
6735
7976
|
const parts = [`${total} ${noun}`, timing];
|
|
6736
7977
|
if (inProgress) {
|
|
6737
7978
|
if (hidden > 0) {
|
|
@@ -6743,12 +7984,14 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6743
7984
|
summary = parts.join(" \xB7 ");
|
|
6744
7985
|
}
|
|
6745
7986
|
const pureThinking = total === 0 && inProgress;
|
|
7987
|
+
const frozenStyle = stoppedReason !== null ? "tool-status-fail" : "tool";
|
|
7988
|
+
const frozenBodyStyle = stoppedReason !== null ? "tool-status-fail" : "dim";
|
|
6746
7989
|
const lines = [
|
|
6747
7990
|
{
|
|
6748
7991
|
prefix: "\u2692 ",
|
|
6749
|
-
prefixStyle: pureThinking ? "tool-status-running" :
|
|
7992
|
+
prefixStyle: pureThinking ? "tool-status-running" : frozenStyle,
|
|
6750
7993
|
body: summary,
|
|
6751
|
-
bodyStyle: pureThinking ? "tool-status-running" :
|
|
7994
|
+
bodyStyle: pureThinking ? "tool-status-running" : frozenBodyStyle
|
|
6752
7995
|
}
|
|
6753
7996
|
];
|
|
6754
7997
|
for (const id of visibleIds) {
|
|
@@ -6762,6 +8005,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6762
8005
|
const startToolsBlock = () => {
|
|
6763
8006
|
toolsBlockStartedAt = Date.now();
|
|
6764
8007
|
toolsBlockEndedAt = null;
|
|
8008
|
+
toolsBlockStopReason = null;
|
|
6765
8009
|
renderToolsBlock();
|
|
6766
8010
|
};
|
|
6767
8011
|
const recordToolCall = (id, title, status) => {
|
|
@@ -6786,6 +8030,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6786
8030
|
if (toolsBlockStartedAt === null) {
|
|
6787
8031
|
toolsBlockStartedAt = Date.now();
|
|
6788
8032
|
toolsBlockEndedAt = null;
|
|
8033
|
+
toolsBlockStopReason = null;
|
|
6789
8034
|
}
|
|
6790
8035
|
toolCallOrder.push(id);
|
|
6791
8036
|
}
|
|
@@ -6887,6 +8132,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6887
8132
|
screen.clearKey("plan");
|
|
6888
8133
|
if (toolsBlockStartedAt !== null) {
|
|
6889
8134
|
toolsBlockEndedAt = Date.now();
|
|
8135
|
+
toolsBlockStopReason = event.stopReason ?? null;
|
|
6890
8136
|
renderToolsBlock();
|
|
6891
8137
|
screen.clearKey("tools");
|
|
6892
8138
|
}
|
|
@@ -6894,6 +8140,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6894
8140
|
toolCallOrder.length = 0;
|
|
6895
8141
|
toolsBlockStartedAt = null;
|
|
6896
8142
|
toolsBlockEndedAt = null;
|
|
8143
|
+
toolsBlockStopReason = null;
|
|
6897
8144
|
toolsExpanded = false;
|
|
6898
8145
|
screen.ensureSeparator();
|
|
6899
8146
|
}
|
|
@@ -6937,12 +8184,14 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6937
8184
|
closeAgentText();
|
|
6938
8185
|
if (toolsBlockStartedAt !== null) {
|
|
6939
8186
|
toolsBlockEndedAt = Date.now();
|
|
8187
|
+
toolsBlockStopReason = null;
|
|
6940
8188
|
renderToolsBlock();
|
|
6941
8189
|
screen.clearKey("tools");
|
|
6942
8190
|
toolStates.clear();
|
|
6943
8191
|
toolCallOrder.length = 0;
|
|
6944
8192
|
toolsBlockStartedAt = null;
|
|
6945
8193
|
toolsBlockEndedAt = null;
|
|
8194
|
+
toolsBlockStopReason = null;
|
|
6946
8195
|
toolsExpanded = false;
|
|
6947
8196
|
}
|
|
6948
8197
|
screen.clearKey("plan");
|
|
@@ -6960,12 +8209,12 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6960
8209
|
id: `tui-reinit-${nanoid3()}`,
|
|
6961
8210
|
method: "initialize",
|
|
6962
8211
|
params: {
|
|
6963
|
-
protocolVersion:
|
|
8212
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
6964
8213
|
clientCapabilities: {
|
|
6965
8214
|
fs: { readTextFile: false, writeTextFile: false },
|
|
6966
8215
|
terminal: false
|
|
6967
8216
|
},
|
|
6968
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
8217
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
6969
8218
|
}
|
|
6970
8219
|
};
|
|
6971
8220
|
try {
|
|
@@ -6979,7 +8228,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
6979
8228
|
params: {
|
|
6980
8229
|
sessionId: resolvedSessionId,
|
|
6981
8230
|
historyPolicy: "none",
|
|
6982
|
-
clientInfo: { name: "hydra-acp-tui", version:
|
|
8231
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
|
|
6983
8232
|
...upstreamSessionId !== void 0 ? {
|
|
6984
8233
|
_meta: {
|
|
6985
8234
|
[HYDRA_META_KEY]: {
|
|
@@ -7103,15 +8352,15 @@ function writeDebugLine(payload) {
|
|
|
7103
8352
|
}
|
|
7104
8353
|
function rotateIfBig(target) {
|
|
7105
8354
|
try {
|
|
7106
|
-
const
|
|
7107
|
-
if (
|
|
8355
|
+
const stat4 = statSync(target);
|
|
8356
|
+
if (stat4.size < logMaxBytes) {
|
|
7108
8357
|
return;
|
|
7109
8358
|
}
|
|
7110
8359
|
renameSync(target, `${target}.0`);
|
|
7111
8360
|
} catch {
|
|
7112
8361
|
}
|
|
7113
8362
|
}
|
|
7114
|
-
var PLAN_PREFIX_TEXT,
|
|
8363
|
+
var PLAN_PREFIX_TEXT, logMaxBytes;
|
|
7115
8364
|
var init_app = __esm({
|
|
7116
8365
|
"src/tui/app.ts"() {
|
|
7117
8366
|
"use strict";
|
|
@@ -7122,6 +8371,7 @@ var init_app = __esm({
|
|
|
7122
8371
|
init_daemon_bootstrap();
|
|
7123
8372
|
init_session();
|
|
7124
8373
|
init_paths();
|
|
8374
|
+
init_hydra_version();
|
|
7125
8375
|
init_history();
|
|
7126
8376
|
init_discovery();
|
|
7127
8377
|
init_picker();
|
|
@@ -7131,7 +8381,7 @@ var init_app = __esm({
|
|
|
7131
8381
|
init_render_update();
|
|
7132
8382
|
init_format();
|
|
7133
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.";
|
|
7134
|
-
|
|
8384
|
+
logMaxBytes = 5 * 1024 * 1024;
|
|
7135
8385
|
}
|
|
7136
8386
|
});
|
|
7137
8387
|
|
|
@@ -7148,9 +8398,9 @@ var init_tui = __esm({
|
|
|
7148
8398
|
});
|
|
7149
8399
|
|
|
7150
8400
|
// src/cli.ts
|
|
7151
|
-
import { readFileSync } from "fs";
|
|
7152
|
-
import { fileURLToPath } from "url";
|
|
7153
|
-
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";
|
|
7154
8404
|
|
|
7155
8405
|
// src/cli/parse-args.ts
|
|
7156
8406
|
function parseArgs(argv) {
|
|
@@ -7243,13 +8493,13 @@ New token: ${newToken}
|
|
|
7243
8493
|
// src/cli/commands/daemon.ts
|
|
7244
8494
|
init_paths();
|
|
7245
8495
|
init_config();
|
|
7246
|
-
import * as
|
|
8496
|
+
import * as fsp6 from "fs/promises";
|
|
7247
8497
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
7248
8498
|
|
|
7249
8499
|
// src/daemon/server.ts
|
|
7250
8500
|
init_config();
|
|
7251
|
-
import * as
|
|
7252
|
-
import * as
|
|
8501
|
+
import * as fs11 from "fs";
|
|
8502
|
+
import * as fsp4 from "fs/promises";
|
|
7253
8503
|
import Fastify from "fastify";
|
|
7254
8504
|
import websocketPlugin from "@fastify/websocket";
|
|
7255
8505
|
import pino from "pino";
|
|
@@ -7404,60 +8654,177 @@ function formatProgress(agentId, received, total, done = false) {
|
|
|
7404
8654
|
const tag2 = done ? "downloaded" : "downloading";
|
|
7405
8655
|
return `hydra-acp: ${tag2} ${agentId} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
7406
8656
|
}
|
|
7407
|
-
const tag = done ? "downloaded" : "downloading";
|
|
7408
|
-
return `hydra-acp: ${tag} ${agentId} ${rxMb} MB`;
|
|
7409
|
-
}
|
|
7410
|
-
function inferArchiveName(url) {
|
|
7411
|
-
const u = new URL(url);
|
|
7412
|
-
const base = path2.posix.basename(u.pathname);
|
|
7413
|
-
return base || "archive";
|
|
7414
|
-
}
|
|
7415
|
-
async function extract(archivePath, dest) {
|
|
7416
|
-
const lower = archivePath.toLowerCase();
|
|
7417
|
-
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || lower.endsWith(".tar")) {
|
|
7418
|
-
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
7419
|
-
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
|
+
);
|
|
7420
8753
|
}
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
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;
|
|
7425
8778
|
}
|
|
7426
|
-
|
|
7427
|
-
|
|
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;
|
|
7428
8785
|
}
|
|
7429
|
-
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
7430
8786
|
}
|
|
7431
|
-
function
|
|
8787
|
+
function runNpmInstall(args) {
|
|
7432
8788
|
return new Promise((resolve5, reject) => {
|
|
7433
|
-
const child =
|
|
7434
|
-
|
|
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));
|
|
7435
8808
|
});
|
|
7436
|
-
child.on("error", reject);
|
|
7437
8809
|
child.on("exit", (code, signal) => {
|
|
7438
8810
|
if (code === 0) {
|
|
7439
8811
|
resolve5();
|
|
7440
8812
|
return;
|
|
7441
8813
|
}
|
|
8814
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
8815
|
+
const tail = stderrTail.trim();
|
|
7442
8816
|
reject(
|
|
7443
8817
|
new Error(
|
|
7444
|
-
|
|
8818
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
8819
|
+
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
7445
8820
|
)
|
|
7446
8821
|
);
|
|
7447
8822
|
});
|
|
7448
8823
|
});
|
|
7449
8824
|
}
|
|
7450
|
-
async function
|
|
7451
|
-
return new Promise((resolve5) => {
|
|
7452
|
-
const finder = process.platform === "win32" ? "where" : "which";
|
|
7453
|
-
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
7454
|
-
child.on("error", () => resolve5(false));
|
|
7455
|
-
child.on("exit", (code) => resolve5(code === 0));
|
|
7456
|
-
});
|
|
7457
|
-
}
|
|
7458
|
-
async function fileExists(p) {
|
|
8825
|
+
async function fileExists2(p) {
|
|
7459
8826
|
try {
|
|
7460
|
-
await
|
|
8827
|
+
await fsp2.access(p);
|
|
7461
8828
|
return true;
|
|
7462
8829
|
} catch {
|
|
7463
8830
|
return false;
|
|
@@ -7467,6 +8834,10 @@ async function fileExists(p) {
|
|
|
7467
8834
|
// src/core/registry.ts
|
|
7468
8835
|
var NpxDistribution = z2.object({
|
|
7469
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(),
|
|
7470
8841
|
args: z2.array(z2.string()).optional(),
|
|
7471
8842
|
env: z2.record(z2.string()).optional()
|
|
7472
8843
|
});
|
|
@@ -7630,9 +9001,23 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
7630
9001
|
if (agent.distribution.npx) {
|
|
7631
9002
|
const npx = agent.distribution.npx;
|
|
7632
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
|
+
});
|
|
7633
9018
|
return {
|
|
7634
|
-
command:
|
|
7635
|
-
args:
|
|
9019
|
+
command: binPath,
|
|
9020
|
+
args: tail,
|
|
7636
9021
|
env: npx.env ?? {}
|
|
7637
9022
|
};
|
|
7638
9023
|
}
|
|
@@ -7667,12 +9052,8 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
7667
9052
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
7668
9053
|
}
|
|
7669
9054
|
|
|
7670
|
-
// src/core/session-manager.ts
|
|
7671
|
-
import * as fs8 from "fs/promises";
|
|
7672
|
-
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
7673
|
-
|
|
7674
9055
|
// src/core/agent-instance.ts
|
|
7675
|
-
import { spawn as
|
|
9056
|
+
import { spawn as spawn3 } from "child_process";
|
|
7676
9057
|
|
|
7677
9058
|
// src/acp/framing.ts
|
|
7678
9059
|
init_types();
|
|
@@ -7753,17 +9134,22 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
7753
9134
|
|
|
7754
9135
|
// src/core/agent-instance.ts
|
|
7755
9136
|
init_connection();
|
|
9137
|
+
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
7756
9138
|
var AgentInstance = class _AgentInstance {
|
|
7757
9139
|
agentId;
|
|
7758
9140
|
cwd;
|
|
7759
9141
|
connection;
|
|
7760
9142
|
child;
|
|
7761
9143
|
exited = false;
|
|
9144
|
+
killed = false;
|
|
9145
|
+
stderrTail = "";
|
|
9146
|
+
stderrTailBytes;
|
|
7762
9147
|
exitHandlers = [];
|
|
7763
9148
|
constructor(opts, child) {
|
|
7764
9149
|
this.agentId = opts.agentId;
|
|
7765
9150
|
this.cwd = opts.cwd;
|
|
7766
9151
|
this.child = child;
|
|
9152
|
+
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
7767
9153
|
if (!child.stdout || !child.stdin) {
|
|
7768
9154
|
throw new Error("agent subprocess missing stdio");
|
|
7769
9155
|
}
|
|
@@ -7771,22 +9157,36 @@ var AgentInstance = class _AgentInstance {
|
|
|
7771
9157
|
this.connection = new JsonRpcConnection(stream);
|
|
7772
9158
|
child.stderr?.setEncoding("utf8");
|
|
7773
9159
|
child.stderr?.on("data", (chunk) => {
|
|
9160
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
|
|
7774
9161
|
process.stderr.write(`[${opts.agentId}] ${chunk}`);
|
|
7775
9162
|
});
|
|
9163
|
+
child.on("error", (err) => {
|
|
9164
|
+
const msg = this.formatFailure(err.message);
|
|
9165
|
+
this.connection.fail(new Error(msg));
|
|
9166
|
+
});
|
|
7776
9167
|
child.on("exit", (code, signal) => {
|
|
7777
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
|
+
}
|
|
7778
9173
|
for (const handler of this.exitHandlers) {
|
|
7779
9174
|
handler(code, signal);
|
|
7780
9175
|
}
|
|
7781
9176
|
});
|
|
7782
9177
|
}
|
|
9178
|
+
formatFailure(reason) {
|
|
9179
|
+
const tail = this.stderrTail.trim();
|
|
9180
|
+
return tail ? `${reason}
|
|
9181
|
+
stderr: ${tail}` : reason;
|
|
9182
|
+
}
|
|
7783
9183
|
static spawn(opts) {
|
|
7784
9184
|
const env = {
|
|
7785
9185
|
...process.env,
|
|
7786
9186
|
...opts.plan.env,
|
|
7787
9187
|
...opts.extraEnv ?? {}
|
|
7788
9188
|
};
|
|
7789
|
-
const child =
|
|
9189
|
+
const child = spawn3(opts.plan.command, opts.plan.args, {
|
|
7790
9190
|
cwd: opts.cwd,
|
|
7791
9191
|
env,
|
|
7792
9192
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7803,196 +9203,33 @@ var AgentInstance = class _AgentInstance {
|
|
|
7803
9203
|
if (this.exited) {
|
|
7804
9204
|
return;
|
|
7805
9205
|
}
|
|
9206
|
+
this.killed = true;
|
|
7806
9207
|
await this.connection.close().catch(() => void 0);
|
|
7807
9208
|
this.child.kill(signal);
|
|
7808
9209
|
}
|
|
7809
9210
|
};
|
|
7810
9211
|
|
|
7811
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";
|
|
7812
9216
|
init_session();
|
|
7813
|
-
|
|
7814
|
-
// src/core/session-store.ts
|
|
7815
|
-
init_paths();
|
|
7816
|
-
import * as fs5 from "fs/promises";
|
|
7817
|
-
import * as path3 from "path";
|
|
7818
|
-
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
7819
|
-
import { z as z4 } from "zod";
|
|
7820
|
-
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7821
|
-
var generateRawId = customAlphabet2(HYDRA_ID_ALPHABET2, 16);
|
|
7822
|
-
var HYDRA_LINEAGE_PREFIX = "hydra_lineage_";
|
|
7823
|
-
function generateLineageId() {
|
|
7824
|
-
return `${HYDRA_LINEAGE_PREFIX}${generateRawId()}`;
|
|
7825
|
-
}
|
|
7826
|
-
var PersistedAgentCommand = z4.object({
|
|
7827
|
-
name: z4.string(),
|
|
7828
|
-
description: z4.string().optional()
|
|
7829
|
-
});
|
|
7830
|
-
var PersistedUsage = z4.object({
|
|
7831
|
-
used: z4.number().optional(),
|
|
7832
|
-
size: z4.number().optional(),
|
|
7833
|
-
costAmount: z4.number().optional(),
|
|
7834
|
-
costCurrency: z4.string().optional()
|
|
7835
|
-
});
|
|
7836
|
-
var SessionRecord = z4.object({
|
|
7837
|
-
version: z4.literal(1),
|
|
7838
|
-
sessionId: z4.string(),
|
|
7839
|
-
// Optional for back-compat with records written before this field
|
|
7840
|
-
// existed; mergeForPersistence generates one on next write so any
|
|
7841
|
-
// touched session converges to having a lineageId. A record that
|
|
7842
|
-
// never gets written again (truly cold and untouched) just won't
|
|
7843
|
-
// participate in lineage-based dedup, which is correct — it was
|
|
7844
|
-
// never exported, so no incoming bundle can claim its lineage.
|
|
7845
|
-
lineageId: z4.string().optional(),
|
|
7846
|
-
upstreamSessionId: z4.string(),
|
|
7847
|
-
// When non-empty, marks a session that was created by import and is
|
|
7848
|
-
// waiting for its first attach to bootstrap a fresh upstream agent
|
|
7849
|
-
// and replay the imported history as a takeover transcript. The
|
|
7850
|
-
// origin's local id at export time, kept for debuggability and as a
|
|
7851
|
-
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
7852
|
-
importedFromSessionId: z4.string().optional(),
|
|
7853
|
-
agentId: z4.string(),
|
|
7854
|
-
cwd: z4.string(),
|
|
7855
|
-
title: z4.string().optional(),
|
|
7856
|
-
agentArgs: z4.array(z4.string()).optional(),
|
|
7857
|
-
// Snapshot of "what is currently true about this session" carried in
|
|
7858
|
-
// meta.json so a late-attaching or cold-resurrected client can be
|
|
7859
|
-
// told via the attach response _meta without depending on history
|
|
7860
|
-
// replay of a snapshot-shaped notification.
|
|
7861
|
-
currentModel: z4.string().optional(),
|
|
7862
|
-
currentMode: z4.string().optional(),
|
|
7863
|
-
currentUsage: PersistedUsage.optional(),
|
|
7864
|
-
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
7865
|
-
createdAt: z4.string(),
|
|
7866
|
-
updatedAt: z4.string()
|
|
7867
|
-
});
|
|
7868
|
-
var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
7869
|
-
function assertSafeId(id) {
|
|
7870
|
-
if (!SESSION_ID_PATTERN.test(id)) {
|
|
7871
|
-
throw new Error(`unsafe session id: ${id}`);
|
|
7872
|
-
}
|
|
7873
|
-
}
|
|
7874
|
-
var SessionStore = class {
|
|
7875
|
-
async write(record) {
|
|
7876
|
-
assertSafeId(record.sessionId);
|
|
7877
|
-
await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
7878
|
-
const full = { version: 1, ...record };
|
|
7879
|
-
await fs5.writeFile(
|
|
7880
|
-
paths.sessionFile(record.sessionId),
|
|
7881
|
-
JSON.stringify(full, null, 2) + "\n",
|
|
7882
|
-
{ encoding: "utf8", mode: 384 }
|
|
7883
|
-
);
|
|
7884
|
-
}
|
|
7885
|
-
async read(sessionId) {
|
|
7886
|
-
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
7887
|
-
return void 0;
|
|
7888
|
-
}
|
|
7889
|
-
let raw;
|
|
7890
|
-
try {
|
|
7891
|
-
raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
|
|
7892
|
-
} catch (err) {
|
|
7893
|
-
const e = err;
|
|
7894
|
-
if (e.code === "ENOENT") {
|
|
7895
|
-
return void 0;
|
|
7896
|
-
}
|
|
7897
|
-
throw err;
|
|
7898
|
-
}
|
|
7899
|
-
try {
|
|
7900
|
-
return SessionRecord.parse(JSON.parse(raw));
|
|
7901
|
-
} catch {
|
|
7902
|
-
return void 0;
|
|
7903
|
-
}
|
|
7904
|
-
}
|
|
7905
|
-
async delete(sessionId) {
|
|
7906
|
-
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
7907
|
-
return;
|
|
7908
|
-
}
|
|
7909
|
-
try {
|
|
7910
|
-
await fs5.unlink(paths.sessionFile(sessionId));
|
|
7911
|
-
} catch (err) {
|
|
7912
|
-
const e = err;
|
|
7913
|
-
if (e.code !== "ENOENT") {
|
|
7914
|
-
throw err;
|
|
7915
|
-
}
|
|
7916
|
-
}
|
|
7917
|
-
try {
|
|
7918
|
-
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
7919
|
-
} catch (err) {
|
|
7920
|
-
const e = err;
|
|
7921
|
-
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
7922
|
-
throw err;
|
|
7923
|
-
}
|
|
7924
|
-
}
|
|
7925
|
-
}
|
|
7926
|
-
// Find a persisted session by lineageId. Used by SessionManager.import
|
|
7927
|
-
// to detect bundles that have already been imported (lineageId match)
|
|
7928
|
-
// so we can either error out or, with replace:true, overwrite.
|
|
7929
|
-
// Returns undefined if no record has that lineageId. Records that
|
|
7930
|
-
// pre-date the lineageId field simply don't match — which is
|
|
7931
|
-
// correct: they were never exported, so no incoming bundle can
|
|
7932
|
-
// legitimately claim their lineage.
|
|
7933
|
-
async findByLineageId(lineageId) {
|
|
7934
|
-
if (lineageId.length === 0) {
|
|
7935
|
-
return void 0;
|
|
7936
|
-
}
|
|
7937
|
-
const all = await this.list().catch(() => []);
|
|
7938
|
-
for (const record of all) {
|
|
7939
|
-
if (record.lineageId === lineageId) {
|
|
7940
|
-
return record;
|
|
7941
|
-
}
|
|
7942
|
-
}
|
|
7943
|
-
return void 0;
|
|
7944
|
-
}
|
|
7945
|
-
async list() {
|
|
7946
|
-
let entries;
|
|
7947
|
-
try {
|
|
7948
|
-
entries = await fs5.readdir(paths.sessionsDir());
|
|
7949
|
-
} catch (err) {
|
|
7950
|
-
const e = err;
|
|
7951
|
-
if (e.code === "ENOENT") {
|
|
7952
|
-
return [];
|
|
7953
|
-
}
|
|
7954
|
-
throw err;
|
|
7955
|
-
}
|
|
7956
|
-
const records = [];
|
|
7957
|
-
for (const entry of entries) {
|
|
7958
|
-
const record = await this.read(entry);
|
|
7959
|
-
if (record) {
|
|
7960
|
-
records.push(record);
|
|
7961
|
-
}
|
|
7962
|
-
}
|
|
7963
|
-
return records;
|
|
7964
|
-
}
|
|
7965
|
-
};
|
|
7966
|
-
function recordFromMemorySession(args) {
|
|
7967
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7968
|
-
return {
|
|
7969
|
-
sessionId: args.sessionId,
|
|
7970
|
-
lineageId: args.lineageId,
|
|
7971
|
-
upstreamSessionId: args.upstreamSessionId,
|
|
7972
|
-
importedFromSessionId: args.importedFromSessionId,
|
|
7973
|
-
agentId: args.agentId,
|
|
7974
|
-
cwd: args.cwd,
|
|
7975
|
-
title: args.title,
|
|
7976
|
-
agentArgs: args.agentArgs,
|
|
7977
|
-
currentModel: args.currentModel,
|
|
7978
|
-
currentMode: args.currentMode,
|
|
7979
|
-
currentUsage: args.currentUsage,
|
|
7980
|
-
agentCommands: args.agentCommands,
|
|
7981
|
-
createdAt: args.createdAt ?? now,
|
|
7982
|
-
updatedAt: args.updatedAt ?? now
|
|
7983
|
-
};
|
|
7984
|
-
}
|
|
9217
|
+
init_session_store();
|
|
7985
9218
|
|
|
7986
9219
|
// src/core/history-store.ts
|
|
7987
9220
|
init_paths();
|
|
7988
9221
|
import * as fs6 from "fs/promises";
|
|
7989
9222
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
7990
|
-
var
|
|
9223
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
7991
9224
|
var HistoryStore = class {
|
|
7992
9225
|
// Serialize writes per session id so appends and rewrites don't
|
|
7993
9226
|
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
7994
9227
|
// failed append doesn't poison every subsequent write.
|
|
7995
9228
|
writeQueues = /* @__PURE__ */ new Map();
|
|
9229
|
+
maxEntries;
|
|
9230
|
+
constructor(options = {}) {
|
|
9231
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
9232
|
+
}
|
|
7996
9233
|
async append(sessionId, entry) {
|
|
7997
9234
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
7998
9235
|
return;
|
|
@@ -8094,8 +9331,8 @@ var HistoryStore = class {
|
|
|
8094
9331
|
recordedAt: obj.recordedAt
|
|
8095
9332
|
});
|
|
8096
9333
|
}
|
|
8097
|
-
if (out.length >
|
|
8098
|
-
return out.slice(-
|
|
9334
|
+
if (out.length > this.maxEntries) {
|
|
9335
|
+
return out.slice(-this.maxEntries);
|
|
8099
9336
|
}
|
|
8100
9337
|
return out;
|
|
8101
9338
|
}
|
|
@@ -8140,6 +9377,7 @@ var HistoryStore = class {
|
|
|
8140
9377
|
init_paths();
|
|
8141
9378
|
init_history();
|
|
8142
9379
|
init_types();
|
|
9380
|
+
init_hydra_version();
|
|
8143
9381
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
8144
9382
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
8145
9383
|
var SessionManager = class {
|
|
@@ -8147,7 +9385,8 @@ var SessionManager = class {
|
|
|
8147
9385
|
this.registry = registry;
|
|
8148
9386
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
8149
9387
|
this.store = store ?? new SessionStore();
|
|
8150
|
-
this.
|
|
9388
|
+
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
9389
|
+
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
8151
9390
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
8152
9391
|
this.defaultModels = options.defaultModels ?? {};
|
|
8153
9392
|
}
|
|
@@ -8159,6 +9398,7 @@ var SessionManager = class {
|
|
|
8159
9398
|
histories;
|
|
8160
9399
|
idleTimeoutMs;
|
|
8161
9400
|
defaultModels;
|
|
9401
|
+
sessionHistoryMaxEntries;
|
|
8162
9402
|
// Serialize meta.json read-modify-write operations per session id so
|
|
8163
9403
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
8164
9404
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -8182,6 +9422,7 @@ var SessionManager = class {
|
|
|
8182
9422
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
8183
9423
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
8184
9424
|
historyStore: this.histories,
|
|
9425
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
8185
9426
|
currentModel: fresh.initialModel
|
|
8186
9427
|
});
|
|
8187
9428
|
await this.attachManagerHooks(session);
|
|
@@ -8233,11 +9474,16 @@ var SessionManager = class {
|
|
|
8233
9474
|
cwd: params.cwd,
|
|
8234
9475
|
plan
|
|
8235
9476
|
});
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
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
|
+
}
|
|
8241
9487
|
let loadResult;
|
|
8242
9488
|
try {
|
|
8243
9489
|
loadResult = await agent.connection.request(
|
|
@@ -8249,10 +9495,12 @@ var SessionManager = class {
|
|
|
8249
9495
|
}
|
|
8250
9496
|
);
|
|
8251
9497
|
} catch (err) {
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
9498
|
+
process.stderr.write(
|
|
9499
|
+
`session/load failed for upstream ${params.upstreamSessionId} on ${params.agentId} (${err.message}); recovering via import-reseed
|
|
9500
|
+
`
|
|
8255
9501
|
);
|
|
9502
|
+
await agent.kill().catch(() => void 0);
|
|
9503
|
+
return this.doResurrectFromImport(params);
|
|
8256
9504
|
}
|
|
8257
9505
|
const session = new Session({
|
|
8258
9506
|
sessionId: params.hydraSessionId,
|
|
@@ -8266,6 +9514,7 @@ var SessionManager = class {
|
|
|
8266
9514
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
8267
9515
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
8268
9516
|
historyStore: this.histories,
|
|
9517
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
8269
9518
|
// Prefer what we previously stored from a current_model_update; if
|
|
8270
9519
|
// we never captured one (e.g. old opencode sessions on disk before
|
|
8271
9520
|
// this fix), fall back to the model the agent ships in its
|
|
@@ -8292,15 +9541,16 @@ var SessionManager = class {
|
|
|
8292
9541
|
// so subsequent resurrects of this session use the normal session/load
|
|
8293
9542
|
// path.
|
|
8294
9543
|
async doResurrectFromImport(params) {
|
|
9544
|
+
const cwd = await this.resolveImportCwd(params.cwd);
|
|
8295
9545
|
const fresh = await this.bootstrapAgent({
|
|
8296
9546
|
agentId: params.agentId,
|
|
8297
|
-
cwd
|
|
9547
|
+
cwd,
|
|
8298
9548
|
agentArgs: params.agentArgs,
|
|
8299
9549
|
mcpServers: []
|
|
8300
9550
|
});
|
|
8301
9551
|
const session = new Session({
|
|
8302
9552
|
sessionId: params.hydraSessionId,
|
|
8303
|
-
cwd
|
|
9553
|
+
cwd,
|
|
8304
9554
|
agentId: params.agentId,
|
|
8305
9555
|
agent: fresh.agent,
|
|
8306
9556
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
@@ -8310,6 +9560,7 @@ var SessionManager = class {
|
|
|
8310
9560
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
8311
9561
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
8312
9562
|
historyStore: this.histories,
|
|
9563
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
8313
9564
|
// Prefer the stored value (set by a previous current_model_update);
|
|
8314
9565
|
// fall back to whatever the agent ships in its session/new response.
|
|
8315
9566
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
@@ -8323,6 +9574,16 @@ var SessionManager = class {
|
|
|
8323
9574
|
void session.seedFromImport().catch(() => void 0);
|
|
8324
9575
|
return session;
|
|
8325
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
|
+
}
|
|
8326
9587
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
8327
9588
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
8328
9589
|
// go through the same env / capabilities / error-handling.
|
|
@@ -8343,9 +9604,9 @@ var SessionManager = class {
|
|
|
8343
9604
|
});
|
|
8344
9605
|
try {
|
|
8345
9606
|
await agent.connection.request("initialize", {
|
|
8346
|
-
protocolVersion:
|
|
9607
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
8347
9608
|
clientCapabilities: {},
|
|
8348
|
-
clientInfo: { name: "hydra", version:
|
|
9609
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
8349
9610
|
});
|
|
8350
9611
|
const newResult = await agent.connection.request(
|
|
8351
9612
|
"session/new",
|
|
@@ -8625,7 +9886,8 @@ var SessionManager = class {
|
|
|
8625
9886
|
await this.writeImportedRecord({
|
|
8626
9887
|
sessionId: existing.sessionId,
|
|
8627
9888
|
bundle,
|
|
8628
|
-
preservedCreatedAt: existing.createdAt
|
|
9889
|
+
preservedCreatedAt: existing.createdAt,
|
|
9890
|
+
cwd: opts.cwd
|
|
8629
9891
|
});
|
|
8630
9892
|
return {
|
|
8631
9893
|
sessionId: existing.sessionId,
|
|
@@ -8634,7 +9896,11 @@ var SessionManager = class {
|
|
|
8634
9896
|
};
|
|
8635
9897
|
}
|
|
8636
9898
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
8637
|
-
await this.writeImportedRecord({
|
|
9899
|
+
await this.writeImportedRecord({
|
|
9900
|
+
sessionId: newId,
|
|
9901
|
+
bundle,
|
|
9902
|
+
cwd: opts.cwd
|
|
9903
|
+
});
|
|
8638
9904
|
return {
|
|
8639
9905
|
sessionId: newId,
|
|
8640
9906
|
importedFromSessionId: bundle.session.sessionId,
|
|
@@ -8664,7 +9930,7 @@ var SessionManager = class {
|
|
|
8664
9930
|
upstreamSessionId: "",
|
|
8665
9931
|
importedFromSessionId: args.bundle.session.sessionId,
|
|
8666
9932
|
agentId: args.bundle.session.agentId,
|
|
8667
|
-
cwd: args.bundle.session.cwd,
|
|
9933
|
+
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
8668
9934
|
title: args.bundle.session.title,
|
|
8669
9935
|
currentModel: args.bundle.session.currentModel,
|
|
8670
9936
|
currentMode: args.bundle.session.currentMode,
|
|
@@ -8857,7 +10123,7 @@ function asString(value) {
|
|
|
8857
10123
|
}
|
|
8858
10124
|
async function loadPromptHistorySafely(sessionId) {
|
|
8859
10125
|
try {
|
|
8860
|
-
const raw = await
|
|
10126
|
+
const raw = await fs9.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
8861
10127
|
const out = [];
|
|
8862
10128
|
for (const line of raw.split("\n")) {
|
|
8863
10129
|
if (line.length === 0) {
|
|
@@ -8878,7 +10144,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
8878
10144
|
}
|
|
8879
10145
|
async function historyMtimeIso(sessionId) {
|
|
8880
10146
|
try {
|
|
8881
|
-
const st = await
|
|
10147
|
+
const st = await fs9.stat(paths.historyFile(sessionId));
|
|
8882
10148
|
return new Date(st.mtimeMs).toISOString();
|
|
8883
10149
|
} catch {
|
|
8884
10150
|
return void 0;
|
|
@@ -8887,10 +10153,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
8887
10153
|
|
|
8888
10154
|
// src/core/extensions.ts
|
|
8889
10155
|
init_paths();
|
|
8890
|
-
import { spawn as
|
|
8891
|
-
import * as
|
|
8892
|
-
import * as
|
|
8893
|
-
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";
|
|
8894
10160
|
var RESTART_BASE_MS = 1e3;
|
|
8895
10161
|
var RESTART_CAP_MS = 6e4;
|
|
8896
10162
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -8911,7 +10177,7 @@ var ExtensionManager = class {
|
|
|
8911
10177
|
if (!this.context) {
|
|
8912
10178
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
8913
10179
|
}
|
|
8914
|
-
await
|
|
10180
|
+
await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
|
|
8915
10181
|
await this.reapOrphans();
|
|
8916
10182
|
for (const entry of this.entries.values()) {
|
|
8917
10183
|
if (!entry.config.enabled) {
|
|
@@ -9120,7 +10386,7 @@ var ExtensionManager = class {
|
|
|
9120
10386
|
async reapOrphans() {
|
|
9121
10387
|
let entries;
|
|
9122
10388
|
try {
|
|
9123
|
-
entries = await
|
|
10389
|
+
entries = await fsp3.readdir(paths.extensionsDir());
|
|
9124
10390
|
} catch (err) {
|
|
9125
10391
|
const e = err;
|
|
9126
10392
|
if (e.code === "ENOENT") {
|
|
@@ -9132,10 +10398,10 @@ var ExtensionManager = class {
|
|
|
9132
10398
|
if (!entry.endsWith(".pid")) {
|
|
9133
10399
|
continue;
|
|
9134
10400
|
}
|
|
9135
|
-
const pidPath =
|
|
10401
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
9136
10402
|
let pid;
|
|
9137
10403
|
try {
|
|
9138
|
-
const raw = await
|
|
10404
|
+
const raw = await fsp3.readFile(pidPath, "utf8");
|
|
9139
10405
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
9140
10406
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
9141
10407
|
pid = parsed;
|
|
@@ -9158,7 +10424,7 @@ var ExtensionManager = class {
|
|
|
9158
10424
|
}
|
|
9159
10425
|
}
|
|
9160
10426
|
}
|
|
9161
|
-
await
|
|
10427
|
+
await fsp3.unlink(pidPath).catch(() => void 0);
|
|
9162
10428
|
}
|
|
9163
10429
|
}
|
|
9164
10430
|
spawn(entry, attempt) {
|
|
@@ -9171,7 +10437,7 @@ var ExtensionManager = class {
|
|
|
9171
10437
|
}
|
|
9172
10438
|
const ext = entry.config;
|
|
9173
10439
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
9174
|
-
const logStream =
|
|
10440
|
+
const logStream = fs10.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
9175
10441
|
flags: "a"
|
|
9176
10442
|
});
|
|
9177
10443
|
logStream.write(
|
|
@@ -9199,7 +10465,7 @@ var ExtensionManager = class {
|
|
|
9199
10465
|
const args = [...baseArgs, ...ext.args];
|
|
9200
10466
|
let child;
|
|
9201
10467
|
try {
|
|
9202
|
-
child =
|
|
10468
|
+
child = spawn4(cmd, args, {
|
|
9203
10469
|
env,
|
|
9204
10470
|
stdio: ["ignore", "pipe", "pipe"],
|
|
9205
10471
|
detached: false
|
|
@@ -9221,7 +10487,7 @@ var ExtensionManager = class {
|
|
|
9221
10487
|
}
|
|
9222
10488
|
if (typeof child.pid === "number") {
|
|
9223
10489
|
try {
|
|
9224
|
-
|
|
10490
|
+
fs10.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
9225
10491
|
`, {
|
|
9226
10492
|
encoding: "utf8",
|
|
9227
10493
|
mode: 384
|
|
@@ -9246,7 +10512,7 @@ var ExtensionManager = class {
|
|
|
9246
10512
|
});
|
|
9247
10513
|
child.on("exit", (code, signal) => {
|
|
9248
10514
|
try {
|
|
9249
|
-
|
|
10515
|
+
fs10.unlinkSync(paths.extensionPidFile(ext.name));
|
|
9250
10516
|
} catch {
|
|
9251
10517
|
}
|
|
9252
10518
|
logStream.write(
|
|
@@ -9304,6 +10570,7 @@ function withCode2(err, code) {
|
|
|
9304
10570
|
|
|
9305
10571
|
// src/daemon/server.ts
|
|
9306
10572
|
init_paths();
|
|
10573
|
+
init_hydra_version();
|
|
9307
10574
|
|
|
9308
10575
|
// src/daemon/auth.ts
|
|
9309
10576
|
var BEARER_PREFIX = "Bearer ";
|
|
@@ -9359,78 +10626,10 @@ function constantTimeEqual(a, b) {
|
|
|
9359
10626
|
|
|
9360
10627
|
// src/daemon/routes/sessions.ts
|
|
9361
10628
|
init_config();
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
// src/core/bundle.ts
|
|
9365
|
-
import { z as z5 } from "zod";
|
|
9366
|
-
var HistoryEntrySchema = z5.object({
|
|
9367
|
-
method: z5.string(),
|
|
9368
|
-
params: z5.unknown(),
|
|
9369
|
-
recordedAt: z5.number()
|
|
9370
|
-
});
|
|
9371
|
-
var BundleSession = z5.object({
|
|
9372
|
-
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
9373
|
-
// the local namespace; lineageId is what survives across hops).
|
|
9374
|
-
sessionId: z5.string(),
|
|
9375
|
-
// Required on bundles — the export path backfills if the source
|
|
9376
|
-
// record was written before lineageId existed.
|
|
9377
|
-
lineageId: z5.string(),
|
|
9378
|
-
agentId: z5.string(),
|
|
9379
|
-
cwd: z5.string(),
|
|
9380
|
-
title: z5.string().optional(),
|
|
9381
|
-
currentModel: z5.string().optional(),
|
|
9382
|
-
currentMode: z5.string().optional(),
|
|
9383
|
-
currentUsage: PersistedUsage.optional(),
|
|
9384
|
-
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
9385
|
-
createdAt: z5.string(),
|
|
9386
|
-
updatedAt: z5.string()
|
|
9387
|
-
});
|
|
9388
|
-
var Bundle = z5.object({
|
|
9389
|
-
version: z5.literal(1),
|
|
9390
|
-
exportedAt: z5.string(),
|
|
9391
|
-
exportedFrom: z5.object({
|
|
9392
|
-
hydraVersion: z5.string(),
|
|
9393
|
-
machine: z5.string()
|
|
9394
|
-
}),
|
|
9395
|
-
session: BundleSession,
|
|
9396
|
-
history: z5.array(HistoryEntrySchema),
|
|
9397
|
-
promptHistory: z5.array(z5.string()).optional()
|
|
9398
|
-
});
|
|
9399
|
-
function encodeBundle(params) {
|
|
9400
|
-
const bundle = {
|
|
9401
|
-
version: 1,
|
|
9402
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9403
|
-
exportedFrom: {
|
|
9404
|
-
hydraVersion: params.hydraVersion,
|
|
9405
|
-
machine: params.machine
|
|
9406
|
-
},
|
|
9407
|
-
session: {
|
|
9408
|
-
sessionId: params.record.sessionId,
|
|
9409
|
-
lineageId: params.record.lineageId,
|
|
9410
|
-
agentId: params.record.agentId,
|
|
9411
|
-
cwd: params.record.cwd,
|
|
9412
|
-
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
9413
|
-
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
9414
|
-
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
9415
|
-
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
9416
|
-
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
9417
|
-
createdAt: params.record.createdAt,
|
|
9418
|
-
updatedAt: params.record.updatedAt
|
|
9419
|
-
},
|
|
9420
|
-
history: params.history
|
|
9421
|
-
};
|
|
9422
|
-
if (params.promptHistory !== void 0) {
|
|
9423
|
-
bundle.promptHistory = params.promptHistory;
|
|
9424
|
-
}
|
|
9425
|
-
return bundle;
|
|
9426
|
-
}
|
|
9427
|
-
function decodeBundle(raw) {
|
|
9428
|
-
return Bundle.parse(raw);
|
|
9429
|
-
}
|
|
9430
|
-
|
|
9431
|
-
// src/daemon/routes/sessions.ts
|
|
10629
|
+
init_bundle();
|
|
9432
10630
|
init_types();
|
|
9433
|
-
|
|
10631
|
+
init_hydra_version();
|
|
10632
|
+
import * as os3 from "os";
|
|
9434
10633
|
function registerSessionRoutes(app, manager, defaults) {
|
|
9435
10634
|
app.get("/v1/sessions", async (request) => {
|
|
9436
10635
|
const query = request.query;
|
|
@@ -9501,7 +10700,7 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9501
10700
|
history: exported.history,
|
|
9502
10701
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
9503
10702
|
hydraVersion: HYDRA_VERSION,
|
|
9504
|
-
machine:
|
|
10703
|
+
machine: os3.hostname()
|
|
9505
10704
|
});
|
|
9506
10705
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
9507
10706
|
reply.header(
|
|
@@ -9516,6 +10715,14 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9516
10715
|
reply.code(400).send({ error: "missing bundle" });
|
|
9517
10716
|
return;
|
|
9518
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
|
+
}
|
|
9519
10726
|
let bundle;
|
|
9520
10727
|
try {
|
|
9521
10728
|
bundle = decodeBundle(body.bundle);
|
|
@@ -9528,7 +10735,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9528
10735
|
}
|
|
9529
10736
|
try {
|
|
9530
10737
|
const result = await manager.importBundle(bundle, {
|
|
9531
|
-
replace: body.replace === true
|
|
10738
|
+
replace: body.replace === true,
|
|
10739
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
9532
10740
|
});
|
|
9533
10741
|
reply.code(201).send(result);
|
|
9534
10742
|
} catch (err) {
|
|
@@ -9769,8 +10977,7 @@ init_connection();
|
|
|
9769
10977
|
init_ws_stream();
|
|
9770
10978
|
init_types();
|
|
9771
10979
|
import { nanoid as nanoid2 } from "nanoid";
|
|
9772
|
-
|
|
9773
|
-
var HYDRA_PROTOCOL_VERSION = 1;
|
|
10980
|
+
init_hydra_version();
|
|
9774
10981
|
function registerAcpWsEndpoint(app, deps) {
|
|
9775
10982
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
9776
10983
|
const token = tokenFromUpgradeRequest({
|
|
@@ -10033,8 +11240,8 @@ function buildResponseMeta(session) {
|
|
|
10033
11240
|
}
|
|
10034
11241
|
function buildInitializeResult() {
|
|
10035
11242
|
return {
|
|
10036
|
-
protocolVersion:
|
|
10037
|
-
agentInfo: { name: "hydra", version:
|
|
11243
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
11244
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION },
|
|
10038
11245
|
agentCapabilities: {
|
|
10039
11246
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
10040
11247
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -10073,14 +11280,13 @@ function bindClientToSession(connection, session, state, clientInfo) {
|
|
|
10073
11280
|
}
|
|
10074
11281
|
|
|
10075
11282
|
// src/daemon/server.ts
|
|
10076
|
-
var HYDRA_VERSION3 = "0.1.0";
|
|
10077
11283
|
async function startDaemon(config) {
|
|
10078
11284
|
ensureLoopbackOrTls(config);
|
|
10079
11285
|
const httpsOptions = config.daemon.tls ? {
|
|
10080
|
-
key: await
|
|
10081
|
-
cert: await
|
|
11286
|
+
key: await fsp4.readFile(config.daemon.tls.key),
|
|
11287
|
+
cert: await fsp4.readFile(config.daemon.tls.cert)
|
|
10082
11288
|
} : void 0;
|
|
10083
|
-
await
|
|
11289
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
10084
11290
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
10085
11291
|
config.daemon.logLevel
|
|
10086
11292
|
);
|
|
@@ -10089,12 +11295,18 @@ async function startDaemon(config) {
|
|
|
10089
11295
|
level: config.daemon.logLevel,
|
|
10090
11296
|
stream: logStream
|
|
10091
11297
|
},
|
|
10092
|
-
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
|
|
10093
11302
|
});
|
|
10094
11303
|
await app.register(websocketPlugin);
|
|
10095
11304
|
setBinaryInstallLogger((msg) => {
|
|
10096
11305
|
app.log.info(msg);
|
|
10097
11306
|
});
|
|
11307
|
+
setNpmInstallLogger((msg) => {
|
|
11308
|
+
app.log.info(msg);
|
|
11309
|
+
});
|
|
10098
11310
|
const auth = bearerAuth({ config });
|
|
10099
11311
|
app.addHook("onRequest", async (request, reply) => {
|
|
10100
11312
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -10106,12 +11318,14 @@ async function startDaemon(config) {
|
|
|
10106
11318
|
await auth(request, reply);
|
|
10107
11319
|
});
|
|
10108
11320
|
const registry = new Registry(config);
|
|
10109
|
-
const
|
|
11321
|
+
const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
|
|
11322
|
+
const manager = new SessionManager(registry, spawner, void 0, {
|
|
10110
11323
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
10111
|
-
defaultModels: config.defaultModels
|
|
11324
|
+
defaultModels: config.defaultModels,
|
|
11325
|
+
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
|
|
10112
11326
|
});
|
|
10113
11327
|
const extensions = new ExtensionManager(extensionList(config));
|
|
10114
|
-
registerHealthRoutes(app,
|
|
11328
|
+
registerHealthRoutes(app, HYDRA_VERSION);
|
|
10115
11329
|
registerSessionRoutes(app, manager, {
|
|
10116
11330
|
agentId: config.defaultAgent,
|
|
10117
11331
|
cwd: config.defaultCwd
|
|
@@ -10130,8 +11344,8 @@ async function startDaemon(config) {
|
|
|
10130
11344
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
10131
11345
|
const address = app.server.address();
|
|
10132
11346
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
10133
|
-
await
|
|
10134
|
-
await
|
|
11347
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
11348
|
+
await fsp4.writeFile(
|
|
10135
11349
|
paths.pidFile(),
|
|
10136
11350
|
JSON.stringify({
|
|
10137
11351
|
pid: process.pid,
|
|
@@ -10157,9 +11371,10 @@ async function startDaemon(config) {
|
|
|
10157
11371
|
await manager.closeAll();
|
|
10158
11372
|
await manager.flushMetaWrites();
|
|
10159
11373
|
setBinaryInstallLogger(null);
|
|
11374
|
+
setNpmInstallLogger(null);
|
|
10160
11375
|
await app.close();
|
|
10161
11376
|
try {
|
|
10162
|
-
|
|
11377
|
+
fs11.unlinkSync(paths.pidFile());
|
|
10163
11378
|
} catch {
|
|
10164
11379
|
}
|
|
10165
11380
|
try {
|
|
@@ -10198,13 +11413,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
10198
11413
|
init_daemon_bootstrap();
|
|
10199
11414
|
|
|
10200
11415
|
// src/cli/commands/log-tail.ts
|
|
10201
|
-
import * as
|
|
10202
|
-
import * as
|
|
11416
|
+
import * as fs12 from "fs";
|
|
11417
|
+
import * as fsp5 from "fs/promises";
|
|
10203
11418
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
10204
11419
|
const opts = parseLogTailFlags(argv);
|
|
10205
|
-
let
|
|
11420
|
+
let stat4;
|
|
10206
11421
|
try {
|
|
10207
|
-
|
|
11422
|
+
stat4 = await fsp5.stat(logPath);
|
|
10208
11423
|
} catch (err) {
|
|
10209
11424
|
const e = err;
|
|
10210
11425
|
if (e.code === "ENOENT") {
|
|
@@ -10215,14 +11430,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
10215
11430
|
}
|
|
10216
11431
|
throw err;
|
|
10217
11432
|
}
|
|
10218
|
-
let position = await printTail(logPath,
|
|
11433
|
+
let position = await printTail(logPath, stat4.size, opts.tail);
|
|
10219
11434
|
if (!opts.follow) {
|
|
10220
11435
|
return;
|
|
10221
11436
|
}
|
|
10222
11437
|
process.stdout.write(`-- following ${logPath} --
|
|
10223
11438
|
`);
|
|
10224
11439
|
let pending = false;
|
|
10225
|
-
const watcher =
|
|
11440
|
+
const watcher = fs12.watch(logPath, () => {
|
|
10226
11441
|
if (pending) {
|
|
10227
11442
|
return;
|
|
10228
11443
|
}
|
|
@@ -10230,14 +11445,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
10230
11445
|
setImmediate(async () => {
|
|
10231
11446
|
pending = false;
|
|
10232
11447
|
try {
|
|
10233
|
-
const s = await
|
|
11448
|
+
const s = await fsp5.stat(logPath);
|
|
10234
11449
|
if (s.size <= position) {
|
|
10235
11450
|
if (s.size < position) {
|
|
10236
11451
|
position = s.size;
|
|
10237
11452
|
}
|
|
10238
11453
|
return;
|
|
10239
11454
|
}
|
|
10240
|
-
const fd = await
|
|
11455
|
+
const fd = await fsp5.open(logPath, "r");
|
|
10241
11456
|
try {
|
|
10242
11457
|
const buf = Buffer.alloc(s.size - position);
|
|
10243
11458
|
await fd.read(buf, 0, buf.length, position);
|
|
@@ -10264,7 +11479,7 @@ async function printTail(logPath, fileSize, lines) {
|
|
|
10264
11479
|
return fileSize;
|
|
10265
11480
|
}
|
|
10266
11481
|
const CHUNK = 64 * 1024;
|
|
10267
|
-
const fd = await
|
|
11482
|
+
const fd = await fsp5.open(logPath, "r");
|
|
10268
11483
|
try {
|
|
10269
11484
|
let position = fileSize;
|
|
10270
11485
|
let collected = "";
|
|
@@ -10433,7 +11648,7 @@ async function runDaemonStatus() {
|
|
|
10433
11648
|
}
|
|
10434
11649
|
async function readPidFile() {
|
|
10435
11650
|
try {
|
|
10436
|
-
const raw = await
|
|
11651
|
+
const raw = await fsp6.readFile(paths.pidFile(), "utf8");
|
|
10437
11652
|
return JSON.parse(raw);
|
|
10438
11653
|
} catch (err) {
|
|
10439
11654
|
const e = err;
|
|
@@ -10458,7 +11673,7 @@ init_sessions();
|
|
|
10458
11673
|
// src/cli/commands/extensions.ts
|
|
10459
11674
|
init_config();
|
|
10460
11675
|
init_paths();
|
|
10461
|
-
import * as
|
|
11676
|
+
import * as fsp7 from "fs/promises";
|
|
10462
11677
|
init_sessions();
|
|
10463
11678
|
async function runExtensionsList() {
|
|
10464
11679
|
const config = await loadConfig();
|
|
@@ -10654,11 +11869,11 @@ async function runExtensionsRemove(name) {
|
|
|
10654
11869
|
}
|
|
10655
11870
|
}
|
|
10656
11871
|
async function readRawConfig() {
|
|
10657
|
-
const raw = await
|
|
11872
|
+
const raw = await fsp7.readFile(paths.config(), "utf8");
|
|
10658
11873
|
return JSON.parse(raw);
|
|
10659
11874
|
}
|
|
10660
11875
|
async function writeRawConfig(raw) {
|
|
10661
|
-
await
|
|
11876
|
+
await fsp7.writeFile(
|
|
10662
11877
|
paths.config(),
|
|
10663
11878
|
JSON.stringify(raw, null, 2) + "\n",
|
|
10664
11879
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -11423,8 +12638,8 @@ async function main() {
|
|
|
11423
12638
|
await runSessionsKill(positional[2]);
|
|
11424
12639
|
return;
|
|
11425
12640
|
}
|
|
11426
|
-
if (sub === "
|
|
11427
|
-
await
|
|
12641
|
+
if (sub === "remove") {
|
|
12642
|
+
await runSessionsRemove(positional[2]);
|
|
11428
12643
|
return;
|
|
11429
12644
|
}
|
|
11430
12645
|
if (sub === "export") {
|
|
@@ -11433,8 +12648,11 @@ async function main() {
|
|
|
11433
12648
|
return;
|
|
11434
12649
|
}
|
|
11435
12650
|
if (sub === "import") {
|
|
12651
|
+
const cwd = resolveOption(flags, "cwd");
|
|
11436
12652
|
await runSessionsImport(positional[2], {
|
|
11437
|
-
replace: flags.replace === true
|
|
12653
|
+
replace: flags.replace === true,
|
|
12654
|
+
info: flags.info === true,
|
|
12655
|
+
...cwd !== void 0 ? { cwd } : {}
|
|
11438
12656
|
});
|
|
11439
12657
|
return;
|
|
11440
12658
|
}
|
|
@@ -11537,9 +12755,9 @@ async function dispatchTui(flags, base) {
|
|
|
11537
12755
|
}
|
|
11538
12756
|
function readVersion() {
|
|
11539
12757
|
try {
|
|
11540
|
-
const here =
|
|
12758
|
+
const here = dirname6(fileURLToPath2(import.meta.url));
|
|
11541
12759
|
const pkg = JSON.parse(
|
|
11542
|
-
|
|
12760
|
+
readFileSync2(resolve4(here, "../package.json"), "utf8")
|
|
11543
12761
|
);
|
|
11544
12762
|
return pkg.version ?? "unknown";
|
|
11545
12763
|
} catch {
|
|
@@ -11566,11 +12784,11 @@ function printHelp() {
|
|
|
11566
12784
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
11567
12785
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
11568
12786
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
11569
|
-
" hydra-acp sessions
|
|
12787
|
+
" hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
|
|
11570
12788
|
" hydra-acp sessions export <id> [--out <file>|.]",
|
|
11571
12789
|
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
11572
|
-
" hydra-acp sessions import <file>|- [--replace]",
|
|
11573
|
-
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live)",
|
|
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",
|
|
11574
12792
|
" hydra-acp extensions list List configured extensions and live state",
|
|
11575
12793
|
" hydra-acp extensions add <name> [opts] Add an extension to config",
|
|
11576
12794
|
" hydra-acp extensions remove <name> Remove an extension from config",
|