@hydra-acp/cli 0.1.56 → 0.1.58
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 +40 -51
- package/dist/cli.js +730 -316
- package/dist/index.d.ts +51 -1
- package/dist/index.js +266 -77
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -108,7 +108,11 @@ var init_paths = __esm({
|
|
|
108
108
|
// line, append-only so concurrent TUIs don't lose each other's
|
|
109
109
|
// writes.
|
|
110
110
|
globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
|
|
111
|
-
tuiLogFile: () => path.join(hydraHome(), "tui.log")
|
|
111
|
+
tuiLogFile: () => path.join(hydraHome(), "tui.log"),
|
|
112
|
+
// Diagnostic dump of every JSON-RPC message that crosses a `hydra-acp
|
|
113
|
+
// shim` process. Append-only NDJSON. One file shared by every shim;
|
|
114
|
+
// each line carries the writing process's pid for disambiguation.
|
|
115
|
+
shimWireLogFile: () => path.join(hydraHome(), "shim-wire.log")
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
});
|
|
@@ -1025,6 +1029,12 @@ function extractHydraMeta(meta) {
|
|
|
1025
1029
|
if (typeof obj.mcpStdin === "boolean") {
|
|
1026
1030
|
out.mcpStdin = obj.mcpStdin;
|
|
1027
1031
|
}
|
|
1032
|
+
if (typeof obj.interactive === "boolean") {
|
|
1033
|
+
out.interactive = obj.interactive;
|
|
1034
|
+
}
|
|
1035
|
+
if (typeof obj.ancillary === "boolean") {
|
|
1036
|
+
out.ancillary = obj.ancillary;
|
|
1037
|
+
}
|
|
1028
1038
|
if (typeof obj.promptAmending === "boolean") {
|
|
1029
1039
|
out.promptAmending = obj.promptAmending;
|
|
1030
1040
|
}
|
|
@@ -1269,10 +1279,14 @@ var init_types = __esm({
|
|
|
1269
1279
|
// local session, an import is a cross-machine takeover.
|
|
1270
1280
|
forkedFromSessionId: z3.string().optional(),
|
|
1271
1281
|
forkedFromMessageId: z3.string().optional(),
|
|
1272
|
-
// clientInfo from the process that issued session/new.
|
|
1273
|
-
//
|
|
1274
|
-
// override flag surface them.
|
|
1282
|
+
// clientInfo from the process that issued session/new. Carried for
|
|
1283
|
+
// log/display; the effective filtering signal is `interactive` below.
|
|
1275
1284
|
originatingClient: z3.object({ name: z3.string(), version: z3.string().optional() }).optional(),
|
|
1285
|
+
// Tristate filter signal computed by effectiveInteractive(): explicit
|
|
1286
|
+
// when the record stored a value, else inferred (legacy cat hint or
|
|
1287
|
+
// history-presence). Clients can use this to render a hint glyph
|
|
1288
|
+
// (e.g. dim non-interactive rows when the user toggles them in).
|
|
1289
|
+
interactive: z3.boolean().optional(),
|
|
1276
1290
|
updatedAt: z3.string(),
|
|
1277
1291
|
attachedClients: z3.number().int().nonnegative(),
|
|
1278
1292
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -1295,7 +1309,10 @@ var init_types = __esm({
|
|
|
1295
1309
|
});
|
|
1296
1310
|
SessionPromptParams = z3.object({
|
|
1297
1311
|
sessionId: z3.string(),
|
|
1298
|
-
prompt: z3.array(z3.unknown())
|
|
1312
|
+
prompt: z3.array(z3.unknown()),
|
|
1313
|
+
// Hydra extensions ride under _meta["hydra-acp"] (e.g. `ancillary` to
|
|
1314
|
+
// mark a non-promoting turn). Kept so Session.prompt can read them.
|
|
1315
|
+
_meta: z3.record(z3.unknown()).optional()
|
|
1299
1316
|
});
|
|
1300
1317
|
SessionCancelParams = z3.object({
|
|
1301
1318
|
sessionId: z3.string()
|
|
@@ -2102,12 +2119,6 @@ var init_stream_buffer = __esm({
|
|
|
2102
2119
|
});
|
|
2103
2120
|
|
|
2104
2121
|
// src/core/hydra-commands.ts
|
|
2105
|
-
function hydraCommandsAsAdvertised() {
|
|
2106
|
-
return HYDRA_COMMANDS.map((c) => ({
|
|
2107
|
-
name: c.argsHint ? `${c.name} ${c.argsHint}` : c.name,
|
|
2108
|
-
description: c.description
|
|
2109
|
-
}));
|
|
2110
|
-
}
|
|
2111
2122
|
var HYDRA_COMMANDS, VERB_INDEX;
|
|
2112
2123
|
var init_hydra_commands = __esm({
|
|
2113
2124
|
"src/core/hydra-commands.ts"() {
|
|
@@ -2628,6 +2639,12 @@ var init_session = __esm({
|
|
|
2628
2639
|
forkedFromSessionId;
|
|
2629
2640
|
forkedFromMessageId;
|
|
2630
2641
|
originatingClient;
|
|
2642
|
+
// Tristate. Mutates from undefined → true on first prompt (or directly
|
|
2643
|
+
// false if init.interactive === false). Persisted via interactiveHandlers.
|
|
2644
|
+
_interactive;
|
|
2645
|
+
get interactive() {
|
|
2646
|
+
return this._interactive;
|
|
2647
|
+
}
|
|
2631
2648
|
title;
|
|
2632
2649
|
// Snapshot state delivered to attaching clients via the attach
|
|
2633
2650
|
// response _meta rather than via history replay (which would be
|
|
@@ -2756,6 +2773,7 @@ var init_session = __esm({
|
|
|
2756
2773
|
agentModelsHandlers = [];
|
|
2757
2774
|
modelHandlers = [];
|
|
2758
2775
|
modeHandlers = [];
|
|
2776
|
+
interactiveHandlers = [];
|
|
2759
2777
|
usageHandlers = [];
|
|
2760
2778
|
cumulativeCost = 0;
|
|
2761
2779
|
// Total cost across all agent lives. costAmount in the returned snapshot
|
|
@@ -2839,6 +2857,7 @@ var init_session = __esm({
|
|
|
2839
2857
|
if (init.firstPromptSeeded) {
|
|
2840
2858
|
this._firstPromptSeeded = true;
|
|
2841
2859
|
}
|
|
2860
|
+
this._interactive = init.interactive;
|
|
2842
2861
|
this.historyStore = init.historyStore;
|
|
2843
2862
|
this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
|
|
2844
2863
|
this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
|
|
@@ -3101,19 +3120,25 @@ var init_session = __esm({
|
|
|
3101
3120
|
return this.loadReplay(historyPolicy, opts);
|
|
3102
3121
|
}
|
|
3103
3122
|
async loadReplay(historyPolicy, opts) {
|
|
3104
|
-
const
|
|
3123
|
+
const raw = await this.getHistorySnapshot();
|
|
3105
3124
|
const state = this.buildStateSnapshotReplay();
|
|
3106
3125
|
if (historyPolicy === "after_message") {
|
|
3107
|
-
const cutoff = opts.afterMessageId ? findMessageIdIndex(
|
|
3126
|
+
const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
|
|
3108
3127
|
if (cutoff < 0) {
|
|
3109
|
-
return {
|
|
3128
|
+
return {
|
|
3129
|
+
entries: [...state, ...coalesceReplay(raw)],
|
|
3130
|
+
appliedPolicy: "full"
|
|
3131
|
+
};
|
|
3110
3132
|
}
|
|
3111
3133
|
return {
|
|
3112
|
-
entries: [...state, ...
|
|
3134
|
+
entries: [...state, ...coalesceReplay(raw.slice(cutoff + 1))],
|
|
3113
3135
|
appliedPolicy: "after_message"
|
|
3114
3136
|
};
|
|
3115
3137
|
}
|
|
3116
|
-
return {
|
|
3138
|
+
return {
|
|
3139
|
+
entries: [...state, ...coalesceReplay(raw)],
|
|
3140
|
+
appliedPolicy: "full"
|
|
3141
|
+
};
|
|
3117
3142
|
}
|
|
3118
3143
|
// Synthesizes one session/update notification per cached STATE_UPDATE_KIND
|
|
3119
3144
|
// so an attaching client receives the current snapshot through the
|
|
@@ -3294,6 +3319,18 @@ var init_session = __esm({
|
|
|
3294
3319
|
const messageId = generateMessageId();
|
|
3295
3320
|
this.maybeSeedTitleFromPrompt(params);
|
|
3296
3321
|
this._firstPromptSeeded = true;
|
|
3322
|
+
const ancillary = extractHydraMeta(
|
|
3323
|
+
(params ?? {})._meta
|
|
3324
|
+
).ancillary === true;
|
|
3325
|
+
if (!ancillary && this._interactive === void 0) {
|
|
3326
|
+
this._interactive = true;
|
|
3327
|
+
for (const handler of this.interactiveHandlers) {
|
|
3328
|
+
try {
|
|
3329
|
+
handler(true);
|
|
3330
|
+
} catch {
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3297
3334
|
return this.enqueueUserPrompt(client, params, messageId);
|
|
3298
3335
|
}
|
|
3299
3336
|
// DEVIATION FROM RFD #533: this broadcast is deliberately deferred
|
|
@@ -4076,13 +4113,7 @@ var init_session = __esm({
|
|
|
4076
4113
|
this.logger?.info(
|
|
4077
4114
|
`live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4078
4115
|
);
|
|
4079
|
-
this.
|
|
4080
|
-
for (const handler of this.modelHandlers) {
|
|
4081
|
-
try {
|
|
4082
|
-
handler(trimmed);
|
|
4083
|
-
} catch {
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4116
|
+
this.applyModelChange(trimmed);
|
|
4086
4117
|
}
|
|
4087
4118
|
}
|
|
4088
4119
|
break;
|
|
@@ -4278,24 +4309,38 @@ var init_session = __esm({
|
|
|
4278
4309
|
onModeChange(handler) {
|
|
4279
4310
|
this.modeHandlers.push(handler);
|
|
4280
4311
|
}
|
|
4312
|
+
onInteractiveChange(handler) {
|
|
4313
|
+
this.interactiveHandlers.push(handler);
|
|
4314
|
+
}
|
|
4281
4315
|
// Apply a model change initiated by a client request (session/set_model)
|
|
4282
4316
|
// when the agent doesn't emit a current_model_update notification, or
|
|
4283
4317
|
// emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
|
|
4284
4318
|
// (persistence) and broadcasts a synthetic current_model_update so all
|
|
4285
4319
|
// attached clients — including the originator — repaint immediately.
|
|
4320
|
+
//
|
|
4321
|
+
// The broadcast fires even when `modelId` already equals currentModel.
|
|
4322
|
+
// claude-acp emits a stale current_model_update (with the pre-change
|
|
4323
|
+
// value) during set_model processing and a separate config_option_update
|
|
4324
|
+
// with the new value; the configOption path updates currentModel here
|
|
4325
|
+
// before our synthetic broadcast would run, so a value-equality guard
|
|
4326
|
+
// would suppress the corrective broadcast and leave attached clients
|
|
4327
|
+
// (notably the TUI, which doesn't render config_option_update) showing
|
|
4328
|
+
// the stale model from the agent's earlier notification.
|
|
4286
4329
|
applyModelChange(modelId) {
|
|
4287
4330
|
const trimmed = modelId.trim();
|
|
4288
|
-
if (!trimmed
|
|
4331
|
+
if (!trimmed) {
|
|
4289
4332
|
return;
|
|
4290
4333
|
}
|
|
4291
|
-
this.
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4334
|
+
if (trimmed !== this.currentModel) {
|
|
4335
|
+
this.logger?.info(
|
|
4336
|
+
`applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4337
|
+
);
|
|
4338
|
+
this.currentModel = trimmed;
|
|
4339
|
+
for (const handler of this.modelHandlers) {
|
|
4340
|
+
try {
|
|
4341
|
+
handler(trimmed);
|
|
4342
|
+
} catch {
|
|
4343
|
+
}
|
|
4299
4344
|
}
|
|
4300
4345
|
}
|
|
4301
4346
|
const update = {
|
|
@@ -4312,23 +4357,39 @@ var init_session = __esm({
|
|
|
4312
4357
|
}
|
|
4313
4358
|
// Apply a mode change initiated by a client request (session/set_mode)
|
|
4314
4359
|
// when the agent doesn't emit a current_mode_update notification on its
|
|
4315
|
-
// own. Fires modeHandlers
|
|
4316
|
-
//
|
|
4360
|
+
// own. Fires modeHandlers (persistence) and broadcasts a synthetic
|
|
4361
|
+
// current_mode_update so all attached clients — including the originator
|
|
4362
|
+
// — repaint immediately, mirroring applyModelChange. Without the
|
|
4363
|
+
// broadcast, peer clients (e.g. the TUI when set_mode was issued by Zed
|
|
4364
|
+
// through the shim) would stay on the prior mode.
|
|
4317
4365
|
applyModeChange(modeId) {
|
|
4318
4366
|
const trimmed = modeId.trim();
|
|
4319
|
-
if (!trimmed
|
|
4367
|
+
if (!trimmed) {
|
|
4320
4368
|
return;
|
|
4321
4369
|
}
|
|
4322
|
-
this.
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4370
|
+
if (trimmed !== this.currentMode) {
|
|
4371
|
+
this.logger?.info(
|
|
4372
|
+
`applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4373
|
+
);
|
|
4374
|
+
this.currentMode = trimmed;
|
|
4375
|
+
for (const handler of this.modeHandlers) {
|
|
4376
|
+
try {
|
|
4377
|
+
handler(trimmed);
|
|
4378
|
+
} catch {
|
|
4379
|
+
}
|
|
4330
4380
|
}
|
|
4331
4381
|
}
|
|
4382
|
+
const update = {
|
|
4383
|
+
sessionUpdate: "current_mode_update",
|
|
4384
|
+
currentModeId: trimmed
|
|
4385
|
+
};
|
|
4386
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
4387
|
+
update.availableModes = [...this.agentAdvertisedModes];
|
|
4388
|
+
}
|
|
4389
|
+
this.recordAndBroadcast("session/update", {
|
|
4390
|
+
sessionId: this.upstreamSessionId,
|
|
4391
|
+
update
|
|
4392
|
+
});
|
|
4332
4393
|
}
|
|
4333
4394
|
onUsageChange(handler) {
|
|
4334
4395
|
this.usageHandlers.push(handler);
|
|
@@ -4340,8 +4401,8 @@ var init_session = __esm({
|
|
|
4340
4401
|
// entries, then whatever the agent advertised.
|
|
4341
4402
|
mergedAvailableCommands() {
|
|
4342
4403
|
const out = [
|
|
4343
|
-
|
|
4344
|
-
{ name: "model
|
|
4404
|
+
{ name: "hydra", description: "Hydra session command (kill, restart, title, agent <agent>)" },
|
|
4405
|
+
{ name: "model", description: "Switch model; omit arg to list available models" },
|
|
4345
4406
|
{ name: "sessions", description: "List all sessions" },
|
|
4346
4407
|
{ name: "help", description: "Show available commands" }
|
|
4347
4408
|
];
|
|
@@ -6248,6 +6309,9 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
|
|
|
6248
6309
|
if (opts.all) {
|
|
6249
6310
|
url.searchParams.set("all", "true");
|
|
6250
6311
|
}
|
|
6312
|
+
if (opts.includeNonInteractive) {
|
|
6313
|
+
url.searchParams.set("includeNonInteractive", "true");
|
|
6314
|
+
}
|
|
6251
6315
|
const response = await fetchImpl(url.toString(), {
|
|
6252
6316
|
headers: { Authorization: `Bearer ${target.token}` }
|
|
6253
6317
|
});
|
|
@@ -6274,7 +6338,8 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
|
|
|
6274
6338
|
forkedFromSessionId: s.forkedFromSessionId,
|
|
6275
6339
|
forkedFromMessageId: s.forkedFromMessageId,
|
|
6276
6340
|
busy: s.busy,
|
|
6277
|
-
originatingClient: s.originatingClient
|
|
6341
|
+
originatingClient: s.originatingClient,
|
|
6342
|
+
interactive: s.interactive
|
|
6278
6343
|
}));
|
|
6279
6344
|
}
|
|
6280
6345
|
async function forkSession(target, id, opts = {}, fetchImpl = fetch) {
|
|
@@ -6338,13 +6403,17 @@ async function regenSessionTitle(target, id, fetchImpl = fetch) {
|
|
|
6338
6403
|
}
|
|
6339
6404
|
}
|
|
6340
6405
|
async function searchSessions(target, query, opts = {}, fetchImpl = fetch) {
|
|
6341
|
-
const
|
|
6342
|
-
url.searchParams.set("q", query);
|
|
6406
|
+
const body = { q: query };
|
|
6343
6407
|
if (opts.sessionIds && opts.sessionIds.length > 0) {
|
|
6344
|
-
|
|
6408
|
+
body.sessionIds = opts.sessionIds;
|
|
6345
6409
|
}
|
|
6346
|
-
const response = await fetchImpl(
|
|
6347
|
-
|
|
6410
|
+
const response = await fetchImpl(`${target.baseUrl}/v1/sessions/search`, {
|
|
6411
|
+
method: "POST",
|
|
6412
|
+
headers: {
|
|
6413
|
+
Authorization: `Bearer ${target.token}`,
|
|
6414
|
+
"Content-Type": "application/json"
|
|
6415
|
+
},
|
|
6416
|
+
body: JSON.stringify(body)
|
|
6348
6417
|
});
|
|
6349
6418
|
if (!response.ok) {
|
|
6350
6419
|
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
@@ -7843,6 +7912,85 @@ var init_update_check = __esm({
|
|
|
7843
7912
|
}
|
|
7844
7913
|
});
|
|
7845
7914
|
|
|
7915
|
+
// src/core/cwd.ts
|
|
7916
|
+
import * as fs22 from "fs/promises";
|
|
7917
|
+
import * as path17 from "path";
|
|
7918
|
+
async function validateLocalCwd(input) {
|
|
7919
|
+
const trimmed = input.trim();
|
|
7920
|
+
if (trimmed.length === 0) {
|
|
7921
|
+
return { ok: false, reason: "path is empty" };
|
|
7922
|
+
}
|
|
7923
|
+
const resolved = path17.resolve(expandHome(trimmed));
|
|
7924
|
+
let stat5;
|
|
7925
|
+
try {
|
|
7926
|
+
stat5 = await fs22.stat(resolved);
|
|
7927
|
+
} catch {
|
|
7928
|
+
return { ok: false, reason: `${resolved} does not exist` };
|
|
7929
|
+
}
|
|
7930
|
+
if (!stat5.isDirectory()) {
|
|
7931
|
+
return { ok: false, reason: `${resolved} is not a directory` };
|
|
7932
|
+
}
|
|
7933
|
+
return { ok: true, path: resolved };
|
|
7934
|
+
}
|
|
7935
|
+
async function pickInitialLocalCwd(sessionCwd) {
|
|
7936
|
+
const candidates = [];
|
|
7937
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7938
|
+
const push = (p) => {
|
|
7939
|
+
if (!seen.has(p)) {
|
|
7940
|
+
seen.add(p);
|
|
7941
|
+
candidates.push(p);
|
|
7942
|
+
}
|
|
7943
|
+
};
|
|
7944
|
+
push(sessionCwd);
|
|
7945
|
+
if (sessionCwd.startsWith("/Users/")) {
|
|
7946
|
+
push("/home/" + sessionCwd.slice("/Users/".length));
|
|
7947
|
+
} else if (sessionCwd.startsWith("/home/")) {
|
|
7948
|
+
push("/Users/" + sessionCwd.slice("/home/".length));
|
|
7949
|
+
}
|
|
7950
|
+
for (const candidate of candidates) {
|
|
7951
|
+
try {
|
|
7952
|
+
const stat5 = await fs22.stat(candidate);
|
|
7953
|
+
if (stat5.isDirectory()) {
|
|
7954
|
+
return candidate;
|
|
7955
|
+
}
|
|
7956
|
+
} catch {
|
|
7957
|
+
}
|
|
7958
|
+
}
|
|
7959
|
+
return null;
|
|
7960
|
+
}
|
|
7961
|
+
async function completeLocalPath(input) {
|
|
7962
|
+
const lastSlash = input.lastIndexOf("/");
|
|
7963
|
+
let prefix;
|
|
7964
|
+
let basePrefix;
|
|
7965
|
+
let dirForRead;
|
|
7966
|
+
if (lastSlash === -1) {
|
|
7967
|
+
prefix = "";
|
|
7968
|
+
basePrefix = input;
|
|
7969
|
+
dirForRead = ".";
|
|
7970
|
+
} else {
|
|
7971
|
+
prefix = input.slice(0, lastSlash + 1);
|
|
7972
|
+
basePrefix = input.slice(lastSlash + 1);
|
|
7973
|
+
dirForRead = lastSlash === 0 ? "/" : prefix;
|
|
7974
|
+
}
|
|
7975
|
+
const resolvedDir = path17.resolve(expandHome(dirForRead));
|
|
7976
|
+
let entries;
|
|
7977
|
+
try {
|
|
7978
|
+
const list = await fs22.readdir(resolvedDir, { withFileTypes: true });
|
|
7979
|
+
entries = list.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
7980
|
+
} catch {
|
|
7981
|
+
return { prefix, basePrefix, matches: [] };
|
|
7982
|
+
}
|
|
7983
|
+
const showHidden = basePrefix.startsWith(".");
|
|
7984
|
+
const matches = entries.filter((e) => e.name.startsWith(basePrefix)).filter((e) => showHidden || !e.name.startsWith(".")).map((e) => e.isDir ? `${e.name}/` : e.name).sort();
|
|
7985
|
+
return { prefix, basePrefix, matches };
|
|
7986
|
+
}
|
|
7987
|
+
var init_cwd = __esm({
|
|
7988
|
+
"src/core/cwd.ts"() {
|
|
7989
|
+
"use strict";
|
|
7990
|
+
init_config();
|
|
7991
|
+
}
|
|
7992
|
+
});
|
|
7993
|
+
|
|
7846
7994
|
// src/tui/input.ts
|
|
7847
7995
|
function formatPasteToken(id, lineCount) {
|
|
7848
7996
|
return `[pasted #${id} +${lineCount} lines]`;
|
|
@@ -8104,12 +8252,18 @@ var init_input = __esm({
|
|
|
8104
8252
|
return [];
|
|
8105
8253
|
case "ctrl-c":
|
|
8106
8254
|
return this.handleCtrlC();
|
|
8107
|
-
case "ctrl-d":
|
|
8255
|
+
case "ctrl-d": {
|
|
8108
8256
|
if (this.bufferIsEmpty()) {
|
|
8109
8257
|
return [{ type: "exit" }];
|
|
8110
8258
|
}
|
|
8259
|
+
const lastLine = this.buffer[this.buffer.length - 1] ?? "";
|
|
8260
|
+
const atEndOfBuffer = this.row === this.buffer.length - 1 && this.col >= lastLine.length;
|
|
8261
|
+
if (atEndOfBuffer) {
|
|
8262
|
+
return [{ type: "exit" }];
|
|
8263
|
+
}
|
|
8111
8264
|
this.deleteForward();
|
|
8112
8265
|
return [];
|
|
8266
|
+
}
|
|
8113
8267
|
case "ctrl-l":
|
|
8114
8268
|
return [{ type: "redraw" }];
|
|
8115
8269
|
case "ctrl-p":
|
|
@@ -8787,9 +8941,9 @@ var init_input = __esm({
|
|
|
8787
8941
|
});
|
|
8788
8942
|
|
|
8789
8943
|
// src/tui/attachments.ts
|
|
8790
|
-
import
|
|
8944
|
+
import path18 from "path";
|
|
8791
8945
|
function mimeFromExtension(p) {
|
|
8792
|
-
return EXTENSION_TO_MIME[
|
|
8946
|
+
return EXTENSION_TO_MIME[path18.extname(p).toLowerCase()] ?? null;
|
|
8793
8947
|
}
|
|
8794
8948
|
function isSupportedImagePath(p) {
|
|
8795
8949
|
return mimeFromExtension(p) !== null;
|
|
@@ -10677,9 +10831,9 @@ uncaught: ${err.stack ?? err.message}
|
|
|
10677
10831
|
this.permissionPrompt = spec ? { ...spec } : null;
|
|
10678
10832
|
this.repaint();
|
|
10679
10833
|
}
|
|
10680
|
-
// Two-line confirmation modal that takes over the prompt area.
|
|
10681
|
-
//
|
|
10682
|
-
//
|
|
10834
|
+
// Two-line confirmation modal that takes over the prompt area. Pass
|
|
10835
|
+
// null to dismiss. Currently unused — kept as a generic primitive for
|
|
10836
|
+
// any future modal that needs a question + hint footer.
|
|
10683
10837
|
setConfirmPrompt(spec) {
|
|
10684
10838
|
this.confirmPrompt = spec ? { ...spec } : null;
|
|
10685
10839
|
this.repaint();
|
|
@@ -12280,7 +12434,11 @@ var init_import_action_prompt = __esm({
|
|
|
12280
12434
|
// src/tui/picker.ts
|
|
12281
12435
|
function createPickerPrefs() {
|
|
12282
12436
|
return {
|
|
12283
|
-
filters: {
|
|
12437
|
+
filters: {
|
|
12438
|
+
cwdOnly: false,
|
|
12439
|
+
hostFilter: "__local",
|
|
12440
|
+
includeNonInteractive: false
|
|
12441
|
+
}
|
|
12284
12442
|
};
|
|
12285
12443
|
}
|
|
12286
12444
|
async function pickSession(term, opts) {
|
|
@@ -12308,10 +12466,8 @@ async function pickSession(term, opts) {
|
|
|
12308
12466
|
if (prefs.filters.cwdOnly) {
|
|
12309
12467
|
base = base.filter((s) => s.cwd === opts.cwd);
|
|
12310
12468
|
}
|
|
12311
|
-
if (!prefs.filters.
|
|
12312
|
-
base = base.filter(
|
|
12313
|
-
(s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
|
|
12314
|
-
);
|
|
12469
|
+
if (!prefs.filters.includeNonInteractive) {
|
|
12470
|
+
base = base.filter((s) => s.interactive === true);
|
|
12315
12471
|
}
|
|
12316
12472
|
base = filterByHost(base, prefs.filters.hostFilter);
|
|
12317
12473
|
return base;
|
|
@@ -12513,8 +12669,8 @@ async function pickSession(term, opts) {
|
|
|
12513
12669
|
prefs.filters.hostFilter === "__local" ? "host: local" : `host: ${prefs.filters.hostFilter}`
|
|
12514
12670
|
);
|
|
12515
12671
|
}
|
|
12516
|
-
if (prefs.filters.
|
|
12517
|
-
parts.push("+
|
|
12672
|
+
if (prefs.filters.includeNonInteractive) {
|
|
12673
|
+
parts.push("+non-interactive");
|
|
12518
12674
|
}
|
|
12519
12675
|
if (above > 0) {
|
|
12520
12676
|
parts.push(`\u2191 ${above} above`);
|
|
@@ -13155,7 +13311,9 @@ ${cells}`;
|
|
|
13155
13311
|
try {
|
|
13156
13312
|
const beforeKey = refreshOpts.silent ? renderFingerprint() : "";
|
|
13157
13313
|
const beforeTotal = total;
|
|
13158
|
-
const next = await listSessions(opts.target
|
|
13314
|
+
const next = await listSessions(opts.target, {
|
|
13315
|
+
includeNonInteractive: true
|
|
13316
|
+
});
|
|
13159
13317
|
const followId = preferredId ?? (selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0);
|
|
13160
13318
|
allSessions = sortSessions(next, opts.cwd);
|
|
13161
13319
|
applyFilter();
|
|
@@ -13633,7 +13791,7 @@ ${cells}`;
|
|
|
13633
13791
|
const effects = composer.feed(event);
|
|
13634
13792
|
const after = composer.state();
|
|
13635
13793
|
const unchanged = before.buffer.length === after.buffer.length && before.buffer.every((line, i) => line === after.buffer[i]) && before.row === after.row && before.col === after.col;
|
|
13636
|
-
if (effects.some((e) => e.type === "exit")
|
|
13794
|
+
if (effects.some((e) => e.type === "exit")) {
|
|
13637
13795
|
cleanup();
|
|
13638
13796
|
resolve8({ kind: "abort" });
|
|
13639
13797
|
return;
|
|
@@ -13733,7 +13891,7 @@ ${cells}`;
|
|
|
13733
13891
|
}
|
|
13734
13892
|
if (name === "i" || name === "I") {
|
|
13735
13893
|
const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
13736
|
-
prefs.filters.
|
|
13894
|
+
prefs.filters.includeNonInteractive = !prefs.filters.includeNonInteractive;
|
|
13737
13895
|
applyFilter();
|
|
13738
13896
|
restoreCursorAfterFilter(keepId);
|
|
13739
13897
|
renderFromScratch();
|
|
@@ -13999,7 +14157,6 @@ var init_picker = __esm({
|
|
|
13999
14157
|
init_session_row();
|
|
14000
14158
|
init_paths();
|
|
14001
14159
|
init_session();
|
|
14002
|
-
init_hydra_version();
|
|
14003
14160
|
init_discovery();
|
|
14004
14161
|
init_history();
|
|
14005
14162
|
init_input();
|
|
@@ -14039,85 +14196,6 @@ var init_picker = __esm({
|
|
|
14039
14196
|
}
|
|
14040
14197
|
});
|
|
14041
14198
|
|
|
14042
|
-
// src/core/cwd.ts
|
|
14043
|
-
import * as fs21 from "fs/promises";
|
|
14044
|
-
import * as path18 from "path";
|
|
14045
|
-
async function validateLocalCwd(input) {
|
|
14046
|
-
const trimmed = input.trim();
|
|
14047
|
-
if (trimmed.length === 0) {
|
|
14048
|
-
return { ok: false, reason: "path is empty" };
|
|
14049
|
-
}
|
|
14050
|
-
const resolved = path18.resolve(expandHome(trimmed));
|
|
14051
|
-
let stat5;
|
|
14052
|
-
try {
|
|
14053
|
-
stat5 = await fs21.stat(resolved);
|
|
14054
|
-
} catch {
|
|
14055
|
-
return { ok: false, reason: `${resolved} does not exist` };
|
|
14056
|
-
}
|
|
14057
|
-
if (!stat5.isDirectory()) {
|
|
14058
|
-
return { ok: false, reason: `${resolved} is not a directory` };
|
|
14059
|
-
}
|
|
14060
|
-
return { ok: true, path: resolved };
|
|
14061
|
-
}
|
|
14062
|
-
async function pickInitialLocalCwd(sessionCwd) {
|
|
14063
|
-
const candidates = [];
|
|
14064
|
-
const seen = /* @__PURE__ */ new Set();
|
|
14065
|
-
const push = (p) => {
|
|
14066
|
-
if (!seen.has(p)) {
|
|
14067
|
-
seen.add(p);
|
|
14068
|
-
candidates.push(p);
|
|
14069
|
-
}
|
|
14070
|
-
};
|
|
14071
|
-
push(sessionCwd);
|
|
14072
|
-
if (sessionCwd.startsWith("/Users/")) {
|
|
14073
|
-
push("/home/" + sessionCwd.slice("/Users/".length));
|
|
14074
|
-
} else if (sessionCwd.startsWith("/home/")) {
|
|
14075
|
-
push("/Users/" + sessionCwd.slice("/home/".length));
|
|
14076
|
-
}
|
|
14077
|
-
for (const candidate of candidates) {
|
|
14078
|
-
try {
|
|
14079
|
-
const stat5 = await fs21.stat(candidate);
|
|
14080
|
-
if (stat5.isDirectory()) {
|
|
14081
|
-
return candidate;
|
|
14082
|
-
}
|
|
14083
|
-
} catch {
|
|
14084
|
-
}
|
|
14085
|
-
}
|
|
14086
|
-
return null;
|
|
14087
|
-
}
|
|
14088
|
-
async function completeLocalPath(input) {
|
|
14089
|
-
const lastSlash = input.lastIndexOf("/");
|
|
14090
|
-
let prefix;
|
|
14091
|
-
let basePrefix;
|
|
14092
|
-
let dirForRead;
|
|
14093
|
-
if (lastSlash === -1) {
|
|
14094
|
-
prefix = "";
|
|
14095
|
-
basePrefix = input;
|
|
14096
|
-
dirForRead = ".";
|
|
14097
|
-
} else {
|
|
14098
|
-
prefix = input.slice(0, lastSlash + 1);
|
|
14099
|
-
basePrefix = input.slice(lastSlash + 1);
|
|
14100
|
-
dirForRead = lastSlash === 0 ? "/" : prefix;
|
|
14101
|
-
}
|
|
14102
|
-
const resolvedDir = path18.resolve(expandHome(dirForRead));
|
|
14103
|
-
let entries;
|
|
14104
|
-
try {
|
|
14105
|
-
const list = await fs21.readdir(resolvedDir, { withFileTypes: true });
|
|
14106
|
-
entries = list.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
14107
|
-
} catch {
|
|
14108
|
-
return { prefix, basePrefix, matches: [] };
|
|
14109
|
-
}
|
|
14110
|
-
const showHidden = basePrefix.startsWith(".");
|
|
14111
|
-
const matches = entries.filter((e) => e.name.startsWith(basePrefix)).filter((e) => showHidden || !e.name.startsWith(".")).map((e) => e.isDir ? `${e.name}/` : e.name).sort();
|
|
14112
|
-
return { prefix, basePrefix, matches };
|
|
14113
|
-
}
|
|
14114
|
-
var init_cwd = __esm({
|
|
14115
|
-
"src/core/cwd.ts"() {
|
|
14116
|
-
"use strict";
|
|
14117
|
-
init_config();
|
|
14118
|
-
}
|
|
14119
|
-
});
|
|
14120
|
-
|
|
14121
14199
|
// src/tui/completion.ts
|
|
14122
14200
|
function longestCommonPrefix(names) {
|
|
14123
14201
|
if (names.length === 0) {
|
|
@@ -14168,24 +14246,25 @@ async function promptForImportCwd(term, session, opts = {}) {
|
|
|
14168
14246
|
const defaultCwd = opts.defaultCwd ?? await pickInitialLocalCwd(session.cwd) ?? os6.homedir();
|
|
14169
14247
|
resetTerminalModes();
|
|
14170
14248
|
const shortId2 = stripHydraSessionPrefix(session.sessionId);
|
|
14171
|
-
const fromMachine = session.importedFromMachine ?? "another machine";
|
|
14172
14249
|
const originalCwd = shortenHomePath(session.cwd);
|
|
14250
|
+
const title = opts.title ?? "Fork locally \u2014 choose cwd";
|
|
14251
|
+
const intro = opts.intro ?? "Pick a local cwd for this session:";
|
|
14252
|
+
const headerRows = [
|
|
14253
|
+
{ label: "session: ", value: shortId2 },
|
|
14254
|
+
...session.importedFromMachine ? [{ label: "from: ", value: session.importedFromMachine }] : [],
|
|
14255
|
+
{ label: "cwd: ", value: originalCwd }
|
|
14256
|
+
];
|
|
14173
14257
|
let buffer = defaultCwd;
|
|
14174
14258
|
let errorLine = null;
|
|
14175
14259
|
let busy = false;
|
|
14176
14260
|
let layout = null;
|
|
14177
14261
|
const render = () => {
|
|
14178
|
-
const contentHeight =
|
|
14262
|
+
const contentHeight = headerRows.length + 6;
|
|
14179
14263
|
layout = drawBox(term, {
|
|
14180
14264
|
contentHeight,
|
|
14181
|
-
title
|
|
14265
|
+
title
|
|
14182
14266
|
});
|
|
14183
14267
|
const innerW = layout.contentW;
|
|
14184
|
-
const headerRows = [
|
|
14185
|
-
{ label: "session: ", value: shortId2 },
|
|
14186
|
-
{ label: "from: ", value: fromMachine },
|
|
14187
|
-
{ label: "cwd: ", value: originalCwd }
|
|
14188
|
-
];
|
|
14189
14268
|
let row = 0;
|
|
14190
14269
|
for (const hr of headerRows) {
|
|
14191
14270
|
term.moveTo(layout.contentX, layout.contentY + row);
|
|
@@ -14195,7 +14274,7 @@ async function promptForImportCwd(term, session, opts = {}) {
|
|
|
14195
14274
|
}
|
|
14196
14275
|
row++;
|
|
14197
14276
|
term.moveTo(layout.contentX, layout.contentY + row);
|
|
14198
|
-
term.noFormat(
|
|
14277
|
+
term.noFormat(` ${intro}`);
|
|
14199
14278
|
row += 2;
|
|
14200
14279
|
paintInputRow(row);
|
|
14201
14280
|
row += 2;
|
|
@@ -14209,7 +14288,7 @@ async function promptForImportCwd(term, session, opts = {}) {
|
|
|
14209
14288
|
);
|
|
14210
14289
|
}
|
|
14211
14290
|
};
|
|
14212
|
-
const inputRow = () =>
|
|
14291
|
+
const inputRow = () => headerRows.length + 3;
|
|
14213
14292
|
const paintInputRow = (rowOffset) => {
|
|
14214
14293
|
if (!layout) {
|
|
14215
14294
|
return;
|
|
@@ -14390,7 +14469,7 @@ var init_import_cwd_prompt = __esm({
|
|
|
14390
14469
|
|
|
14391
14470
|
// src/tui/clipboard.ts
|
|
14392
14471
|
import { spawn as nodeSpawn } from "child_process";
|
|
14393
|
-
import
|
|
14472
|
+
import fs23 from "fs/promises";
|
|
14394
14473
|
import os7 from "os";
|
|
14395
14474
|
import path19 from "path";
|
|
14396
14475
|
async function readClipboard(envIn = {}) {
|
|
@@ -14431,7 +14510,7 @@ async function readMacOS(env) {
|
|
|
14431
14510
|
return img;
|
|
14432
14511
|
}
|
|
14433
14512
|
} catch {
|
|
14434
|
-
await
|
|
14513
|
+
await fs23.unlink(tmpPath).catch(() => void 0);
|
|
14435
14514
|
}
|
|
14436
14515
|
try {
|
|
14437
14516
|
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
@@ -14546,9 +14625,9 @@ async function which(env, cmd) {
|
|
|
14546
14625
|
}
|
|
14547
14626
|
async function readFileAsAttachment(p, unlinkAfter) {
|
|
14548
14627
|
try {
|
|
14549
|
-
const buf = await
|
|
14628
|
+
const buf = await fs23.readFile(p);
|
|
14550
14629
|
if (unlinkAfter) {
|
|
14551
|
-
await
|
|
14630
|
+
await fs23.unlink(p).catch(() => void 0);
|
|
14552
14631
|
}
|
|
14553
14632
|
if (buf.length === 0) {
|
|
14554
14633
|
return { ok: false, reason: "no image on clipboard" };
|
|
@@ -14673,7 +14752,7 @@ function parseReattachResponse(result) {
|
|
|
14673
14752
|
return out;
|
|
14674
14753
|
}
|
|
14675
14754
|
function shouldDriftSnap(args) {
|
|
14676
|
-
return !args.replayDraining && args.pendingTurns > 0 && args.queueSize === 0 && !args.ownTurnInFlight && !args.hasInFlightHead;
|
|
14755
|
+
return !args.replayDraining && !args.amended && args.pendingTurns > 0 && args.queueSize === 0 && !args.ownTurnInFlight && !args.hasInFlightHead;
|
|
14677
14756
|
}
|
|
14678
14757
|
function computeAttachReconcile(args) {
|
|
14679
14758
|
if (args.daemonTurnStartedAt !== void 0) {
|
|
@@ -14694,10 +14773,10 @@ var init_reconnect_state = __esm({
|
|
|
14694
14773
|
});
|
|
14695
14774
|
|
|
14696
14775
|
// src/tui/app.ts
|
|
14697
|
-
import { appendFileSync, statSync, renameSync } from "fs";
|
|
14776
|
+
import { appendFileSync, statSync as statSync2, renameSync as renameSync2 } from "fs";
|
|
14698
14777
|
import { nanoid as nanoid3 } from "nanoid";
|
|
14699
14778
|
import termkit from "terminal-kit";
|
|
14700
|
-
import
|
|
14779
|
+
import fs24 from "fs/promises";
|
|
14701
14780
|
import path20 from "path";
|
|
14702
14781
|
function isReadonlyForbiddenEffect(effect) {
|
|
14703
14782
|
switch (effect.type) {
|
|
@@ -14835,6 +14914,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
14835
14914
|
}
|
|
14836
14915
|
};
|
|
14837
14916
|
let pendingTurns = 0;
|
|
14917
|
+
let cancelling = false;
|
|
14838
14918
|
let currentHeadMessageId;
|
|
14839
14919
|
let sessionBusySince = null;
|
|
14840
14920
|
let sessionElapsedTimer = null;
|
|
@@ -14845,6 +14925,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
14845
14925
|
pendingTurns = Math.max(0, pendingTurns + delta);
|
|
14846
14926
|
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
14847
14927
|
if (before === 0 && pendingTurns > 0) {
|
|
14928
|
+
cancelling = false;
|
|
14848
14929
|
sessionBusySince = Date.now();
|
|
14849
14930
|
lastUpdateAt = Date.now();
|
|
14850
14931
|
dispatcherRef?.setTurnRunning(true);
|
|
@@ -14865,6 +14946,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
14865
14946
|
}, 1e3);
|
|
14866
14947
|
}
|
|
14867
14948
|
} else if (before > 0 && pendingTurns === 0) {
|
|
14949
|
+
cancelling = false;
|
|
14868
14950
|
sessionBusySince = null;
|
|
14869
14951
|
lastUpdateAt = null;
|
|
14870
14952
|
dispatcherRef?.setTurnRunning(false);
|
|
@@ -14879,6 +14961,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
14879
14961
|
stalled: false
|
|
14880
14962
|
});
|
|
14881
14963
|
}
|
|
14964
|
+
} else if (pendingTurns > 0 && cancelling) {
|
|
14965
|
+
cancelling = false;
|
|
14966
|
+
if (screenReady) {
|
|
14967
|
+
screenRef.setBanner({ status: "busy", stalled: false });
|
|
14968
|
+
}
|
|
14882
14969
|
}
|
|
14883
14970
|
void delta;
|
|
14884
14971
|
};
|
|
@@ -15237,18 +15324,19 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15237
15324
|
historyPolicy: "full",
|
|
15238
15325
|
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
|
|
15239
15326
|
...opts.readonly === true ? { readonly: true } : {},
|
|
15240
|
-
// Forward the user-chosen cwd
|
|
15241
|
-
//
|
|
15242
|
-
//
|
|
15243
|
-
//
|
|
15244
|
-
//
|
|
15245
|
-
|
|
15327
|
+
// Forward the user-chosen cwd via a full resume hint. An empty
|
|
15328
|
+
// upstreamSessionId routes through doResurrectFromImport
|
|
15329
|
+
// (first-launch imports); a real one takes the normal session/load
|
|
15330
|
+
// path (repairing a local session whose recorded cwd is gone).
|
|
15331
|
+
// Either way the daemon resurrects with this cwd instead of the
|
|
15332
|
+
// stale recorded one.
|
|
15333
|
+
...ctx.resumeHint !== void 0 ? {
|
|
15246
15334
|
_meta: {
|
|
15247
15335
|
[HYDRA_META_KEY]: {
|
|
15248
15336
|
resume: {
|
|
15249
|
-
upstreamSessionId:
|
|
15250
|
-
agentId: ctx.
|
|
15251
|
-
cwd: ctx.
|
|
15337
|
+
upstreamSessionId: ctx.resumeHint.upstreamSessionId,
|
|
15338
|
+
agentId: ctx.resumeHint.agentId,
|
|
15339
|
+
cwd: ctx.resumeHint.cwd
|
|
15252
15340
|
}
|
|
15253
15341
|
}
|
|
15254
15342
|
}
|
|
@@ -15335,9 +15423,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15335
15423
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
15336
15424
|
continue;
|
|
15337
15425
|
}
|
|
15338
|
-
if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
|
|
15339
|
-
continue;
|
|
15340
|
-
}
|
|
15341
15426
|
if (tryHandleHelpKey(ev)) {
|
|
15342
15427
|
continue;
|
|
15343
15428
|
}
|
|
@@ -15551,86 +15636,35 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15551
15636
|
const cancelRemoteTurn = () => {
|
|
15552
15637
|
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
15553
15638
|
};
|
|
15554
|
-
const
|
|
15555
|
-
if (
|
|
15556
|
-
turnInFlight.cancel();
|
|
15639
|
+
const markCancelling = () => {
|
|
15640
|
+
if (screenRef === null) {
|
|
15557
15641
|
return;
|
|
15558
15642
|
}
|
|
15559
|
-
if (pendingTurns
|
|
15560
|
-
cancelRemoteTurn();
|
|
15643
|
+
if (pendingTurns !== 1) {
|
|
15561
15644
|
return;
|
|
15562
15645
|
}
|
|
15563
|
-
|
|
15646
|
+
cancelling = true;
|
|
15647
|
+
screenRef.setBanner({
|
|
15648
|
+
status: "cancelling",
|
|
15649
|
+
elapsedMs: void 0,
|
|
15650
|
+
stalled: false
|
|
15651
|
+
});
|
|
15564
15652
|
};
|
|
15565
|
-
|
|
15566
|
-
|
|
15567
|
-
|
|
15568
|
-
|
|
15569
|
-
return;
|
|
15570
|
-
}
|
|
15571
|
-
if (pendingTurns === 0) {
|
|
15572
|
-
stop(0);
|
|
15573
|
-
return;
|
|
15574
|
-
}
|
|
15575
|
-
let onlyClient = false;
|
|
15576
|
-
try {
|
|
15577
|
-
const sessions = await listSessions(target);
|
|
15578
|
-
const me = sessions.find((s) => s.sessionId === resolvedSessionId);
|
|
15579
|
-
onlyClient = !me || me.attachedClients <= 1;
|
|
15580
|
-
} catch {
|
|
15581
|
-
stop(0);
|
|
15653
|
+
const sigintHandler = () => {
|
|
15654
|
+
if (turnInFlight) {
|
|
15655
|
+
turnInFlight.cancel();
|
|
15656
|
+
markCancelling();
|
|
15582
15657
|
return;
|
|
15583
15658
|
}
|
|
15584
|
-
if (
|
|
15585
|
-
|
|
15659
|
+
if (pendingTurns > 0) {
|
|
15660
|
+
cancelRemoteTurn();
|
|
15661
|
+
markCancelling();
|
|
15586
15662
|
return;
|
|
15587
15663
|
}
|
|
15588
|
-
|
|
15589
|
-
screen.setConfirmPrompt({
|
|
15590
|
-
question: "Agent is still working. Interrupt it before exit?",
|
|
15591
|
-
hint: "y interrupt then exit \xB7 n / Enter detach silently \xB7 Esc cancel"
|
|
15592
|
-
});
|
|
15664
|
+
requestExit();
|
|
15593
15665
|
};
|
|
15594
|
-
const
|
|
15595
|
-
|
|
15596
|
-
screen.setConfirmPrompt(null);
|
|
15597
|
-
};
|
|
15598
|
-
const tryHandleExitConfirmKey = (ev) => {
|
|
15599
|
-
if (!exitConfirmation) {
|
|
15600
|
-
return false;
|
|
15601
|
-
}
|
|
15602
|
-
if (ev.type === "char") {
|
|
15603
|
-
const ch = ev.ch.toLowerCase();
|
|
15604
|
-
if (ch === "y") {
|
|
15605
|
-
dismissExitConfirmation();
|
|
15606
|
-
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
15607
|
-
stop(0);
|
|
15608
|
-
return true;
|
|
15609
|
-
}
|
|
15610
|
-
if (ch === "n") {
|
|
15611
|
-
dismissExitConfirmation();
|
|
15612
|
-
stop(0);
|
|
15613
|
-
return true;
|
|
15614
|
-
}
|
|
15615
|
-
return true;
|
|
15616
|
-
}
|
|
15617
|
-
if (ev.type === "key") {
|
|
15618
|
-
if (ev.name === "enter") {
|
|
15619
|
-
dismissExitConfirmation();
|
|
15620
|
-
stop(0);
|
|
15621
|
-
return true;
|
|
15622
|
-
}
|
|
15623
|
-
if (ev.name === "escape") {
|
|
15624
|
-
dismissExitConfirmation();
|
|
15625
|
-
return true;
|
|
15626
|
-
}
|
|
15627
|
-
if (ev.name === "ctrl-c" || ev.name === "ctrl-d") {
|
|
15628
|
-
dismissExitConfirmation();
|
|
15629
|
-
stop(0);
|
|
15630
|
-
return true;
|
|
15631
|
-
}
|
|
15632
|
-
}
|
|
15633
|
-
return true;
|
|
15666
|
+
const requestExit = () => {
|
|
15667
|
+
stop(0);
|
|
15634
15668
|
};
|
|
15635
15669
|
const buildHelpEntries = () => {
|
|
15636
15670
|
const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
|
|
@@ -15702,7 +15736,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15702
15736
|
let resolvedChoice = null;
|
|
15703
15737
|
let attachOverrides = null;
|
|
15704
15738
|
while (resolvedChoice === null) {
|
|
15705
|
-
const sessions = await listSessions(target);
|
|
15739
|
+
const sessions = await listSessions(target, { includeNonInteractive: true });
|
|
15706
15740
|
const choice2 = await pickSession(term, {
|
|
15707
15741
|
cwd: resolvedCwd,
|
|
15708
15742
|
sessions,
|
|
@@ -15740,14 +15774,43 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15740
15774
|
readonly: false,
|
|
15741
15775
|
cwd: decided2.ctx.cwd
|
|
15742
15776
|
};
|
|
15743
|
-
if (decided2.ctx.
|
|
15744
|
-
attachOverrides.
|
|
15777
|
+
if (decided2.ctx.resumeHint !== void 0) {
|
|
15778
|
+
attachOverrides.resumeHint = decided2.ctx.resumeHint;
|
|
15745
15779
|
}
|
|
15746
15780
|
break;
|
|
15747
15781
|
}
|
|
15748
15782
|
const chosen = sessions.find((s) => s.sessionId === choice2.sessionId);
|
|
15749
15783
|
const isImportedFirstLaunch = chosen !== void 0 && !!chosen.importedFromMachine && !chosen.upstreamSessionId && choice2.readonly !== true;
|
|
15750
15784
|
if (!isImportedFirstLaunch) {
|
|
15785
|
+
if (target.isLocal && chosen && !chosen.importedFromMachine && choice2.readonly !== true) {
|
|
15786
|
+
const v = await validateLocalCwd(chosen.cwd);
|
|
15787
|
+
if (!v.ok) {
|
|
15788
|
+
const r = await promptForImportCwd(term, chosen, {
|
|
15789
|
+
defaultCwd: expandHome(config.defaultCwd),
|
|
15790
|
+
title: "Working directory missing \u2014 choose cwd",
|
|
15791
|
+
intro: "This session's working directory no longer exists. Pick a new one:"
|
|
15792
|
+
});
|
|
15793
|
+
if (r.kind === "cancel") {
|
|
15794
|
+
screen.start({ skipFullscreen: true });
|
|
15795
|
+
screen.resumeRepaint();
|
|
15796
|
+
return;
|
|
15797
|
+
}
|
|
15798
|
+
if (r.kind === "back") {
|
|
15799
|
+
continue;
|
|
15800
|
+
}
|
|
15801
|
+
resolvedChoice = { choice: choice2, sessions };
|
|
15802
|
+
attachOverrides = {
|
|
15803
|
+
readonly: false,
|
|
15804
|
+
cwd: r.path,
|
|
15805
|
+
resumeHint: {
|
|
15806
|
+
agentId: choice2.agentId ?? chosen.agentId ?? "",
|
|
15807
|
+
cwd: r.path,
|
|
15808
|
+
upstreamSessionId: ""
|
|
15809
|
+
}
|
|
15810
|
+
};
|
|
15811
|
+
break;
|
|
15812
|
+
}
|
|
15813
|
+
}
|
|
15751
15814
|
resolvedChoice = { choice: choice2, sessions };
|
|
15752
15815
|
break;
|
|
15753
15816
|
}
|
|
@@ -15766,8 +15829,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15766
15829
|
readonly: opsShim.readonly === true,
|
|
15767
15830
|
cwd: decided.ctx.cwd
|
|
15768
15831
|
};
|
|
15769
|
-
if (decided.ctx.
|
|
15770
|
-
attachOverrides.
|
|
15832
|
+
if (decided.ctx.resumeHint !== void 0) {
|
|
15833
|
+
attachOverrides.resumeHint = decided.ctx.resumeHint;
|
|
15771
15834
|
}
|
|
15772
15835
|
}
|
|
15773
15836
|
const { choice } = resolvedChoice;
|
|
@@ -15802,10 +15865,10 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15802
15865
|
if (choice.agentId !== void 0) {
|
|
15803
15866
|
nextOpts.agentId = choice.agentId;
|
|
15804
15867
|
}
|
|
15805
|
-
if (attachOverrides?.
|
|
15806
|
-
nextOpts.
|
|
15868
|
+
if (attachOverrides?.resumeHint !== void 0) {
|
|
15869
|
+
nextOpts.resumeHint = attachOverrides.resumeHint;
|
|
15807
15870
|
} else {
|
|
15808
|
-
delete nextOpts.
|
|
15871
|
+
delete nextOpts.resumeHint;
|
|
15809
15872
|
}
|
|
15810
15873
|
resume(nextOpts);
|
|
15811
15874
|
};
|
|
@@ -15908,10 +15971,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15908
15971
|
} else if (pendingTurns > 0) {
|
|
15909
15972
|
cancelRemoteTurn();
|
|
15910
15973
|
}
|
|
15974
|
+
markCancelling();
|
|
15911
15975
|
return;
|
|
15912
15976
|
}
|
|
15913
15977
|
case "exit":
|
|
15914
|
-
|
|
15978
|
+
requestExit();
|
|
15915
15979
|
return;
|
|
15916
15980
|
case "plan-toggle":
|
|
15917
15981
|
void handleModeToggle(effect.on);
|
|
@@ -15999,7 +16063,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
15999
16063
|
continue;
|
|
16000
16064
|
}
|
|
16001
16065
|
try {
|
|
16002
|
-
const buf = await
|
|
16066
|
+
const buf = await fs24.readFile(token);
|
|
16003
16067
|
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
16004
16068
|
screen.notify(
|
|
16005
16069
|
`image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
|
|
@@ -16209,7 +16273,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16209
16273
|
switch (cmd) {
|
|
16210
16274
|
case "/quit":
|
|
16211
16275
|
case "/exit":
|
|
16212
|
-
|
|
16276
|
+
requestExit();
|
|
16213
16277
|
return true;
|
|
16214
16278
|
case "/clear":
|
|
16215
16279
|
toolStates.clear();
|
|
@@ -16220,6 +16284,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16220
16284
|
toolsBlockStopReason = null;
|
|
16221
16285
|
toolsExpanded = false;
|
|
16222
16286
|
lastEditMarkPath = null;
|
|
16287
|
+
turnHasShownProse = false;
|
|
16223
16288
|
screen.clearScrollback();
|
|
16224
16289
|
return true;
|
|
16225
16290
|
case "/demo-plan": {
|
|
@@ -16565,6 +16630,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16565
16630
|
}
|
|
16566
16631
|
};
|
|
16567
16632
|
let lastEditMarkPath = null;
|
|
16633
|
+
let turnHasShownProse = false;
|
|
16568
16634
|
const maybeRenderEditDiff = (toolCallId) => {
|
|
16569
16635
|
const mode = config.tui.showFileUpdates;
|
|
16570
16636
|
if (mode === "none") {
|
|
@@ -16581,6 +16647,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16581
16647
|
}
|
|
16582
16648
|
return;
|
|
16583
16649
|
}
|
|
16650
|
+
if (!turnHasShownProse) {
|
|
16651
|
+
return;
|
|
16652
|
+
}
|
|
16584
16653
|
const diff = state.editDiff;
|
|
16585
16654
|
if (diff.path && diff.path === lastEditMarkPath) {
|
|
16586
16655
|
return;
|
|
@@ -16666,6 +16735,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16666
16735
|
toolsExpanded = false;
|
|
16667
16736
|
toolsBlockEndedAt = null;
|
|
16668
16737
|
lastEditMarkPath = null;
|
|
16738
|
+
turnHasShownProse = false;
|
|
16669
16739
|
startToolsBlock();
|
|
16670
16740
|
screen.redraw();
|
|
16671
16741
|
return;
|
|
@@ -16673,6 +16743,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16673
16743
|
if (event.kind === "agent-text") {
|
|
16674
16744
|
closeThought();
|
|
16675
16745
|
if (event.text.length > 0) {
|
|
16746
|
+
turnHasShownProse = true;
|
|
16676
16747
|
lastEditMarkPath = null;
|
|
16677
16748
|
}
|
|
16678
16749
|
appendAgentText(event.text);
|
|
@@ -16681,6 +16752,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16681
16752
|
if (event.kind === "agent-thought") {
|
|
16682
16753
|
closeAgentText();
|
|
16683
16754
|
if (viewPrefs.showThoughts && event.text.length > 0) {
|
|
16755
|
+
turnHasShownProse = true;
|
|
16684
16756
|
lastEditMarkPath = null;
|
|
16685
16757
|
}
|
|
16686
16758
|
appendThought(event.text);
|
|
@@ -16804,13 +16876,15 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16804
16876
|
toolsExpanded = false;
|
|
16805
16877
|
upstreamInterruptedSeen = false;
|
|
16806
16878
|
lastEditMarkPath = null;
|
|
16879
|
+
turnHasShownProse = false;
|
|
16807
16880
|
screen.ensureSeparator();
|
|
16808
16881
|
if (shouldDriftSnap({
|
|
16809
16882
|
pendingTurns,
|
|
16810
16883
|
queueSize: queueCache.size,
|
|
16811
16884
|
ownTurnInFlight: turnInFlight !== null,
|
|
16812
16885
|
hasInFlightHead: currentHeadMessageId !== void 0,
|
|
16813
|
-
replayDraining
|
|
16886
|
+
replayDraining,
|
|
16887
|
+
amended: event.amended === true
|
|
16814
16888
|
})) {
|
|
16815
16889
|
adjustPendingTurns(-pendingTurns);
|
|
16816
16890
|
}
|
|
@@ -16889,6 +16963,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
16889
16963
|
toolsBlockStopReason = null;
|
|
16890
16964
|
toolsExpanded = false;
|
|
16891
16965
|
lastEditMarkPath = null;
|
|
16966
|
+
turnHasShownProse = false;
|
|
16892
16967
|
};
|
|
16893
16968
|
onDisconnectHook = () => {
|
|
16894
16969
|
screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
|
|
@@ -17038,8 +17113,8 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
|
|
|
17038
17113
|
agentId: opts.agentId ?? "",
|
|
17039
17114
|
cwd
|
|
17040
17115
|
};
|
|
17041
|
-
if (opts.
|
|
17042
|
-
ctx.
|
|
17116
|
+
if (opts.resumeHint !== void 0) {
|
|
17117
|
+
ctx.resumeHint = opts.resumeHint;
|
|
17043
17118
|
}
|
|
17044
17119
|
return ctx;
|
|
17045
17120
|
}
|
|
@@ -17061,7 +17136,7 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
|
|
|
17061
17136
|
};
|
|
17062
17137
|
}
|
|
17063
17138
|
while (true) {
|
|
17064
|
-
const sessions = await listSessions(target);
|
|
17139
|
+
const sessions = await listSessions(target, { includeNonInteractive: true });
|
|
17065
17140
|
const choice = await pickSession(term, {
|
|
17066
17141
|
cwd,
|
|
17067
17142
|
sessions,
|
|
@@ -17101,6 +17176,33 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
|
|
|
17101
17176
|
}
|
|
17102
17177
|
return decided.ctx;
|
|
17103
17178
|
}
|
|
17179
|
+
if (target.isLocal && chosen && !chosen.importedFromMachine && !opts.readonly) {
|
|
17180
|
+
const v = await validateLocalCwd(chosen.cwd);
|
|
17181
|
+
if (!v.ok) {
|
|
17182
|
+
const r = await promptForImportCwd(term, chosen, {
|
|
17183
|
+
defaultCwd: expandHome(config.defaultCwd),
|
|
17184
|
+
title: "Working directory missing \u2014 choose cwd",
|
|
17185
|
+
intro: "This session's working directory no longer exists. Pick a new one:"
|
|
17186
|
+
});
|
|
17187
|
+
if (r.kind === "cancel") {
|
|
17188
|
+
return null;
|
|
17189
|
+
}
|
|
17190
|
+
if (r.kind === "back") {
|
|
17191
|
+
continue;
|
|
17192
|
+
}
|
|
17193
|
+
const agentId = choice.agentId ?? chosen.agentId ?? "";
|
|
17194
|
+
return {
|
|
17195
|
+
sessionId: choice.sessionId,
|
|
17196
|
+
agentId,
|
|
17197
|
+
cwd: r.path,
|
|
17198
|
+
resumeHint: {
|
|
17199
|
+
agentId,
|
|
17200
|
+
cwd: r.path,
|
|
17201
|
+
upstreamSessionId: ""
|
|
17202
|
+
}
|
|
17203
|
+
};
|
|
17204
|
+
}
|
|
17205
|
+
}
|
|
17104
17206
|
return {
|
|
17105
17207
|
sessionId: choice.sessionId,
|
|
17106
17208
|
agentId: choice.agentId ?? "",
|
|
@@ -17143,7 +17245,9 @@ async function runImportedFirstLaunchFlow(term, chosen, choice, opts) {
|
|
|
17143
17245
|
sessionId: choice.sessionId,
|
|
17144
17246
|
agentId,
|
|
17145
17247
|
cwd: cwdResult.path,
|
|
17146
|
-
|
|
17248
|
+
// Empty upstreamSessionId → import-reseed path for a never-launched
|
|
17249
|
+
// imported session.
|
|
17250
|
+
resumeHint: { agentId, cwd: cwdResult.path, upstreamSessionId: "" }
|
|
17147
17251
|
}
|
|
17148
17252
|
};
|
|
17149
17253
|
}
|
|
@@ -17187,9 +17291,15 @@ fork failed: ${err.message}
|
|
|
17187
17291
|
// For foreign-never-launched forks, the daemon stamped the chosen
|
|
17188
17292
|
// cwd onto meta.json via the POST body, but the very first attach
|
|
17189
17293
|
// still goes through the import-reseed path (upstreamSessionId=""),
|
|
17190
|
-
// and
|
|
17294
|
+
// and the resume hint is what makes attachManagerHooks persist
|
|
17191
17295
|
// the local cwd over the bundle's recorded one.
|
|
17192
|
-
...isForeignNeverLaunched ? {
|
|
17296
|
+
...isForeignNeverLaunched ? {
|
|
17297
|
+
resumeHint: {
|
|
17298
|
+
agentId: choice.sourceAgentId ?? "",
|
|
17299
|
+
cwd,
|
|
17300
|
+
upstreamSessionId: ""
|
|
17301
|
+
}
|
|
17302
|
+
} : {}
|
|
17193
17303
|
}
|
|
17194
17304
|
};
|
|
17195
17305
|
}
|
|
@@ -17315,11 +17425,11 @@ function createInstallStatusLine(term, baseLabel) {
|
|
|
17315
17425
|
}
|
|
17316
17426
|
function rotateIfBig(target) {
|
|
17317
17427
|
try {
|
|
17318
|
-
const stat5 =
|
|
17428
|
+
const stat5 = statSync2(target);
|
|
17319
17429
|
if (stat5.size < logMaxBytes) {
|
|
17320
17430
|
return;
|
|
17321
17431
|
}
|
|
17322
|
-
|
|
17432
|
+
renameSync2(target, `${target}.0`);
|
|
17323
17433
|
} catch {
|
|
17324
17434
|
}
|
|
17325
17435
|
}
|
|
@@ -17331,6 +17441,7 @@ var init_app = __esm({
|
|
|
17331
17441
|
init_types();
|
|
17332
17442
|
init_resilient_ws();
|
|
17333
17443
|
init_config();
|
|
17444
|
+
init_cwd();
|
|
17334
17445
|
init_remote_target();
|
|
17335
17446
|
init_daemon_bootstrap();
|
|
17336
17447
|
init_bin_name();
|
|
@@ -18828,10 +18939,17 @@ var SessionRecord = z5.object({
|
|
|
18828
18939
|
// ended at. Kept so future UI can show "branched from turn N of session X".
|
|
18829
18940
|
forkedFromSessionId: z5.string().optional(),
|
|
18830
18941
|
forkedFromMessageId: z5.string().optional(),
|
|
18831
|
-
// clientInfo from the process that issued session/new.
|
|
18832
|
-
//
|
|
18833
|
-
//
|
|
18942
|
+
// clientInfo from the process that issued session/new. Display only
|
|
18943
|
+
// since the `interactive` flag below; kept on the record for log
|
|
18944
|
+
// attribution and as the legacy hint inside effectiveInteractive
|
|
18945
|
+
// (pre-flag cat sessions can be recognised from this field).
|
|
18834
18946
|
originatingClient: PersistedOriginatingClient.optional(),
|
|
18947
|
+
// Tristate: true once the session has had a real turn, false when
|
|
18948
|
+
// explicitly created as ancillary (e.g. `hydra cat`), undefined for
|
|
18949
|
+
// pre-flag records / freshly-created sessions that haven't decided
|
|
18950
|
+
// yet. effectiveInteractive() in session-manager.ts is the single
|
|
18951
|
+
// resolver — every filter site goes through it.
|
|
18952
|
+
interactive: z5.boolean().optional(),
|
|
18835
18953
|
createdAt: z5.string(),
|
|
18836
18954
|
updatedAt: z5.string()
|
|
18837
18955
|
});
|
|
@@ -18950,6 +19068,7 @@ function recordFromMemorySession(args) {
|
|
|
18950
19068
|
forkedFromSessionId: args.forkedFromSessionId,
|
|
18951
19069
|
forkedFromMessageId: args.forkedFromMessageId,
|
|
18952
19070
|
originatingClient: args.originatingClient,
|
|
19071
|
+
interactive: args.interactive,
|
|
18953
19072
|
createdAt: args.createdAt ?? now,
|
|
18954
19073
|
updatedAt: args.updatedAt ?? now
|
|
18955
19074
|
};
|
|
@@ -19753,6 +19872,7 @@ var HistoryStore = class {
|
|
|
19753
19872
|
|
|
19754
19873
|
// src/core/session-manager.ts
|
|
19755
19874
|
init_paths();
|
|
19875
|
+
init_config();
|
|
19756
19876
|
init_history();
|
|
19757
19877
|
|
|
19758
19878
|
// src/core/bundle.ts
|
|
@@ -19788,6 +19908,14 @@ var BundleSession = z6.object({
|
|
|
19788
19908
|
currentUsage: PersistedUsage.optional(),
|
|
19789
19909
|
agentCommands: z6.array(PersistedAgentCommand).optional(),
|
|
19790
19910
|
agentModes: z6.array(PersistedAgentMode).optional(),
|
|
19911
|
+
// Raw interactive tristate (NOT the resolved effectiveInteractive) so
|
|
19912
|
+
// the value stays promotable on the destination: a cat/empty source
|
|
19913
|
+
// arrives as undefined and a real turn there can still flip it to
|
|
19914
|
+
// true. Carried alongside originatingClient so the importer's
|
|
19915
|
+
// effectiveInteractive can re-apply the cat-name hint at read time
|
|
19916
|
+
// without freezing a sticky `false` into the record.
|
|
19917
|
+
interactive: z6.boolean().optional(),
|
|
19918
|
+
originatingClient: PersistedOriginatingClient.optional(),
|
|
19791
19919
|
createdAt: z6.string(),
|
|
19792
19920
|
updatedAt: z6.string()
|
|
19793
19921
|
});
|
|
@@ -19831,6 +19959,8 @@ function encodeBundle(params) {
|
|
|
19831
19959
|
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
19832
19960
|
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
19833
19961
|
...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
|
|
19962
|
+
...params.record.interactive !== void 0 ? { interactive: params.record.interactive } : {},
|
|
19963
|
+
...params.record.originatingClient !== void 0 ? { originatingClient: params.record.originatingClient } : {},
|
|
19834
19964
|
createdAt: params.record.createdAt,
|
|
19835
19965
|
updatedAt: params.record.updatedAt
|
|
19836
19966
|
},
|
|
@@ -19869,6 +19999,7 @@ var SessionManager = class {
|
|
|
19869
19999
|
this.logger = options.logger;
|
|
19870
20000
|
this.npmRegistry = options.npmRegistry;
|
|
19871
20001
|
this.extensionCommands = options.extensionCommands;
|
|
20002
|
+
this.defaultCwd = options.defaultCwd ?? "~";
|
|
19872
20003
|
this.synopsisCoordinator = new SynopsisCoordinator({
|
|
19873
20004
|
registry: this.registry,
|
|
19874
20005
|
store: this.store,
|
|
@@ -19902,6 +20033,7 @@ var SessionManager = class {
|
|
|
19902
20033
|
logger;
|
|
19903
20034
|
npmRegistry;
|
|
19904
20035
|
extensionCommands;
|
|
20036
|
+
defaultCwd;
|
|
19905
20037
|
// Background queue for ephemeral-agent synopsis generation. Runs
|
|
19906
20038
|
// out-of-band so session close is instant; persists synopsis/title
|
|
19907
20039
|
// via the same enqueueMetaWrite path the in-session handlers used.
|
|
@@ -19961,6 +20093,7 @@ var SessionManager = class {
|
|
|
19961
20093
|
transformChain: params.transformChain,
|
|
19962
20094
|
parentSessionId: params.parentSessionId,
|
|
19963
20095
|
originatingClient: params.originatingClient,
|
|
20096
|
+
interactive: params.interactive,
|
|
19964
20097
|
extensionCommands: this.extensionCommands,
|
|
19965
20098
|
scheduleSynopsis: () => this.synopsisCoordinator.schedule(session.sessionId)
|
|
19966
20099
|
});
|
|
@@ -20007,6 +20140,9 @@ var SessionManager = class {
|
|
|
20007
20140
|
if (params.upstreamSessionId === "") {
|
|
20008
20141
|
return this.doResurrectFromImport(params);
|
|
20009
20142
|
}
|
|
20143
|
+
if (!await this.dirExists(params.cwd)) {
|
|
20144
|
+
return this.doResurrectFromImport(params);
|
|
20145
|
+
}
|
|
20010
20146
|
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
20011
20147
|
npmRegistry: this.npmRegistry,
|
|
20012
20148
|
onInstallProgress: params.onInstallProgress
|
|
@@ -20134,6 +20270,7 @@ var SessionManager = class {
|
|
|
20134
20270
|
firstPromptSeeded: !!params.title,
|
|
20135
20271
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
20136
20272
|
originatingClient: params.originatingClient,
|
|
20273
|
+
interactive: params.interactive,
|
|
20137
20274
|
forkedFromSessionId: params.forkedFromSessionId,
|
|
20138
20275
|
forkedFromMessageId: params.forkedFromMessageId,
|
|
20139
20276
|
extensionCommands: this.extensionCommands,
|
|
@@ -20150,7 +20287,7 @@ var SessionManager = class {
|
|
|
20150
20287
|
// so subsequent resurrects of this session use the normal session/load
|
|
20151
20288
|
// path.
|
|
20152
20289
|
async doResurrectFromImport(params) {
|
|
20153
|
-
const cwd = await this.
|
|
20290
|
+
const cwd = await this.resolveResurrectCwd(params.cwd);
|
|
20154
20291
|
const fresh = await this.bootstrapAgent({
|
|
20155
20292
|
agentId: params.agentId,
|
|
20156
20293
|
cwd,
|
|
@@ -20205,6 +20342,7 @@ var SessionManager = class {
|
|
|
20205
20342
|
firstPromptSeeded: !!params.title,
|
|
20206
20343
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
20207
20344
|
originatingClient: params.originatingClient,
|
|
20345
|
+
interactive: params.interactive,
|
|
20208
20346
|
forkedFromSessionId: params.forkedFromSessionId,
|
|
20209
20347
|
forkedFromMessageId: params.forkedFromMessageId,
|
|
20210
20348
|
extensionCommands: this.extensionCommands,
|
|
@@ -20214,15 +20352,50 @@ var SessionManager = class {
|
|
|
20214
20352
|
void session.seedFromImport().catch(() => void 0);
|
|
20215
20353
|
return session;
|
|
20216
20354
|
}
|
|
20217
|
-
async
|
|
20355
|
+
async dirExists(cwd) {
|
|
20218
20356
|
try {
|
|
20219
|
-
|
|
20220
|
-
if (stat5.isDirectory()) {
|
|
20221
|
-
return cwd;
|
|
20222
|
-
}
|
|
20357
|
+
return (await fs14.stat(cwd)).isDirectory();
|
|
20223
20358
|
} catch {
|
|
20359
|
+
return false;
|
|
20224
20360
|
}
|
|
20225
|
-
|
|
20361
|
+
}
|
|
20362
|
+
// When the last client detaches from a session that resolves to
|
|
20363
|
+
// non-interactive — e.g. a `hydra cat` run, born interactive:undefined
|
|
20364
|
+
// with originatingClient hydra-acp-cat, whose every prompt is ancillary
|
|
20365
|
+
// — close it so its agent process doesn't linger until the (default 1h)
|
|
20366
|
+
// idle timeout fires. The cold record is kept, so the rare refine-in-TUI
|
|
20367
|
+
// still works via the resurrect/reseed path. Sessions promoted to
|
|
20368
|
+
// interactive (driven by a real, non-ancillary prompt) resolve to true
|
|
20369
|
+
// and are left running.
|
|
20370
|
+
async reapIfOrphanedNonInteractive(sessionId) {
|
|
20371
|
+
const session = this.sessions.get(sessionId);
|
|
20372
|
+
if (!session || session.attachedCount > 0) {
|
|
20373
|
+
return;
|
|
20374
|
+
}
|
|
20375
|
+
const interactive = effectiveInteractive(
|
|
20376
|
+
{
|
|
20377
|
+
interactive: session.interactive,
|
|
20378
|
+
...session.originatingClient ? { originatingClient: session.originatingClient } : {}
|
|
20379
|
+
},
|
|
20380
|
+
true
|
|
20381
|
+
);
|
|
20382
|
+
if (interactive !== false) {
|
|
20383
|
+
return;
|
|
20384
|
+
}
|
|
20385
|
+
this.logger?.info(
|
|
20386
|
+
`reaping orphaned non-interactive session ${sessionId} (agent killed, cold record kept)`
|
|
20387
|
+
);
|
|
20388
|
+
await session.close({ deleteRecord: false }).catch(() => void 0);
|
|
20389
|
+
}
|
|
20390
|
+
// Resolve a recorded cwd for resurrect: use it if it still exists,
|
|
20391
|
+
// otherwise fall back to the configured defaultCwd. Covers both bundles
|
|
20392
|
+
// imported from another machine and local sessions (e.g. `cat`) whose
|
|
20393
|
+
// recorded dir was cleaned up, so the reseed spawn never ENOENTs.
|
|
20394
|
+
async resolveResurrectCwd(cwd) {
|
|
20395
|
+
if (await this.dirExists(cwd)) {
|
|
20396
|
+
return cwd;
|
|
20397
|
+
}
|
|
20398
|
+
return expandHome(this.defaultCwd);
|
|
20226
20399
|
}
|
|
20227
20400
|
// Pull every session the agent itself remembers (across all cwds) and
|
|
20228
20401
|
// persist a cold hydra record for each one we don't already track.
|
|
@@ -20311,6 +20484,10 @@ var SessionManager = class {
|
|
|
20311
20484
|
agentId,
|
|
20312
20485
|
cwd: entry.cwd,
|
|
20313
20486
|
pendingHistorySync: true,
|
|
20487
|
+
// `hydra agent sync` is a user-explicit "show me agent-side
|
|
20488
|
+
// sessions" action; the rows are meant to be visible immediately
|
|
20489
|
+
// even before the first resurrect populates history.jsonl.
|
|
20490
|
+
interactive: true,
|
|
20314
20491
|
createdAt: ts,
|
|
20315
20492
|
updatedAt: ts
|
|
20316
20493
|
};
|
|
@@ -20477,6 +20654,11 @@ var SessionManager = class {
|
|
|
20477
20654
|
() => void 0
|
|
20478
20655
|
);
|
|
20479
20656
|
});
|
|
20657
|
+
session.onInteractiveChange((interactive) => {
|
|
20658
|
+
void this.persistSnapshot(session.sessionId, { interactive }).catch(
|
|
20659
|
+
() => void 0
|
|
20660
|
+
);
|
|
20661
|
+
});
|
|
20480
20662
|
session.onUsageChange((usage) => {
|
|
20481
20663
|
void this.persistSnapshot(session.sessionId, {
|
|
20482
20664
|
currentUsage: usageSnapshotToPersisted(usage)
|
|
@@ -20570,6 +20752,7 @@ var SessionManager = class {
|
|
|
20570
20752
|
createdAt: record.createdAt,
|
|
20571
20753
|
pendingHistorySync: record.pendingHistorySync,
|
|
20572
20754
|
originatingClient: record.originatingClient,
|
|
20755
|
+
interactive: record.interactive,
|
|
20573
20756
|
forkedFromSessionId: record.forkedFromSessionId,
|
|
20574
20757
|
forkedFromMessageId: record.forkedFromMessageId
|
|
20575
20758
|
};
|
|
@@ -20657,12 +20840,27 @@ var SessionManager = class {
|
|
|
20657
20840
|
async list(filter = {}) {
|
|
20658
20841
|
const entries = [];
|
|
20659
20842
|
const liveIds = /* @__PURE__ */ new Set();
|
|
20843
|
+
const includeRow = (interactive) => {
|
|
20844
|
+
if (filter.includeNonInteractive) return true;
|
|
20845
|
+
return interactive === true;
|
|
20846
|
+
};
|
|
20660
20847
|
for (const session of this.sessions.values()) {
|
|
20661
20848
|
if (filter.cwd && session.cwd !== filter.cwd) {
|
|
20662
20849
|
continue;
|
|
20663
20850
|
}
|
|
20664
20851
|
liveIds.add(session.sessionId);
|
|
20665
|
-
const
|
|
20852
|
+
const hist = await historyStatus(session.sessionId);
|
|
20853
|
+
const interactive = effectiveInteractive(
|
|
20854
|
+
{
|
|
20855
|
+
interactive: session.interactive,
|
|
20856
|
+
...session.originatingClient ? { originatingClient: session.originatingClient } : {}
|
|
20857
|
+
},
|
|
20858
|
+
hist.hasContent
|
|
20859
|
+
);
|
|
20860
|
+
if (!includeRow(interactive)) {
|
|
20861
|
+
continue;
|
|
20862
|
+
}
|
|
20863
|
+
const used = hist.mtime ?? new Date(session.updatedAt).toISOString();
|
|
20666
20864
|
entries.push({
|
|
20667
20865
|
sessionId: session.sessionId,
|
|
20668
20866
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -20675,6 +20873,7 @@ var SessionManager = class {
|
|
|
20675
20873
|
forkedFromSessionId: session.forkedFromSessionId,
|
|
20676
20874
|
forkedFromMessageId: session.forkedFromMessageId,
|
|
20677
20875
|
originatingClient: session.originatingClient,
|
|
20876
|
+
interactive,
|
|
20678
20877
|
updatedAt: used,
|
|
20679
20878
|
attachedClients: session.attachedCount,
|
|
20680
20879
|
status: "live",
|
|
@@ -20689,7 +20888,12 @@ var SessionManager = class {
|
|
|
20689
20888
|
if (filter.cwd && r.cwd !== filter.cwd) {
|
|
20690
20889
|
continue;
|
|
20691
20890
|
}
|
|
20692
|
-
const
|
|
20891
|
+
const hist = await historyStatus(r.sessionId);
|
|
20892
|
+
const interactive = effectiveInteractive(r, hist.hasContent);
|
|
20893
|
+
if (!includeRow(interactive)) {
|
|
20894
|
+
continue;
|
|
20895
|
+
}
|
|
20896
|
+
const used = hist.mtime ?? r.updatedAt;
|
|
20693
20897
|
entries.push({
|
|
20694
20898
|
sessionId: r.sessionId,
|
|
20695
20899
|
upstreamSessionId: r.upstreamSessionId,
|
|
@@ -20707,6 +20911,7 @@ var SessionManager = class {
|
|
|
20707
20911
|
forkedFromSessionId: r.forkedFromSessionId,
|
|
20708
20912
|
forkedFromMessageId: r.forkedFromMessageId,
|
|
20709
20913
|
originatingClient: r.originatingClient,
|
|
20914
|
+
interactive,
|
|
20710
20915
|
updatedAt: used,
|
|
20711
20916
|
attachedClients: 0,
|
|
20712
20917
|
status: "cold",
|
|
@@ -20941,8 +21146,18 @@ var SessionManager = class {
|
|
|
20941
21146
|
currentUsage: args.bundle.session.currentUsage,
|
|
20942
21147
|
agentCommands: args.bundle.session.agentCommands,
|
|
20943
21148
|
agentModes: args.bundle.session.agentModes,
|
|
21149
|
+
// Carry the source's raw interactive tristate and originating
|
|
21150
|
+
// client rather than forcing true. A real conversation arrives
|
|
21151
|
+
// as true (visible immediately); an empty source arrives as
|
|
21152
|
+
// undefined (hidden until a turn lands here); a cat source
|
|
21153
|
+
// arrives as undefined + cat originatingClient, so
|
|
21154
|
+
// effectiveInteractive hides it via the hint while leaving it
|
|
21155
|
+
// promotable. Legacy bundles (pre-flag) carry neither and fall
|
|
21156
|
+
// back to effectiveInteractive's history-presence inference.
|
|
21157
|
+
interactive: args.bundle.session.interactive,
|
|
21158
|
+
originatingClient: args.bundle.session.originatingClient,
|
|
20944
21159
|
createdAt: args.preservedCreatedAt ?? now,
|
|
20945
|
-
// Fallback path for
|
|
21160
|
+
// Fallback path for historyStatus (used when the history file
|
|
20946
21161
|
// is missing). Keep this consistent with the utimes stamp above.
|
|
20947
21162
|
updatedAt: args.bundle.session.updatedAt
|
|
20948
21163
|
});
|
|
@@ -21051,6 +21266,8 @@ var SessionManager = class {
|
|
|
21051
21266
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
21052
21267
|
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
21053
21268
|
...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
|
|
21269
|
+
...update.interactive !== void 0 ? { interactive: update.interactive } : {},
|
|
21270
|
+
...update.cwd !== void 0 ? { cwd: update.cwd } : {},
|
|
21054
21271
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21055
21272
|
});
|
|
21056
21273
|
});
|
|
@@ -21227,6 +21444,7 @@ function mergeForPersistence(session, existing) {
|
|
|
21227
21444
|
forkedFromSessionId: session.forkedFromSessionId ?? existing?.forkedFromSessionId,
|
|
21228
21445
|
forkedFromMessageId: session.forkedFromMessageId ?? existing?.forkedFromMessageId,
|
|
21229
21446
|
originatingClient: session.originatingClient ?? existing?.originatingClient,
|
|
21447
|
+
interactive: session.interactive ?? existing?.interactive,
|
|
21230
21448
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
21231
21449
|
});
|
|
21232
21450
|
}
|
|
@@ -21533,13 +21751,25 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
21533
21751
|
return [];
|
|
21534
21752
|
}
|
|
21535
21753
|
}
|
|
21536
|
-
async function
|
|
21754
|
+
async function historyStatus(sessionId) {
|
|
21537
21755
|
try {
|
|
21538
21756
|
const st = await fs14.stat(paths.historyFile(sessionId));
|
|
21539
|
-
return
|
|
21757
|
+
return {
|
|
21758
|
+
mtime: new Date(st.mtimeMs).toISOString(),
|
|
21759
|
+
hasContent: st.size > 0
|
|
21760
|
+
};
|
|
21540
21761
|
} catch {
|
|
21541
|
-
return
|
|
21762
|
+
return { hasContent: false };
|
|
21763
|
+
}
|
|
21764
|
+
}
|
|
21765
|
+
function effectiveInteractive(record, hasContent) {
|
|
21766
|
+
if (record.interactive !== void 0) {
|
|
21767
|
+
return record.interactive;
|
|
21542
21768
|
}
|
|
21769
|
+
if (record.originatingClient?.name === HYDRA_CAT_CLIENT_NAME) {
|
|
21770
|
+
return false;
|
|
21771
|
+
}
|
|
21772
|
+
return hasContent ? true : void 0;
|
|
21543
21773
|
}
|
|
21544
21774
|
|
|
21545
21775
|
// src/core/child-supervisor.ts
|
|
@@ -23317,17 +23547,21 @@ function resolveHydraHost(defaults) {
|
|
|
23317
23547
|
function registerSessionRoutes(app, manager, defaults) {
|
|
23318
23548
|
app.get("/v1/sessions", async (request) => {
|
|
23319
23549
|
const query = request.query;
|
|
23320
|
-
const
|
|
23550
|
+
const includeNonInteractive = query?.includeNonInteractive === "1" || query?.includeNonInteractive === "true";
|
|
23551
|
+
const sessions = await manager.list({
|
|
23552
|
+
cwd: query?.cwd,
|
|
23553
|
+
includeNonInteractive
|
|
23554
|
+
});
|
|
23321
23555
|
return { sessions };
|
|
23322
23556
|
});
|
|
23323
|
-
app.
|
|
23324
|
-
const
|
|
23325
|
-
const q =
|
|
23557
|
+
app.post("/v1/sessions/search", async (request, reply) => {
|
|
23558
|
+
const body = request.body ?? {};
|
|
23559
|
+
const q = typeof body.q === "string" ? body.q : "";
|
|
23326
23560
|
if (q.trim().length === 0) {
|
|
23327
23561
|
reply.code(400).send({ error: "q is required" });
|
|
23328
23562
|
return reply;
|
|
23329
23563
|
}
|
|
23330
|
-
const ids =
|
|
23564
|
+
const ids = Array.isArray(body.sessionIds) ? body.sessionIds.filter((s) => typeof s === "string" && s.length > 0) : void 0;
|
|
23331
23565
|
const out = await searchHistories(manager, q, { sessionIds: ids });
|
|
23332
23566
|
return out;
|
|
23333
23567
|
});
|
|
@@ -23922,13 +24156,8 @@ function parseRegisterBody2(body) {
|
|
|
23922
24156
|
}
|
|
23923
24157
|
|
|
23924
24158
|
// src/daemon/routes/config.ts
|
|
23925
|
-
function registerConfigRoutes(app,
|
|
23926
|
-
app.get("/v1/config", async () =>
|
|
23927
|
-
return {
|
|
23928
|
-
defaultAgent: defaults.defaultAgent,
|
|
23929
|
-
defaultCwd: defaults.defaultCwd
|
|
23930
|
-
};
|
|
23931
|
-
});
|
|
24159
|
+
function registerConfigRoutes(app, snapshot) {
|
|
24160
|
+
app.get("/v1/config", async () => snapshot);
|
|
23932
24161
|
}
|
|
23933
24162
|
|
|
23934
24163
|
// src/daemon/routes/auth.ts
|
|
@@ -24448,7 +24677,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
24448
24677
|
model: hydraMeta.model,
|
|
24449
24678
|
onInstallProgress: makeInstallProgressForwarder(connection),
|
|
24450
24679
|
transformChain,
|
|
24451
|
-
originatingClient: state.clientInfo
|
|
24680
|
+
originatingClient: state.clientInfo,
|
|
24681
|
+
...hydraMeta.interactive !== void 0 ? { interactive: hydraMeta.interactive } : {}
|
|
24452
24682
|
});
|
|
24453
24683
|
} catch (err) {
|
|
24454
24684
|
if (stdinReservation !== void 0) {
|
|
@@ -24575,8 +24805,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
24575
24805
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
24576
24806
|
throw err;
|
|
24577
24807
|
}
|
|
24808
|
+
const resurrectWithOriginator = resurrectParams.originatingClient ? resurrectParams : { ...resurrectParams, originatingClient: state.clientInfo };
|
|
24578
24809
|
session = await deps.manager.resurrect({
|
|
24579
|
-
...
|
|
24810
|
+
...resurrectWithOriginator,
|
|
24580
24811
|
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
24581
24812
|
});
|
|
24582
24813
|
wireDefaultTransformers(session, deps);
|
|
@@ -24633,6 +24864,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
24633
24864
|
const session = deps.manager.get(params.sessionId);
|
|
24634
24865
|
session?.detach(att.clientId);
|
|
24635
24866
|
state.attached.delete(params.sessionId);
|
|
24867
|
+
if (session) {
|
|
24868
|
+
void deps.manager.reapIfOrphanedNonInteractive(params.sessionId);
|
|
24869
|
+
}
|
|
24636
24870
|
return { sessionId: params.sessionId, status: "detached" };
|
|
24637
24871
|
});
|
|
24638
24872
|
connection.onRequest("session/list", async (raw) => {
|
|
@@ -25880,7 +26114,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
25880
26114
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
25881
26115
|
logger: agentLogger,
|
|
25882
26116
|
npmRegistry: config.npmRegistry,
|
|
25883
|
-
extensionCommands
|
|
26117
|
+
extensionCommands,
|
|
26118
|
+
defaultCwd: config.defaultCwd
|
|
25884
26119
|
});
|
|
25885
26120
|
const extensions = new ExtensionManager(extensionList(config), void 0, {
|
|
25886
26121
|
tokenRegistry: processRegistry
|
|
@@ -25901,7 +26136,12 @@ async function startDaemon(config, serviceToken) {
|
|
|
25901
26136
|
registerTransformerRoutes(app, transformers);
|
|
25902
26137
|
registerConfigRoutes(app, {
|
|
25903
26138
|
defaultAgent: config.defaultAgent,
|
|
25904
|
-
defaultCwd: config.defaultCwd
|
|
26139
|
+
defaultCwd: config.defaultCwd,
|
|
26140
|
+
defaultModels: { ...config.defaultModels },
|
|
26141
|
+
...config.synopsisAgent !== void 0 ? { synopsisAgent: config.synopsisAgent } : {},
|
|
26142
|
+
...config.synopsisModel !== void 0 ? { synopsisModel: config.synopsisModel } : {},
|
|
26143
|
+
synopsisOnClose: config.synopsisOnClose,
|
|
26144
|
+
defaultTransformers: [...config.defaultTransformers]
|
|
25905
26145
|
});
|
|
25906
26146
|
registerAuthRoutes(app, {
|
|
25907
26147
|
store: sessionTokenStore,
|
|
@@ -26389,7 +26629,6 @@ init_remote_target();
|
|
|
26389
26629
|
init_remote_url();
|
|
26390
26630
|
init_session();
|
|
26391
26631
|
init_discovery();
|
|
26392
|
-
init_hydra_version();
|
|
26393
26632
|
import * as fs19 from "fs/promises";
|
|
26394
26633
|
import * as path16 from "path";
|
|
26395
26634
|
init_session_row();
|
|
@@ -26398,6 +26637,9 @@ async function runSessionsList(opts = {}) {
|
|
|
26398
26637
|
const serviceToken = await loadServiceToken();
|
|
26399
26638
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
26400
26639
|
const url = new URL(`${baseUrl}/v1/sessions`);
|
|
26640
|
+
if (opts.includeNonInteractive || opts.all) {
|
|
26641
|
+
url.searchParams.set("includeNonInteractive", "true");
|
|
26642
|
+
}
|
|
26401
26643
|
const response = await fetch(url.toString(), {
|
|
26402
26644
|
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
26403
26645
|
});
|
|
@@ -26407,13 +26649,11 @@ async function runSessionsList(opts = {}) {
|
|
|
26407
26649
|
process.exit(1);
|
|
26408
26650
|
}
|
|
26409
26651
|
const body = await response.json();
|
|
26410
|
-
const
|
|
26411
|
-
(s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
|
|
26412
|
-
);
|
|
26652
|
+
const sessionsAfterInteractiveFilter = body.sessions;
|
|
26413
26653
|
const host = opts.host ?? "local";
|
|
26414
|
-
const hostFiltered = host === "all" ?
|
|
26654
|
+
const hostFiltered = host === "all" ? sessionsAfterInteractiveFilter : host === "local" ? sessionsAfterInteractiveFilter.filter(
|
|
26415
26655
|
(s) => !s.importedFromMachine || !!s.upstreamSessionId
|
|
26416
|
-
) :
|
|
26656
|
+
) : sessionsAfterInteractiveFilter.filter(
|
|
26417
26657
|
(s) => s.importedFromMachine === host && !s.upstreamSessionId
|
|
26418
26658
|
);
|
|
26419
26659
|
if (opts.json) {
|
|
@@ -28380,6 +28620,7 @@ function maxLen3(headerCell, values) {
|
|
|
28380
28620
|
init_config();
|
|
28381
28621
|
init_service_token();
|
|
28382
28622
|
init_paths();
|
|
28623
|
+
import * as fsp12 from "fs/promises";
|
|
28383
28624
|
async function runAgentsList() {
|
|
28384
28625
|
const config = await loadConfig();
|
|
28385
28626
|
const serviceToken = await loadServiceToken();
|
|
@@ -28609,6 +28850,104 @@ async function runAgentsLogs(agentId, rest) {
|
|
|
28609
28850
|
const logPath = paths.agentLogFile(agentId);
|
|
28610
28851
|
await runLogTail(logPath, rest, "No log file (agent never ran?)");
|
|
28611
28852
|
}
|
|
28853
|
+
async function runAgentsSet(agentId, modelId) {
|
|
28854
|
+
const config = await loadConfig();
|
|
28855
|
+
const serviceToken = await loadServiceToken();
|
|
28856
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
28857
|
+
if (!agentId) {
|
|
28858
|
+
const daemonView2 = await fetchDaemonAgentDefaults(baseUrl, serviceToken);
|
|
28859
|
+
const view = daemonView2 ?? readAgentDefaults(await readRawConfig3());
|
|
28860
|
+
process.stdout.write(`${formatDefaultLine(view)}
|
|
28861
|
+
`);
|
|
28862
|
+
return;
|
|
28863
|
+
}
|
|
28864
|
+
let known;
|
|
28865
|
+
try {
|
|
28866
|
+
const r = await fetch(`${baseUrl}/v1/agents`, {
|
|
28867
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
28868
|
+
});
|
|
28869
|
+
if (r.ok) {
|
|
28870
|
+
const body = await r.json();
|
|
28871
|
+
known = body.agents.map((a) => a.id);
|
|
28872
|
+
}
|
|
28873
|
+
} catch {
|
|
28874
|
+
}
|
|
28875
|
+
if (known !== void 0 && !known.includes(agentId)) {
|
|
28876
|
+
process.stderr.write(
|
|
28877
|
+
`hydra agent set: '${agentId}' is not in the registry. Known ids: ${known.join(", ")}
|
|
28878
|
+
`
|
|
28879
|
+
);
|
|
28880
|
+
process.exit(1);
|
|
28881
|
+
return;
|
|
28882
|
+
}
|
|
28883
|
+
const raw = await readRawConfig3();
|
|
28884
|
+
if (modelId === void 0) {
|
|
28885
|
+
raw.defaultAgent = agentId;
|
|
28886
|
+
await writeRawConfig3(raw);
|
|
28887
|
+
} else {
|
|
28888
|
+
const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
|
|
28889
|
+
models[agentId] = modelId;
|
|
28890
|
+
raw.defaultModels = models;
|
|
28891
|
+
await writeRawConfig3(raw);
|
|
28892
|
+
}
|
|
28893
|
+
const disk = readAgentDefaults(await readRawConfig3());
|
|
28894
|
+
if (modelId !== void 0 && agentId !== disk.agent) {
|
|
28895
|
+
process.stdout.write(
|
|
28896
|
+
`Default model for ${agentId} is now ${modelId}.
|
|
28897
|
+
`
|
|
28898
|
+
);
|
|
28899
|
+
}
|
|
28900
|
+
process.stdout.write(`${formatDefaultLine(disk)}
|
|
28901
|
+
`);
|
|
28902
|
+
const daemonView = await fetchDaemonAgentDefaults(baseUrl, serviceToken);
|
|
28903
|
+
if (daemonView === void 0) {
|
|
28904
|
+
return;
|
|
28905
|
+
}
|
|
28906
|
+
if (daemonView.agent === disk.agent && daemonView.model === disk.model) {
|
|
28907
|
+
return;
|
|
28908
|
+
}
|
|
28909
|
+
process.stdout.write(
|
|
28910
|
+
`Daemon still has ${formatAgentModel(daemonView)} \u2014 restart with \`hydra-acp daemon restart\` to apply.
|
|
28911
|
+
`
|
|
28912
|
+
);
|
|
28913
|
+
}
|
|
28914
|
+
function formatDefaultLine(view) {
|
|
28915
|
+
return `Default agent is ${formatAgentModel(view)}`;
|
|
28916
|
+
}
|
|
28917
|
+
function formatAgentModel(view) {
|
|
28918
|
+
return view.model !== void 0 ? `${view.agent} with ${view.model}` : view.agent;
|
|
28919
|
+
}
|
|
28920
|
+
function readAgentDefaults(raw) {
|
|
28921
|
+
const agent = typeof raw.defaultAgent === "string" ? raw.defaultAgent : "(unset)";
|
|
28922
|
+
const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
|
|
28923
|
+
const rawModel = models[agent];
|
|
28924
|
+
return typeof rawModel === "string" ? { agent, model: rawModel } : { agent };
|
|
28925
|
+
}
|
|
28926
|
+
async function fetchDaemonAgentDefaults(baseUrl, serviceToken) {
|
|
28927
|
+
try {
|
|
28928
|
+
const r = await fetch(`${baseUrl}/v1/config`, {
|
|
28929
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
28930
|
+
});
|
|
28931
|
+
if (!r.ok) {
|
|
28932
|
+
return void 0;
|
|
28933
|
+
}
|
|
28934
|
+
const body = await r.json();
|
|
28935
|
+
return readAgentDefaults(body);
|
|
28936
|
+
} catch {
|
|
28937
|
+
return void 0;
|
|
28938
|
+
}
|
|
28939
|
+
}
|
|
28940
|
+
async function readRawConfig3() {
|
|
28941
|
+
const raw = await fsp12.readFile(paths.config(), "utf8");
|
|
28942
|
+
return JSON.parse(raw);
|
|
28943
|
+
}
|
|
28944
|
+
async function writeRawConfig3(raw) {
|
|
28945
|
+
await fsp12.writeFile(
|
|
28946
|
+
paths.config(),
|
|
28947
|
+
JSON.stringify(raw, null, 2) + "\n",
|
|
28948
|
+
{ encoding: "utf8", mode: 384 }
|
|
28949
|
+
);
|
|
28950
|
+
}
|
|
28612
28951
|
async function runAgentsRefresh() {
|
|
28613
28952
|
const config = await loadConfig();
|
|
28614
28953
|
const serviceToken = await loadServiceToken();
|
|
@@ -28768,6 +29107,7 @@ function maxLen5(headerCell, values) {
|
|
|
28768
29107
|
}
|
|
28769
29108
|
|
|
28770
29109
|
// src/shim/proxy.ts
|
|
29110
|
+
import * as fs21 from "fs";
|
|
28771
29111
|
init_config();
|
|
28772
29112
|
init_remote_target();
|
|
28773
29113
|
init_daemon_bootstrap();
|
|
@@ -28944,6 +29284,8 @@ function isResponse2(msg) {
|
|
|
28944
29284
|
|
|
28945
29285
|
// src/shim/proxy.ts
|
|
28946
29286
|
init_permission_pick();
|
|
29287
|
+
init_hydra_version();
|
|
29288
|
+
init_paths();
|
|
28947
29289
|
|
|
28948
29290
|
// src/core/process-title.ts
|
|
28949
29291
|
init_bin_name();
|
|
@@ -29028,6 +29370,7 @@ function wireShim({
|
|
|
29028
29370
|
tracker
|
|
29029
29371
|
}) {
|
|
29030
29372
|
upstream.onMessage((msg) => {
|
|
29373
|
+
wireLog("daemon\u2192client", msg);
|
|
29031
29374
|
tracker.observeFromServer(msg);
|
|
29032
29375
|
if (opts.dangerouslySkipPermissions === true && isPermissionRequest(msg)) {
|
|
29033
29376
|
void upstream.send({
|
|
@@ -29042,7 +29385,12 @@ function wireShim({
|
|
|
29042
29385
|
});
|
|
29043
29386
|
const namingState = { name: opts.name, used: false };
|
|
29044
29387
|
downstream.onMessage((msg) => {
|
|
29388
|
+
wireLog("client\u2192daemon", msg);
|
|
29045
29389
|
tracker.observeFromClient(msg);
|
|
29390
|
+
if (isInitializeRequest(msg)) {
|
|
29391
|
+
void upstream.send(normaliseInitializeClientInfo(msg));
|
|
29392
|
+
return;
|
|
29393
|
+
}
|
|
29046
29394
|
if (isSessionNewRequest(msg)) {
|
|
29047
29395
|
if (opts.sessionId) {
|
|
29048
29396
|
void upstream.send(buildAttachFromNew(msg, opts.sessionId));
|
|
@@ -29186,6 +29534,29 @@ async function replayAttach(stream, ctx, afterMessageId) {
|
|
|
29186
29534
|
function isSessionNewRequest(msg) {
|
|
29187
29535
|
return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/new";
|
|
29188
29536
|
}
|
|
29537
|
+
function isInitializeRequest(msg) {
|
|
29538
|
+
return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "initialize";
|
|
29539
|
+
}
|
|
29540
|
+
function normaliseInitializeClientInfo(msg) {
|
|
29541
|
+
const params = msg.params ?? {};
|
|
29542
|
+
const existing = params.clientInfo;
|
|
29543
|
+
const existingObj = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : void 0;
|
|
29544
|
+
const existingName = existingObj && typeof existingObj.name === "string" ? existingObj.name.trim() : "";
|
|
29545
|
+
if (existingName.length > 0) {
|
|
29546
|
+
return msg;
|
|
29547
|
+
}
|
|
29548
|
+
return {
|
|
29549
|
+
...msg,
|
|
29550
|
+
params: {
|
|
29551
|
+
...params,
|
|
29552
|
+
clientInfo: {
|
|
29553
|
+
...existingObj ?? {},
|
|
29554
|
+
name: "hydra-acp-shim",
|
|
29555
|
+
version: HYDRA_VERSION
|
|
29556
|
+
}
|
|
29557
|
+
}
|
|
29558
|
+
};
|
|
29559
|
+
}
|
|
29189
29560
|
function isPermissionRequest(msg) {
|
|
29190
29561
|
return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/request_permission";
|
|
29191
29562
|
}
|
|
@@ -29207,6 +29578,40 @@ function rewriteSessionNewWithAgent(msg, agentId) {
|
|
|
29207
29578
|
params: { ...params, agentId }
|
|
29208
29579
|
};
|
|
29209
29580
|
}
|
|
29581
|
+
var WIRE_LOG_MAX_BYTES = 25 * 1024 * 1024;
|
|
29582
|
+
var wireLogChecked = false;
|
|
29583
|
+
var wireLogPath = null;
|
|
29584
|
+
function wireLog(direction, msg) {
|
|
29585
|
+
if (!process.env.HYDRA_SHIM_WIRE_LOG) {
|
|
29586
|
+
return;
|
|
29587
|
+
}
|
|
29588
|
+
if (!wireLogChecked) {
|
|
29589
|
+
wireLogChecked = true;
|
|
29590
|
+
try {
|
|
29591
|
+
wireLogPath = paths.shimWireLogFile();
|
|
29592
|
+
fs21.mkdirSync(paths.home(), { recursive: true });
|
|
29593
|
+
const st = fs21.statSync(wireLogPath, { throwIfNoEntry: false });
|
|
29594
|
+
if (st && st.size > WIRE_LOG_MAX_BYTES) {
|
|
29595
|
+
fs21.renameSync(wireLogPath, `${wireLogPath}.1`);
|
|
29596
|
+
}
|
|
29597
|
+
} catch {
|
|
29598
|
+
wireLogPath = null;
|
|
29599
|
+
}
|
|
29600
|
+
}
|
|
29601
|
+
if (!wireLogPath) {
|
|
29602
|
+
return;
|
|
29603
|
+
}
|
|
29604
|
+
try {
|
|
29605
|
+
const line = JSON.stringify({
|
|
29606
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29607
|
+
pid: process.pid,
|
|
29608
|
+
dir: direction,
|
|
29609
|
+
msg
|
|
29610
|
+
}) + "\n";
|
|
29611
|
+
fs21.appendFile(wireLogPath, line, () => void 0);
|
|
29612
|
+
} catch {
|
|
29613
|
+
}
|
|
29614
|
+
}
|
|
29210
29615
|
function injectHydraMeta(msg, additions) {
|
|
29211
29616
|
const params = msg.params ?? {};
|
|
29212
29617
|
const existingMeta = params._meta ?? {};
|
|
@@ -29359,6 +29764,13 @@ function applyStyle(text, style) {
|
|
|
29359
29764
|
// src/cli/commands/cat.ts
|
|
29360
29765
|
var DEFAULT_STREAM_THRESHOLD = 1 * 1024 * 1024;
|
|
29361
29766
|
var HYDRA_STDIN_TOOL_PREFIX = "mcp__hydra-acp-stdin__";
|
|
29767
|
+
function catPromptParams(sessionId, prompt) {
|
|
29768
|
+
return {
|
|
29769
|
+
sessionId,
|
|
29770
|
+
prompt,
|
|
29771
|
+
_meta: { [HYDRA_META_KEY]: { ancillary: true } }
|
|
29772
|
+
};
|
|
29773
|
+
}
|
|
29362
29774
|
function deriveTitleFromPrompt(prompt) {
|
|
29363
29775
|
if (!prompt) {
|
|
29364
29776
|
return void 0;
|
|
@@ -29539,10 +29951,7 @@ async function runCatLoop(args) {
|
|
|
29539
29951
|
return;
|
|
29540
29952
|
}
|
|
29541
29953
|
try {
|
|
29542
|
-
await conn.request("session/prompt",
|
|
29543
|
-
sessionId,
|
|
29544
|
-
prompt: promptBlocks
|
|
29545
|
-
});
|
|
29954
|
+
await conn.request("session/prompt", catPromptParams(sessionId, promptBlocks));
|
|
29546
29955
|
firstChunkSent = true;
|
|
29547
29956
|
} catch (err) {
|
|
29548
29957
|
stderr(`hydra-acp cat: prompt failed: ${err.message}
|
|
@@ -29765,10 +30174,10 @@ function runStreamingPath(args) {
|
|
|
29765
30174
|
}
|
|
29766
30175
|
await writeChain.catch(() => void 0);
|
|
29767
30176
|
const promptText = buildStreamPromptText(opts.prompt, open2.capacityBytes);
|
|
29768
|
-
const promptDone = conn.request(
|
|
29769
|
-
|
|
29770
|
-
|
|
29771
|
-
|
|
30177
|
+
const promptDone = conn.request(
|
|
30178
|
+
"session/prompt",
|
|
30179
|
+
catPromptParams(sessionId, [{ type: "text", text: promptText }])
|
|
30180
|
+
).catch((err) => {
|
|
29772
30181
|
args.onPromptFailed(
|
|
29773
30182
|
new Error(`prompt failed: ${err.message}`)
|
|
29774
30183
|
);
|
|
@@ -30098,7 +30507,7 @@ async function main() {
|
|
|
30098
30507
|
all: flags.all === true,
|
|
30099
30508
|
json: flags.json === true,
|
|
30100
30509
|
host: typeof flags.host === "string" ? flags.host : void 0,
|
|
30101
|
-
|
|
30510
|
+
includeNonInteractive: flags["include-non-interactive"] === true
|
|
30102
30511
|
});
|
|
30103
30512
|
return;
|
|
30104
30513
|
}
|
|
@@ -30262,6 +30671,10 @@ async function main() {
|
|
|
30262
30671
|
await runAgentsSync(positional[2]);
|
|
30263
30672
|
return;
|
|
30264
30673
|
}
|
|
30674
|
+
if (sub === "set") {
|
|
30675
|
+
await runAgentsSet(positional[2], positional[3]);
|
|
30676
|
+
return;
|
|
30677
|
+
}
|
|
30265
30678
|
if (sub === "log" || sub === "logs") {
|
|
30266
30679
|
const agIdx = argv.indexOf(subcommand);
|
|
30267
30680
|
const tail = argv.slice(agIdx + 2);
|
|
@@ -30467,10 +30880,10 @@ function printHelp() {
|
|
|
30467
30880
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
30468
30881
|
" hydra-acp daemon stop|restart",
|
|
30469
30882
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
30470
|
-
" hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-
|
|
30471
|
-
" List sessions (live + 20 most-recent cold; --all
|
|
30883
|
+
" hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-non-interactive]",
|
|
30884
|
+
" List sessions (live + 20 most-recent cold; --all lifts the cold cap AND surfaces non-interactive sessions; --json emits JSON for scripts).",
|
|
30472
30885
|
" --host filters by origin machine: 'local' (default) shows only sessions created here, 'all' shows everything, or pass a hostname (e.g. machine-b) to show only imports from that peer.",
|
|
30473
|
-
" --include-
|
|
30886
|
+
" --include-non-interactive surfaces ancillary (e.g. `hydra cat`) or never-prompted sessions while keeping the cold cap (a narrower --all).",
|
|
30474
30887
|
" hydra-acp session info <id> [--verbose] [--json] [--diff] [--fold] [--no-color] [--no-pager]",
|
|
30475
30888
|
" Aggregate one session: turn count, tool histogram, files touched, cost/duration, synopsis. --diff appends the session diff under the summary and pages the whole thing on a TTY (inherits --fold).",
|
|
30476
30889
|
" hydra-acp session diff <id> [--json] [--no-color] [--no-pager] [--fold]",
|
|
@@ -30498,6 +30911,7 @@ function printHelp() {
|
|
|
30498
30911
|
" hydra-acp agent [list] List agents in the cached registry",
|
|
30499
30912
|
" hydra-acp agent refresh Force a registry re-fetch",
|
|
30500
30913
|
" hydra-acp agent install <id> Pre-install <id> from the registry (else lazy on first session)",
|
|
30914
|
+
" hydra-acp agent set [<id>] [model] With no args, report the daemon's current default agent and its default model. With <id>, set <id> as the default agent (config.defaultAgent). With <id> and [model], set the per-agent default model (config.defaultModels[<id>]).",
|
|
30501
30915
|
" hydra-acp agent sync <id> Spawn <id> just long enough to ACP session/list it, then persist any sessions it remembers (across every cwd) as cold rows in `session list`",
|
|
30502
30916
|
" hydra-acp agent log <id> [-f] [-n N] Tail or follow an agent's spawn/stderr log",
|
|
30503
30917
|
" hydra-acp auth password [--force] Set the daemon's master password",
|