@hydra-acp/cli 0.1.15 → 0.1.16
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/dist/cli.js +1841 -1333
- package/dist/index.d.ts +1 -0
- package/dist/index.js +674 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -360,6 +360,12 @@ function extractHydraMeta(meta) {
|
|
|
360
360
|
if (typeof obj.currentMode === "string") {
|
|
361
361
|
out.currentMode = obj.currentMode;
|
|
362
362
|
}
|
|
363
|
+
if (obj.currentUsage) {
|
|
364
|
+
const parsed = SessionListUsage.safeParse(obj.currentUsage);
|
|
365
|
+
if (parsed.success) {
|
|
366
|
+
out.currentUsage = parsed.data;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
363
369
|
if (typeof obj.turnStartedAt === "number" && obj.turnStartedAt > 0) {
|
|
364
370
|
out.turnStartedAt = obj.turnStartedAt;
|
|
365
371
|
}
|
|
@@ -1204,21 +1210,126 @@ var init_session = __esm({
|
|
|
1204
1210
|
}
|
|
1205
1211
|
this.clients.set(client.clientId, client);
|
|
1206
1212
|
this.updatedAt = Date.now();
|
|
1207
|
-
if (historyPolicy === "none"
|
|
1213
|
+
if (historyPolicy === "none") {
|
|
1208
1214
|
return Promise.resolve({ entries: [], appliedPolicy: historyPolicy });
|
|
1209
1215
|
}
|
|
1216
|
+
if (historyPolicy === "pending_only") {
|
|
1217
|
+
return Promise.resolve({
|
|
1218
|
+
entries: this.buildStateSnapshotReplay(),
|
|
1219
|
+
appliedPolicy: historyPolicy
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1210
1222
|
return this.loadReplay(historyPolicy, opts);
|
|
1211
1223
|
}
|
|
1212
1224
|
async loadReplay(historyPolicy, opts) {
|
|
1213
1225
|
const all = await this.getHistorySnapshot();
|
|
1226
|
+
const state = this.buildStateSnapshotReplay();
|
|
1214
1227
|
if (historyPolicy === "after_message") {
|
|
1215
1228
|
const cutoff = opts.afterMessageId ? findMessageIdIndex(all, opts.afterMessageId) : -1;
|
|
1216
1229
|
if (cutoff < 0) {
|
|
1217
|
-
return { entries: all, appliedPolicy: "full" };
|
|
1230
|
+
return { entries: [...state, ...all], appliedPolicy: "full" };
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
entries: [...state, ...all.slice(cutoff + 1)],
|
|
1234
|
+
appliedPolicy: "after_message"
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
return { entries: [...state, ...all], appliedPolicy: "full" };
|
|
1238
|
+
}
|
|
1239
|
+
// Synthesizes one session/update notification per cached STATE_UPDATE_KIND
|
|
1240
|
+
// so an attaching client receives the current snapshot through the
|
|
1241
|
+
// standard ACP event channel. Without this, third-party clients would
|
|
1242
|
+
// never see current_model/mode/usage/info/commands on resume — they're
|
|
1243
|
+
// filtered out of recorded history (canonical state lives in meta.json
|
|
1244
|
+
// and is *also* surfaced via the attach response _meta, but third-party
|
|
1245
|
+
// clients can't read hydra's namespaced meta).
|
|
1246
|
+
buildStateSnapshotReplay() {
|
|
1247
|
+
const out = [];
|
|
1248
|
+
const sessionId = this.sessionId;
|
|
1249
|
+
const recordedAt = Date.now();
|
|
1250
|
+
if (this.title !== void 0 && this.title.length > 0) {
|
|
1251
|
+
out.push({
|
|
1252
|
+
method: "session/update",
|
|
1253
|
+
params: {
|
|
1254
|
+
sessionId,
|
|
1255
|
+
update: {
|
|
1256
|
+
sessionUpdate: "session_info_update",
|
|
1257
|
+
title: this.title
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
recordedAt
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
1264
|
+
out.push({
|
|
1265
|
+
method: "session/update",
|
|
1266
|
+
params: {
|
|
1267
|
+
sessionId,
|
|
1268
|
+
update: {
|
|
1269
|
+
sessionUpdate: "current_model_update",
|
|
1270
|
+
currentModel: this.currentModel
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
recordedAt
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (this.currentMode !== void 0 && this.currentMode.length > 0) {
|
|
1277
|
+
out.push({
|
|
1278
|
+
method: "session/update",
|
|
1279
|
+
params: {
|
|
1280
|
+
sessionId,
|
|
1281
|
+
update: {
|
|
1282
|
+
sessionUpdate: "current_mode_update",
|
|
1283
|
+
currentMode: this.currentMode
|
|
1284
|
+
}
|
|
1285
|
+
},
|
|
1286
|
+
recordedAt
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
const cmds = this.mergedAvailableCommands();
|
|
1290
|
+
if (cmds.length > 0) {
|
|
1291
|
+
out.push({
|
|
1292
|
+
method: "session/update",
|
|
1293
|
+
params: {
|
|
1294
|
+
sessionId,
|
|
1295
|
+
update: {
|
|
1296
|
+
sessionUpdate: "available_commands_update",
|
|
1297
|
+
availableCommands: cmds
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
recordedAt
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (this.currentUsage !== void 0) {
|
|
1304
|
+
const u = this.currentUsage;
|
|
1305
|
+
const update = {
|
|
1306
|
+
sessionUpdate: "usage_update"
|
|
1307
|
+
};
|
|
1308
|
+
if (typeof u.used === "number") {
|
|
1309
|
+
update.used = u.used;
|
|
1310
|
+
}
|
|
1311
|
+
if (typeof u.size === "number") {
|
|
1312
|
+
update.size = u.size;
|
|
1313
|
+
}
|
|
1314
|
+
if (typeof u.costAmount === "number" || typeof u.costCurrency === "string") {
|
|
1315
|
+
const cost = {};
|
|
1316
|
+
if (typeof u.costAmount === "number") {
|
|
1317
|
+
cost.amount = u.costAmount;
|
|
1318
|
+
}
|
|
1319
|
+
if (typeof u.costCurrency === "string") {
|
|
1320
|
+
cost.currency = u.costCurrency;
|
|
1321
|
+
}
|
|
1322
|
+
update.cost = cost;
|
|
1323
|
+
}
|
|
1324
|
+
if (Object.keys(update).length > 1) {
|
|
1325
|
+
out.push({
|
|
1326
|
+
method: "session/update",
|
|
1327
|
+
params: { sessionId, update },
|
|
1328
|
+
recordedAt
|
|
1329
|
+
});
|
|
1218
1330
|
}
|
|
1219
|
-
return { entries: all.slice(cutoff + 1), appliedPolicy: "after_message" };
|
|
1220
1331
|
}
|
|
1221
|
-
return
|
|
1332
|
+
return out;
|
|
1222
1333
|
}
|
|
1223
1334
|
// Dispatch in-flight permission requests to a freshly-attached
|
|
1224
1335
|
// client. Called by the daemon's WS handler *after* it finishes
|
|
@@ -2477,633 +2588,1252 @@ var init_bundle = __esm({
|
|
|
2477
2588
|
}
|
|
2478
2589
|
});
|
|
2479
2590
|
|
|
2480
|
-
// src/
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
let closed = false;
|
|
2485
|
-
const emitClose = (err) => {
|
|
2486
|
-
if (closed) {
|
|
2487
|
-
return;
|
|
2488
|
-
}
|
|
2489
|
-
closed = true;
|
|
2490
|
-
for (const handler of closeHandlers) {
|
|
2491
|
-
handler(err);
|
|
2492
|
-
}
|
|
2493
|
-
};
|
|
2494
|
-
ws.on("message", (data, isBinary) => {
|
|
2495
|
-
if (isBinary) {
|
|
2496
|
-
return;
|
|
2497
|
-
}
|
|
2498
|
-
const text = data.toString("utf8");
|
|
2499
|
-
try {
|
|
2500
|
-
const parsed = JSON.parse(text);
|
|
2501
|
-
for (const handler of messageHandlers) {
|
|
2502
|
-
handler(parsed);
|
|
2503
|
-
}
|
|
2504
|
-
} catch (err) {
|
|
2505
|
-
for (const handler of messageHandlers) {
|
|
2506
|
-
handler({
|
|
2507
|
-
jsonrpc: "2.0",
|
|
2508
|
-
id: 0,
|
|
2509
|
-
error: {
|
|
2510
|
-
code: JsonRpcErrorCodes.ParseError,
|
|
2511
|
-
message: `Failed to parse WS frame: ${err.message}`
|
|
2512
|
-
}
|
|
2513
|
-
});
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
});
|
|
2517
|
-
ws.on("close", () => emitClose());
|
|
2518
|
-
ws.on("error", (err) => emitClose(err));
|
|
2519
|
-
return {
|
|
2520
|
-
async send(message) {
|
|
2521
|
-
if (closed) {
|
|
2522
|
-
throw new Error("ws is closed");
|
|
2523
|
-
}
|
|
2524
|
-
const text = JSON.stringify(message);
|
|
2525
|
-
await new Promise((resolve5, reject) => {
|
|
2526
|
-
ws.send(text, (err) => {
|
|
2527
|
-
if (err) {
|
|
2528
|
-
reject(err);
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
resolve5();
|
|
2532
|
-
});
|
|
2533
|
-
});
|
|
2534
|
-
},
|
|
2535
|
-
onMessage(handler) {
|
|
2536
|
-
messageHandlers.push(handler);
|
|
2537
|
-
},
|
|
2538
|
-
onClose(handler) {
|
|
2539
|
-
closeHandlers.push(handler);
|
|
2540
|
-
},
|
|
2541
|
-
async close() {
|
|
2542
|
-
if (closed) {
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
|
-
ws.close();
|
|
2546
|
-
emitClose();
|
|
2547
|
-
}
|
|
2548
|
-
};
|
|
2591
|
+
// src/core/render-update.ts
|
|
2592
|
+
import stripAnsi from "strip-ansi";
|
|
2593
|
+
function sanitizeWireText(text) {
|
|
2594
|
+
return stripAnsi(text).replace(STRIP_CONTROLS, "");
|
|
2549
2595
|
}
|
|
2550
|
-
|
|
2551
|
-
"
|
|
2552
|
-
"use strict";
|
|
2553
|
-
init_types();
|
|
2554
|
-
}
|
|
2555
|
-
});
|
|
2556
|
-
|
|
2557
|
-
// src/core/daemon-bootstrap.ts
|
|
2558
|
-
import { spawn as spawn5 } from "child_process";
|
|
2559
|
-
import { setTimeout as sleep } from "timers/promises";
|
|
2560
|
-
async function ensureDaemonReachable(config) {
|
|
2561
|
-
if (await pingHealth(config)) {
|
|
2562
|
-
return;
|
|
2563
|
-
}
|
|
2564
|
-
process.stderr.write("hydra-acp: daemon not running; starting it...\n");
|
|
2565
|
-
spawnDaemonDetached();
|
|
2566
|
-
await waitForDaemonReady(config);
|
|
2596
|
+
function sanitizeSingleLine(text) {
|
|
2597
|
+
return sanitizeWireText(text).replace(/[\n\t]+/g, " ").replace(/ +/g, " ").trim();
|
|
2567
2598
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
try {
|
|
2572
|
-
const response = await fetch(url, {
|
|
2573
|
-
signal: AbortSignal.timeout(500)
|
|
2574
|
-
});
|
|
2575
|
-
return response.ok;
|
|
2576
|
-
} catch {
|
|
2577
|
-
return false;
|
|
2599
|
+
function mapUpdate(update) {
|
|
2600
|
+
if (!update || typeof update !== "object") {
|
|
2601
|
+
return null;
|
|
2578
2602
|
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
|
|
2603
|
+
const u = update;
|
|
2604
|
+
const tag = u.sessionUpdate ?? u.kind;
|
|
2605
|
+
if (typeof tag !== "string") {
|
|
2606
|
+
return null;
|
|
2584
2607
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
return;
|
|
2601
|
-
|
|
2602
|
-
|
|
2608
|
+
switch (tag) {
|
|
2609
|
+
case "agent_message_chunk":
|
|
2610
|
+
return mapAgentText(u);
|
|
2611
|
+
case "agent_thought_chunk":
|
|
2612
|
+
case "agent_thought":
|
|
2613
|
+
return mapAgentThought(u);
|
|
2614
|
+
case "user_message_chunk":
|
|
2615
|
+
return mapUserText(u);
|
|
2616
|
+
case "prompt_received":
|
|
2617
|
+
return mapPromptReceived(u);
|
|
2618
|
+
case "tool_call":
|
|
2619
|
+
return mapToolCall(u);
|
|
2620
|
+
case "tool_call_update":
|
|
2621
|
+
return mapToolCallUpdate(u);
|
|
2622
|
+
case "plan":
|
|
2623
|
+
return mapPlan(u);
|
|
2624
|
+
case "current_mode_update":
|
|
2625
|
+
return mapMode(u);
|
|
2626
|
+
case "current_model_update":
|
|
2627
|
+
return mapModel(u);
|
|
2628
|
+
case "turn_complete":
|
|
2629
|
+
return mapTurnComplete(u);
|
|
2630
|
+
case "usage_update":
|
|
2631
|
+
return mapUsage(u);
|
|
2632
|
+
case "available_commands_update":
|
|
2633
|
+
return mapAvailableCommands(u);
|
|
2634
|
+
case "session_info_update":
|
|
2635
|
+
return mapSessionInfo(u);
|
|
2636
|
+
default:
|
|
2637
|
+
return { kind: "unknown", sessionUpdate: tag, raw: update };
|
|
2603
2638
|
}
|
|
2604
|
-
throw new Error(
|
|
2605
|
-
`hydra-acp daemon did not become ready within ${timeoutMs}ms`
|
|
2606
|
-
);
|
|
2607
2639
|
}
|
|
2608
|
-
|
|
2609
|
-
"
|
|
2610
|
-
|
|
2640
|
+
function mapSessionInfo(u) {
|
|
2641
|
+
const rawTitle = readString(u, "title");
|
|
2642
|
+
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
2643
|
+
const meta = u._meta;
|
|
2644
|
+
let agentId;
|
|
2645
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
2646
|
+
const ns = meta["hydra-acp"];
|
|
2647
|
+
if (ns && typeof ns === "object" && !Array.isArray(ns)) {
|
|
2648
|
+
const candidate = ns.agentId;
|
|
2649
|
+
if (typeof candidate === "string") {
|
|
2650
|
+
agentId = candidate;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2611
2653
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
// src/core/agent-display.ts
|
|
2615
|
-
function shortenModel(model) {
|
|
2616
|
-
if (!model) {
|
|
2617
|
-
return void 0;
|
|
2654
|
+
if (title === void 0 && agentId === void 0) {
|
|
2655
|
+
return null;
|
|
2618
2656
|
}
|
|
2619
|
-
const
|
|
2620
|
-
if (
|
|
2621
|
-
|
|
2657
|
+
const event = { kind: "session-info" };
|
|
2658
|
+
if (title !== void 0) {
|
|
2659
|
+
event.title = title;
|
|
2622
2660
|
}
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
function formatAgentWithModel(agentId, model) {
|
|
2626
|
-
const agent = agentId ?? "?";
|
|
2627
|
-
const short = shortenModel(model);
|
|
2628
|
-
if (!short) {
|
|
2629
|
-
return agent;
|
|
2661
|
+
if (agentId !== void 0) {
|
|
2662
|
+
event.agentId = agentId;
|
|
2630
2663
|
}
|
|
2631
|
-
return
|
|
2664
|
+
return event;
|
|
2632
2665
|
}
|
|
2633
|
-
function
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
return base;
|
|
2637
|
-
}
|
|
2638
|
-
const compact = formatCostCompact(usage.costAmount, usage.costCurrency);
|
|
2639
|
-
if (compact === null) {
|
|
2640
|
-
return base;
|
|
2666
|
+
function normalizeAdvertisedCommands(list) {
|
|
2667
|
+
if (!Array.isArray(list)) {
|
|
2668
|
+
return [];
|
|
2641
2669
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2670
|
+
const out = [];
|
|
2671
|
+
for (const raw of list) {
|
|
2672
|
+
if (!raw || typeof raw !== "object") {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
const c = raw;
|
|
2676
|
+
if (typeof c.name !== "string" || c.name.length === 0) {
|
|
2677
|
+
continue;
|
|
2678
|
+
}
|
|
2679
|
+
const rawName = c.name.startsWith("/") ? c.name : `/${c.name}`;
|
|
2680
|
+
const cmd = { name: sanitizeSingleLine(rawName) };
|
|
2681
|
+
if (typeof c.description === "string") {
|
|
2682
|
+
cmd.description = sanitizeSingleLine(c.description);
|
|
2683
|
+
}
|
|
2684
|
+
out.push(cmd);
|
|
2685
|
+
}
|
|
2686
|
+
return out;
|
|
2648
2687
|
}
|
|
2649
|
-
function
|
|
2650
|
-
const
|
|
2651
|
-
if (
|
|
2688
|
+
function mapAvailableCommands(u) {
|
|
2689
|
+
const list = u.availableCommands ?? u.commands;
|
|
2690
|
+
if (!Array.isArray(list)) {
|
|
2652
2691
|
return null;
|
|
2653
2692
|
}
|
|
2654
|
-
|
|
2655
|
-
return `${sign}${whole}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
2693
|
+
return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
|
|
2656
2694
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
AGENT_MODEL_SEP = "\u2022";
|
|
2695
|
+
function mapUsage(u) {
|
|
2696
|
+
const event = { kind: "usage-update" };
|
|
2697
|
+
if (typeof u.used === "number") {
|
|
2698
|
+
event.used = u.used;
|
|
2662
2699
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
// src/cli/session-row.ts
|
|
2666
|
-
function toRow(s, now = Date.now()) {
|
|
2667
|
-
return {
|
|
2668
|
-
session: stripHydraSessionPrefix(s.sessionId),
|
|
2669
|
-
upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
|
|
2670
|
-
state: formatState(s.status, s.attachedClients),
|
|
2671
|
-
agent: formatAgentCell(s.agentId, s.currentUsage),
|
|
2672
|
-
age: formatRelativeAge(s.updatedAt, now),
|
|
2673
|
-
title: s.title ?? "-",
|
|
2674
|
-
cwd: shortenHomePath(s.cwd)
|
|
2675
|
-
};
|
|
2676
|
-
}
|
|
2677
|
-
function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
|
|
2678
|
-
if (upstreamSessionId && upstreamSessionId.length > 0) {
|
|
2679
|
-
return upstreamSessionId;
|
|
2700
|
+
if (typeof u.size === "number") {
|
|
2701
|
+
event.size = u.size;
|
|
2680
2702
|
}
|
|
2681
|
-
if (
|
|
2682
|
-
|
|
2703
|
+
if (u.cost && typeof u.cost === "object") {
|
|
2704
|
+
const cost = u.cost;
|
|
2705
|
+
if (typeof cost.amount === "number") {
|
|
2706
|
+
event.costAmount = cost.amount;
|
|
2707
|
+
}
|
|
2708
|
+
if (typeof cost.currency === "string") {
|
|
2709
|
+
event.costCurrency = cost.currency;
|
|
2710
|
+
}
|
|
2683
2711
|
}
|
|
2684
|
-
return
|
|
2712
|
+
return event;
|
|
2685
2713
|
}
|
|
2686
|
-
function
|
|
2687
|
-
|
|
2688
|
-
|
|
2714
|
+
function mapAgentText(u) {
|
|
2715
|
+
const text = extractContentText(u.content);
|
|
2716
|
+
if (text === null) {
|
|
2717
|
+
return null;
|
|
2689
2718
|
}
|
|
2690
|
-
return
|
|
2719
|
+
return { kind: "agent-text", text };
|
|
2691
2720
|
}
|
|
2692
|
-
function
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
2699
|
-
cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
|
|
2700
|
-
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
2701
|
-
};
|
|
2721
|
+
function mapAgentThought(u) {
|
|
2722
|
+
const text = typeof u.text === "string" ? sanitizeWireText(u.text) : extractContentText(u.content);
|
|
2723
|
+
if (text === null) {
|
|
2724
|
+
return null;
|
|
2725
|
+
}
|
|
2726
|
+
return { kind: "agent-thought", text };
|
|
2702
2727
|
}
|
|
2703
|
-
function
|
|
2704
|
-
|
|
2705
|
-
|
|
2728
|
+
function mapUserText(u) {
|
|
2729
|
+
const meta = u._meta;
|
|
2730
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
2731
|
+
const hydra = meta["hydra-acp"];
|
|
2732
|
+
if (hydra && typeof hydra === "object" && !Array.isArray(hydra) && hydra.compatFor === "prompt_received") {
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2706
2735
|
}
|
|
2707
|
-
const
|
|
2708
|
-
if (
|
|
2709
|
-
return
|
|
2736
|
+
const text = extractContentText(u.content);
|
|
2737
|
+
if (text === null) {
|
|
2738
|
+
return null;
|
|
2710
2739
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2740
|
+
return { kind: "user-text", text };
|
|
2741
|
+
}
|
|
2742
|
+
function mapPromptReceived(u) {
|
|
2743
|
+
const promptText = extractPromptText2(u.prompt);
|
|
2744
|
+
if (promptText === null) {
|
|
2745
|
+
return null;
|
|
2715
2746
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2747
|
+
return { kind: "user-text", text: promptText };
|
|
2748
|
+
}
|
|
2749
|
+
function mapToolCall(u) {
|
|
2750
|
+
const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
|
|
2751
|
+
if (!toolCallId) {
|
|
2752
|
+
return null;
|
|
2719
2753
|
}
|
|
2720
|
-
const
|
|
2721
|
-
|
|
2722
|
-
|
|
2754
|
+
const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
|
|
2755
|
+
const title = sanitizeSingleLine(rawTitle);
|
|
2756
|
+
const status = readString(u, "status");
|
|
2757
|
+
const rawKind = readString(u, "kind");
|
|
2758
|
+
const event = { kind: "tool-call", toolCallId, title };
|
|
2759
|
+
if (status !== void 0) {
|
|
2760
|
+
event.status = status;
|
|
2723
2761
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
return `${day}d`;
|
|
2762
|
+
if (rawKind !== void 0) {
|
|
2763
|
+
event.rawKind = rawKind;
|
|
2727
2764
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2765
|
+
return event;
|
|
2766
|
+
}
|
|
2767
|
+
function mapToolCallUpdate(u) {
|
|
2768
|
+
const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
|
|
2769
|
+
if (!toolCallId) {
|
|
2770
|
+
return null;
|
|
2731
2771
|
}
|
|
2732
|
-
const
|
|
2733
|
-
|
|
2734
|
-
|
|
2772
|
+
const rawTitle = readString(u, "title");
|
|
2773
|
+
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
2774
|
+
const status = readString(u, "status");
|
|
2775
|
+
const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
|
|
2776
|
+
if (!meaningful) {
|
|
2777
|
+
return null;
|
|
2735
2778
|
}
|
|
2736
|
-
const
|
|
2737
|
-
|
|
2779
|
+
const event = { kind: "tool-call-update", toolCallId };
|
|
2780
|
+
if (title !== void 0) {
|
|
2781
|
+
event.title = title;
|
|
2782
|
+
}
|
|
2783
|
+
if (status !== void 0) {
|
|
2784
|
+
event.status = status;
|
|
2785
|
+
}
|
|
2786
|
+
return event;
|
|
2738
2787
|
}
|
|
2739
|
-
function
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2788
|
+
function mapPlan(u) {
|
|
2789
|
+
const entries = u.entries;
|
|
2790
|
+
if (!Array.isArray(entries)) {
|
|
2791
|
+
return null;
|
|
2792
|
+
}
|
|
2793
|
+
const normalized = [];
|
|
2794
|
+
for (const raw of entries) {
|
|
2795
|
+
if (!raw || typeof raw !== "object") {
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2798
|
+
const e = raw;
|
|
2799
|
+
const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
|
|
2800
|
+
if (!content) {
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
const entry = { content };
|
|
2804
|
+
if (typeof e.status === "string") {
|
|
2805
|
+
entry.status = e.status;
|
|
2806
|
+
}
|
|
2807
|
+
if (typeof e.priority === "string") {
|
|
2808
|
+
entry.priority = e.priority;
|
|
2744
2809
|
}
|
|
2810
|
+
normalized.push(entry);
|
|
2745
2811
|
}
|
|
2746
|
-
return
|
|
2812
|
+
return { kind: "plan", entries: normalized };
|
|
2747
2813
|
}
|
|
2748
|
-
function
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
r.state.padEnd(w.state),
|
|
2753
|
-
r.agent.padEnd(w.agent),
|
|
2754
|
-
r.age.padStart(w.age)
|
|
2755
|
-
].join(SEP);
|
|
2756
|
-
if (maxWidth === void 0) {
|
|
2757
|
-
return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
|
|
2814
|
+
function mapMode(u) {
|
|
2815
|
+
const mode = readString(u, "currentMode") ?? readString(u, "mode");
|
|
2816
|
+
if (!mode) {
|
|
2817
|
+
return null;
|
|
2758
2818
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2819
|
+
return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
|
|
2820
|
+
}
|
|
2821
|
+
function mapModel(u) {
|
|
2822
|
+
const model = readString(u, "currentModel") ?? readString(u, "model");
|
|
2823
|
+
if (!model) {
|
|
2824
|
+
return null;
|
|
2762
2825
|
}
|
|
2763
|
-
|
|
2764
|
-
const cwdAlloc = Math.min(cwdCap, Math.max(0, budget - SEP.length - 1));
|
|
2765
|
-
const cwdCell = truncateMiddle(r.cwd, cwdAlloc).padEnd(cwdAlloc);
|
|
2766
|
-
const titleBudget = Math.max(0, budget - cwdAlloc - SEP.length);
|
|
2767
|
-
const titleCell = truncateRight(r.title, titleBudget);
|
|
2768
|
-
return [fixed, cwdCell, titleCell].join(SEP);
|
|
2826
|
+
return { kind: "model-changed", model: sanitizeSingleLine(model) };
|
|
2769
2827
|
}
|
|
2770
|
-
function
|
|
2771
|
-
|
|
2772
|
-
|
|
2828
|
+
function mapTurnComplete(u) {
|
|
2829
|
+
const stopReason = readString(u, "stopReason");
|
|
2830
|
+
return stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" };
|
|
2831
|
+
}
|
|
2832
|
+
function extractContentText(content) {
|
|
2833
|
+
if (typeof content === "string") {
|
|
2834
|
+
return sanitizeWireText(content);
|
|
2773
2835
|
}
|
|
2774
|
-
if (
|
|
2775
|
-
return
|
|
2836
|
+
if (!content || typeof content !== "object") {
|
|
2837
|
+
return null;
|
|
2776
2838
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2839
|
+
const c = content;
|
|
2840
|
+
if (c.type === "text" && typeof c.text === "string") {
|
|
2841
|
+
return sanitizeWireText(c.text);
|
|
2779
2842
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
function truncateMiddle(s, max) {
|
|
2783
|
-
if (max <= 0) {
|
|
2784
|
-
return "";
|
|
2843
|
+
if (typeof c.text === "string") {
|
|
2844
|
+
return sanitizeWireText(c.text);
|
|
2785
2845
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2846
|
+
return null;
|
|
2847
|
+
}
|
|
2848
|
+
function extractPromptText2(prompt) {
|
|
2849
|
+
if (!Array.isArray(prompt)) {
|
|
2850
|
+
return null;
|
|
2788
2851
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
2852
|
+
const parts = [];
|
|
2853
|
+
for (const block of prompt) {
|
|
2854
|
+
const text = extractContentText(block);
|
|
2855
|
+
if (text !== null) {
|
|
2856
|
+
parts.push(text);
|
|
2857
|
+
}
|
|
2791
2858
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2859
|
+
if (parts.length === 0) {
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2862
|
+
return parts.join("");
|
|
2795
2863
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
"
|
|
2864
|
+
function readString(u, key) {
|
|
2865
|
+
const v = u[key];
|
|
2866
|
+
return typeof v === "string" ? v : void 0;
|
|
2867
|
+
}
|
|
2868
|
+
var STRIP_CONTROLS;
|
|
2869
|
+
var init_render_update = __esm({
|
|
2870
|
+
"src/core/render-update.ts"() {
|
|
2799
2871
|
"use strict";
|
|
2800
|
-
|
|
2801
|
-
init_paths();
|
|
2802
|
-
init_session();
|
|
2803
|
-
HEADER = {
|
|
2804
|
-
session: "SESSION",
|
|
2805
|
-
upstream: "UPSTREAM",
|
|
2806
|
-
state: "STATE",
|
|
2807
|
-
agent: "AGENT",
|
|
2808
|
-
age: "AGE",
|
|
2809
|
-
title: "TITLE",
|
|
2810
|
-
cwd: "CWD"
|
|
2811
|
-
};
|
|
2812
|
-
SEP = " ";
|
|
2813
|
-
DEFAULT_CWD_MAX_WIDTH = 24;
|
|
2872
|
+
STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
2814
2873
|
}
|
|
2815
2874
|
});
|
|
2816
2875
|
|
|
2817
|
-
// src/
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
const
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
if (!response.ok) {
|
|
2828
|
-
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
2829
|
-
`);
|
|
2830
|
-
process.exit(1);
|
|
2831
|
-
}
|
|
2832
|
-
const body = await response.json();
|
|
2833
|
-
if (body.sessions.length === 0) {
|
|
2834
|
-
process.stdout.write("No active sessions.\n");
|
|
2835
|
-
return;
|
|
2836
|
-
}
|
|
2837
|
-
const sorted = body.sessions.slice().sort((a, b) => {
|
|
2838
|
-
const liveDiff = (b.status === "live" ? 1 : 0) - (a.status === "live" ? 1 : 0);
|
|
2839
|
-
if (liveDiff !== 0) {
|
|
2840
|
-
return liveDiff;
|
|
2841
|
-
}
|
|
2842
|
-
return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
|
|
2843
|
-
});
|
|
2844
|
-
let visible = sorted;
|
|
2845
|
-
let truncated = 0;
|
|
2846
|
-
if (!opts.all) {
|
|
2847
|
-
const liveCount = sorted.filter((s) => s.status !== "cold").length;
|
|
2848
|
-
const limit = config.sessionListColdLimit;
|
|
2849
|
-
const coldSlice = sorted.slice(liveCount, liveCount + limit);
|
|
2850
|
-
const hiddenCold = sorted.length - liveCount - coldSlice.length;
|
|
2851
|
-
visible = [...sorted.slice(0, liveCount), ...coldSlice];
|
|
2852
|
-
truncated = hiddenCold;
|
|
2853
|
-
}
|
|
2854
|
-
const now = Date.now();
|
|
2855
|
-
const rows = visible.map((s) => toRow(s, now));
|
|
2856
|
-
const widths = computeWidths(rows);
|
|
2857
|
-
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2858
|
-
const cwdMax = config.tui.cwdColumnMaxWidth;
|
|
2859
|
-
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
|
|
2860
|
-
for (const r of rows) {
|
|
2861
|
-
process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
|
|
2862
|
-
}
|
|
2863
|
-
if (truncated > 0) {
|
|
2864
|
-
process.stdout.write(
|
|
2865
|
-
`
|
|
2866
|
-
... ${truncated} more cold session${truncated === 1 ? "" : "s"} hidden. Use --all to show.
|
|
2867
|
-
`
|
|
2868
|
-
);
|
|
2876
|
+
// src/core/transcript.ts
|
|
2877
|
+
function bundleToMarkdown(bundle) {
|
|
2878
|
+
const events = collectEvents(bundle);
|
|
2879
|
+
const toolFinalStates = collectToolFinalStates(events);
|
|
2880
|
+
const out = [];
|
|
2881
|
+
emitHeader(out, bundle);
|
|
2882
|
+
emitBody(out, events, toolFinalStates);
|
|
2883
|
+
let text = out.join("\n");
|
|
2884
|
+
if (!text.endsWith("\n")) {
|
|
2885
|
+
text += "\n";
|
|
2869
2886
|
}
|
|
2887
|
+
return text;
|
|
2870
2888
|
}
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2889
|
+
function collectEvents(bundle) {
|
|
2890
|
+
const out = [];
|
|
2891
|
+
for (const entry of bundle.history) {
|
|
2892
|
+
if (entry.method !== "session/update") {
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
const params = entry.params;
|
|
2896
|
+
if (!params || typeof params !== "object") {
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
const event = mapUpdate(params.update);
|
|
2900
|
+
if (event === null) {
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
out.push({ event, recordedAt: entry.recordedAt });
|
|
2886
2904
|
}
|
|
2887
|
-
|
|
2888
|
-
`);
|
|
2905
|
+
return out;
|
|
2889
2906
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2907
|
+
function collectToolFinalStates(events) {
|
|
2908
|
+
const out = /* @__PURE__ */ new Map();
|
|
2909
|
+
for (const { event } of events) {
|
|
2910
|
+
if (event.kind === "tool-call") {
|
|
2911
|
+
const existing = out.get(event.toolCallId);
|
|
2912
|
+
out.set(event.toolCallId, {
|
|
2913
|
+
title: event.title,
|
|
2914
|
+
status: event.status ?? existing?.status ?? "pending"
|
|
2915
|
+
});
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
if (event.kind === "tool-call-update") {
|
|
2919
|
+
const existing = out.get(event.toolCallId) ?? {
|
|
2920
|
+
title: "tool call",
|
|
2921
|
+
status: "pending"
|
|
2922
|
+
};
|
|
2923
|
+
out.set(event.toolCallId, {
|
|
2924
|
+
title: event.title ?? existing.title,
|
|
2925
|
+
status: event.status ?? existing.status
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2905
2928
|
}
|
|
2906
|
-
|
|
2907
|
-
`);
|
|
2929
|
+
return out;
|
|
2908
2930
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
}
|
|
2931
|
+
function emitHeader(out, bundle) {
|
|
2932
|
+
const session = bundle.session;
|
|
2933
|
+
const shortId2 = stripHydraSessionPrefix(session.sessionId);
|
|
2934
|
+
const title = session.title?.trim() || `Hydra session ${shortId2}`;
|
|
2935
|
+
out.push(`# ${escapeInline(title)}`);
|
|
2936
|
+
out.push("");
|
|
2937
|
+
const lines = [];
|
|
2938
|
+
lines.push(`- **Session:** \`${shortId2}\` (lineage \`${session.lineageId}\`)`);
|
|
2939
|
+
const agentBits = [session.agentId];
|
|
2940
|
+
if (session.currentModel) {
|
|
2941
|
+
agentBits.push(`model: ${session.currentModel}`);
|
|
2942
|
+
}
|
|
2943
|
+
if (session.currentMode) {
|
|
2944
|
+
agentBits.push(`mode: ${session.currentMode}`);
|
|
2945
|
+
}
|
|
2946
|
+
lines.push(`- **Agent:** ${agentBits.filter(Boolean).join(" \xB7 ")}`);
|
|
2947
|
+
lines.push(`- **Cwd:** ${session.cwd}`);
|
|
2948
|
+
lines.push(
|
|
2949
|
+
`- **Exported:** ${bundle.exportedAt} from ${bundle.exportedFrom.machine} (hydra ${bundle.exportedFrom.hydraVersion})`
|
|
2923
2950
|
);
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
process.stdout.write(body);
|
|
2933
|
-
if (!body.endsWith("\n")) {
|
|
2934
|
-
process.stdout.write("\n");
|
|
2951
|
+
const usage = session.currentUsage;
|
|
2952
|
+
if (usage && (usage.used !== void 0 || usage.costAmount !== void 0)) {
|
|
2953
|
+
const usageBits = [];
|
|
2954
|
+
if (usage.used !== void 0) {
|
|
2955
|
+
const denom = usage.size !== void 0 ? `${formatNumber(usage.size)}` : void 0;
|
|
2956
|
+
usageBits.push(
|
|
2957
|
+
denom ? `${formatNumber(usage.used)} / ${denom} tokens` : `${formatNumber(usage.used)} tokens`
|
|
2958
|
+
);
|
|
2935
2959
|
}
|
|
2936
|
-
|
|
2960
|
+
if (usage.costAmount !== void 0) {
|
|
2961
|
+
const currency = usage.costCurrency ?? "USD";
|
|
2962
|
+
usageBits.push(`$${usage.costAmount.toFixed(2)} ${currency}`);
|
|
2963
|
+
}
|
|
2964
|
+
lines.push(`- **Usage:** ${usageBits.join(" \xB7 ")}`);
|
|
2937
2965
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2941
|
-
process.stdout.write(`Wrote ${resolved}
|
|
2942
|
-
`);
|
|
2966
|
+
out.push(lines.join("\n"));
|
|
2967
|
+
out.push("");
|
|
2943
2968
|
}
|
|
2944
|
-
|
|
2945
|
-
if (!
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
process.exit(2);
|
|
2969
|
+
function emitBody(out, events, toolFinalStates) {
|
|
2970
|
+
if (!events.some((e) => isVisible(e.event))) {
|
|
2971
|
+
out.push("_No conversation history recorded._");
|
|
2972
|
+
out.push("");
|
|
2973
|
+
return;
|
|
2950
2974
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2975
|
+
const seenToolIds = /* @__PURE__ */ new Set();
|
|
2976
|
+
let turn = 0;
|
|
2977
|
+
let agentBuffer = "";
|
|
2978
|
+
let inTurn = false;
|
|
2979
|
+
const flushAgent = () => {
|
|
2980
|
+
if (agentBuffer.length === 0) {
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
out.push(agentBuffer.trimEnd());
|
|
2984
|
+
out.push("");
|
|
2985
|
+
agentBuffer = "";
|
|
2986
|
+
};
|
|
2987
|
+
const startTurnIfNeeded = () => {
|
|
2988
|
+
if (inTurn) {
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
turn += 1;
|
|
2992
|
+
out.push("---");
|
|
2993
|
+
out.push("");
|
|
2994
|
+
out.push(`## Turn ${turn}`);
|
|
2995
|
+
out.push("");
|
|
2996
|
+
inTurn = true;
|
|
2997
|
+
};
|
|
2998
|
+
for (const { event } of events) {
|
|
2999
|
+
switch (event.kind) {
|
|
3000
|
+
case "user-text": {
|
|
3001
|
+
flushAgent();
|
|
3002
|
+
turn += 1;
|
|
3003
|
+
out.push("---");
|
|
3004
|
+
out.push("");
|
|
3005
|
+
out.push(`## Turn ${turn}`);
|
|
3006
|
+
out.push("");
|
|
3007
|
+
out.push("**User:**");
|
|
3008
|
+
out.push("");
|
|
3009
|
+
for (const line of event.text.split("\n")) {
|
|
3010
|
+
out.push(`> ${escapeInline(line)}`);
|
|
3011
|
+
}
|
|
3012
|
+
out.push("");
|
|
3013
|
+
out.push("**Assistant:**");
|
|
3014
|
+
out.push("");
|
|
3015
|
+
inTurn = true;
|
|
3016
|
+
break;
|
|
2960
3017
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3018
|
+
case "agent-text":
|
|
3019
|
+
startTurnIfNeeded();
|
|
3020
|
+
agentBuffer += event.text;
|
|
3021
|
+
break;
|
|
3022
|
+
case "agent-thought": {
|
|
3023
|
+
startTurnIfNeeded();
|
|
3024
|
+
flushAgent();
|
|
3025
|
+
const lines = event.text.split("\n");
|
|
3026
|
+
for (const line of lines) {
|
|
3027
|
+
out.push(`> _${escapeInline(line)}_`);
|
|
3028
|
+
}
|
|
3029
|
+
out.push("");
|
|
3030
|
+
break;
|
|
3031
|
+
}
|
|
3032
|
+
case "tool-call": {
|
|
3033
|
+
startTurnIfNeeded();
|
|
3034
|
+
flushAgent();
|
|
3035
|
+
if (seenToolIds.has(event.toolCallId)) {
|
|
3036
|
+
break;
|
|
3037
|
+
}
|
|
3038
|
+
seenToolIds.add(event.toolCallId);
|
|
3039
|
+
const final = toolFinalStates.get(event.toolCallId) ?? {
|
|
3040
|
+
title: event.title,
|
|
3041
|
+
status: event.status ?? "pending"
|
|
3042
|
+
};
|
|
3043
|
+
out.push(`- ${statusGlyph(final.status)} ${formatToolLine(final)}`);
|
|
3044
|
+
out.push("");
|
|
3045
|
+
break;
|
|
3046
|
+
}
|
|
3047
|
+
case "tool-call-update":
|
|
3048
|
+
break;
|
|
3049
|
+
case "plan": {
|
|
3050
|
+
startTurnIfNeeded();
|
|
3051
|
+
flushAgent();
|
|
3052
|
+
out.push("**Plan:**");
|
|
3053
|
+
out.push("");
|
|
3054
|
+
for (const entry of event.entries) {
|
|
3055
|
+
const checked = entry.status === "completed" ? "[x]" : "[ ]";
|
|
3056
|
+
out.push(`- ${checked} ${escapeInline(entry.content)}`);
|
|
3057
|
+
}
|
|
3058
|
+
out.push("");
|
|
3059
|
+
break;
|
|
3060
|
+
}
|
|
3061
|
+
case "mode-changed":
|
|
3062
|
+
startTurnIfNeeded();
|
|
3063
|
+
flushAgent();
|
|
3064
|
+
out.push(`_mode: ${escapeInline(event.mode)}_`);
|
|
3065
|
+
out.push("");
|
|
3066
|
+
break;
|
|
3067
|
+
case "model-changed":
|
|
3068
|
+
startTurnIfNeeded();
|
|
3069
|
+
flushAgent();
|
|
3070
|
+
out.push(`_model: ${escapeInline(event.model)}_`);
|
|
3071
|
+
out.push("");
|
|
3072
|
+
break;
|
|
3073
|
+
case "turn-complete":
|
|
3074
|
+
flushAgent();
|
|
3075
|
+
break;
|
|
3076
|
+
case "usage-update":
|
|
3077
|
+
case "available-commands":
|
|
3078
|
+
case "session-info":
|
|
3079
|
+
case "unknown":
|
|
3080
|
+
break;
|
|
2965
3081
|
}
|
|
2966
|
-
cwdOverride = resolved;
|
|
2967
3082
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3083
|
+
flushAgent();
|
|
3084
|
+
}
|
|
3085
|
+
function isVisible(event) {
|
|
3086
|
+
switch (event.kind) {
|
|
3087
|
+
case "usage-update":
|
|
3088
|
+
case "available-commands":
|
|
3089
|
+
case "session-info":
|
|
3090
|
+
case "unknown":
|
|
3091
|
+
case "turn-complete":
|
|
3092
|
+
return false;
|
|
3093
|
+
default:
|
|
3094
|
+
return true;
|
|
2973
3095
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3096
|
+
}
|
|
3097
|
+
function formatToolLine(state) {
|
|
3098
|
+
const status = state.status;
|
|
3099
|
+
const suffix = status === "completed" || status === void 0 ? "" : ` _(${status})_`;
|
|
3100
|
+
return `${escapeInline(state.title)}${suffix}`;
|
|
3101
|
+
}
|
|
3102
|
+
function statusGlyph(status) {
|
|
3103
|
+
switch (status) {
|
|
3104
|
+
case "completed":
|
|
3105
|
+
return "\u2713";
|
|
3106
|
+
case "failed":
|
|
3107
|
+
return "\u2717";
|
|
3108
|
+
case "cancelled":
|
|
3109
|
+
case "rejected":
|
|
3110
|
+
return "\u2298";
|
|
3111
|
+
case "in_progress":
|
|
3112
|
+
return "\u21BB";
|
|
3113
|
+
default:
|
|
3114
|
+
return "\xB7";
|
|
2981
3115
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3116
|
+
}
|
|
3117
|
+
function escapeInline(text) {
|
|
3118
|
+
return text.replace(/</g, "<").replace(/>/g, ">");
|
|
3119
|
+
}
|
|
3120
|
+
function formatNumber(n) {
|
|
3121
|
+
return n.toLocaleString("en-US");
|
|
3122
|
+
}
|
|
3123
|
+
var init_transcript = __esm({
|
|
3124
|
+
"src/core/transcript.ts"() {
|
|
3125
|
+
"use strict";
|
|
3126
|
+
init_render_update();
|
|
3127
|
+
init_session();
|
|
2986
3128
|
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3129
|
+
});
|
|
3130
|
+
|
|
3131
|
+
// src/acp/ws-stream.ts
|
|
3132
|
+
function wsToMessageStream(ws) {
|
|
3133
|
+
const messageHandlers = [];
|
|
3134
|
+
const closeHandlers = [];
|
|
3135
|
+
let closed = false;
|
|
3136
|
+
const emitClose = (err) => {
|
|
3137
|
+
if (closed) {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
closed = true;
|
|
3141
|
+
for (const handler of closeHandlers) {
|
|
3142
|
+
handler(err);
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
3145
|
+
ws.on("message", (data, isBinary) => {
|
|
3146
|
+
if (isBinary) {
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
const text = data.toString("utf8");
|
|
3150
|
+
try {
|
|
3151
|
+
const parsed = JSON.parse(text);
|
|
3152
|
+
for (const handler of messageHandlers) {
|
|
3153
|
+
handler(parsed);
|
|
3154
|
+
}
|
|
3155
|
+
} catch (err) {
|
|
3156
|
+
for (const handler of messageHandlers) {
|
|
3157
|
+
handler({
|
|
3158
|
+
jsonrpc: "2.0",
|
|
3159
|
+
id: 0,
|
|
3160
|
+
error: {
|
|
3161
|
+
code: JsonRpcErrorCodes.ParseError,
|
|
3162
|
+
message: `Failed to parse WS frame: ${err.message}`
|
|
3163
|
+
}
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3000
3167
|
});
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
process.stderr.write(
|
|
3004
|
-
`Bundle already imported as ${detail.existingSessionId ?? "unknown"}. Use --replace to overwrite.
|
|
3005
|
-
`
|
|
3006
|
-
);
|
|
3007
|
-
process.exit(1);
|
|
3008
|
-
}
|
|
3009
|
-
if (!response.ok) {
|
|
3010
|
-
const text = await response.text().catch(() => "");
|
|
3011
|
-
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
3012
|
-
`);
|
|
3013
|
-
process.exit(1);
|
|
3014
|
-
}
|
|
3015
|
-
const result = await response.json();
|
|
3016
|
-
process.stdout.write(
|
|
3017
|
-
result.replaced ? `Replaced ${result.sessionId} (from ${result.importedFromSessionId})
|
|
3018
|
-
` : `Imported as ${result.sessionId} (from ${result.importedFromSessionId})
|
|
3019
|
-
`
|
|
3020
|
-
);
|
|
3021
|
-
}
|
|
3022
|
-
function bundleToSummary(parsed) {
|
|
3168
|
+
ws.on("close", () => emitClose());
|
|
3169
|
+
ws.on("error", (err) => emitClose(err));
|
|
3023
3170
|
return {
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3171
|
+
async send(message) {
|
|
3172
|
+
if (closed) {
|
|
3173
|
+
throw new Error("ws is closed");
|
|
3174
|
+
}
|
|
3175
|
+
const text = JSON.stringify(message);
|
|
3176
|
+
await new Promise((resolve5, reject) => {
|
|
3177
|
+
ws.send(text, (err) => {
|
|
3178
|
+
if (err) {
|
|
3179
|
+
reject(err);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
resolve5();
|
|
3183
|
+
});
|
|
3184
|
+
});
|
|
3185
|
+
},
|
|
3186
|
+
onMessage(handler) {
|
|
3187
|
+
messageHandlers.push(handler);
|
|
3188
|
+
},
|
|
3189
|
+
onClose(handler) {
|
|
3190
|
+
closeHandlers.push(handler);
|
|
3191
|
+
},
|
|
3192
|
+
async close() {
|
|
3193
|
+
if (closed) {
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
ws.close();
|
|
3197
|
+
emitClose();
|
|
3198
|
+
}
|
|
3033
3199
|
};
|
|
3034
3200
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3201
|
+
var init_ws_stream = __esm({
|
|
3202
|
+
"src/acp/ws-stream.ts"() {
|
|
3203
|
+
"use strict";
|
|
3204
|
+
init_types();
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
|
|
3208
|
+
// src/core/daemon-bootstrap.ts
|
|
3209
|
+
import { spawn as spawn5 } from "child_process";
|
|
3210
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
3211
|
+
async function ensureDaemonReachable(config) {
|
|
3212
|
+
if (await pingHealth(config)) {
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
process.stderr.write("hydra-acp: daemon not running; starting it...\n");
|
|
3216
|
+
spawnDaemonDetached();
|
|
3217
|
+
await waitForDaemonReady(config);
|
|
3218
|
+
}
|
|
3219
|
+
async function pingHealth(config) {
|
|
3220
|
+
const protocol = config.daemon.tls ? "https" : "http";
|
|
3221
|
+
const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/v1/health`;
|
|
3037
3222
|
try {
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3223
|
+
const response = await fetch(url, {
|
|
3224
|
+
signal: AbortSignal.timeout(500)
|
|
3225
|
+
});
|
|
3226
|
+
return response.ok;
|
|
3227
|
+
} catch {
|
|
3228
|
+
return false;
|
|
3043
3229
|
}
|
|
3044
|
-
const summary = bundleToSummary(parsed);
|
|
3045
|
-
const row = toRow(summary);
|
|
3046
|
-
const widths = computeWidths([row]);
|
|
3047
|
-
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
3048
|
-
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
3049
|
-
process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
3050
|
-
const originUpstream = parsed.session.upstreamSessionId ?? "-";
|
|
3051
|
-
process.stdout.write(
|
|
3052
|
-
`
|
|
3053
|
-
lineage: ${parsed.session.lineageId}
|
|
3054
|
-
exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
|
|
3055
|
-
origin session: ${parsed.session.sessionId}
|
|
3056
|
-
origin upstream: ${originUpstream}
|
|
3057
|
-
history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
|
|
3058
|
-
` : "\n")
|
|
3059
|
-
);
|
|
3060
3230
|
}
|
|
3061
|
-
|
|
3062
|
-
const
|
|
3063
|
-
|
|
3064
|
-
|
|
3231
|
+
function spawnDaemonDetached() {
|
|
3232
|
+
const cliPath = process.argv[1];
|
|
3233
|
+
if (!cliPath) {
|
|
3234
|
+
throw new Error("Cannot determine hydra-acp binary path to spawn daemon");
|
|
3065
3235
|
}
|
|
3066
|
-
|
|
3236
|
+
const child = spawn5(
|
|
3237
|
+
process.execPath,
|
|
3238
|
+
[cliPath, "daemon", "start", "--foreground"],
|
|
3239
|
+
{
|
|
3240
|
+
detached: true,
|
|
3241
|
+
stdio: "ignore",
|
|
3242
|
+
env: process.env
|
|
3243
|
+
}
|
|
3244
|
+
);
|
|
3245
|
+
child.unref();
|
|
3067
3246
|
}
|
|
3068
|
-
function
|
|
3069
|
-
const
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
return match[1];
|
|
3247
|
+
async function waitForDaemonReady(config, timeoutMs = 15e3) {
|
|
3248
|
+
const deadline = Date.now() + timeoutMs;
|
|
3249
|
+
while (Date.now() < deadline) {
|
|
3250
|
+
if (await pingHealth(config)) {
|
|
3251
|
+
return;
|
|
3074
3252
|
}
|
|
3253
|
+
await sleep(150);
|
|
3075
3254
|
}
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
function httpBase(host, port, tls) {
|
|
3080
|
-
const protocol = tls ? "https" : "http";
|
|
3081
|
-
return `${protocol}://${host}:${port}`;
|
|
3255
|
+
throw new Error(
|
|
3256
|
+
`hydra-acp daemon did not become ready within ${timeoutMs}ms`
|
|
3257
|
+
);
|
|
3082
3258
|
}
|
|
3083
|
-
var
|
|
3084
|
-
"src/
|
|
3259
|
+
var init_daemon_bootstrap = __esm({
|
|
3260
|
+
"src/core/daemon-bootstrap.ts"() {
|
|
3085
3261
|
"use strict";
|
|
3086
|
-
init_config();
|
|
3087
|
-
init_bundle();
|
|
3088
|
-
init_session_row();
|
|
3089
3262
|
}
|
|
3090
3263
|
});
|
|
3091
3264
|
|
|
3092
|
-
// src/
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3265
|
+
// src/core/agent-display.ts
|
|
3266
|
+
function shortenModel(model) {
|
|
3267
|
+
if (!model) {
|
|
3268
|
+
return void 0;
|
|
3269
|
+
}
|
|
3270
|
+
const idx = model.lastIndexOf("/");
|
|
3271
|
+
if (idx === -1) {
|
|
3272
|
+
return model;
|
|
3273
|
+
}
|
|
3274
|
+
return model.slice(idx + 1);
|
|
3097
3275
|
}
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3276
|
+
function formatAgentWithModel(agentId, model) {
|
|
3277
|
+
const agent = agentId ?? "?";
|
|
3278
|
+
const short = shortenModel(model);
|
|
3279
|
+
if (!short) {
|
|
3280
|
+
return agent;
|
|
3281
|
+
}
|
|
3282
|
+
return `${agent}${AGENT_MODEL_SEP}${short}`;
|
|
3283
|
+
}
|
|
3284
|
+
function formatAgentCell(agentId, usage) {
|
|
3285
|
+
const base = agentId ?? "?";
|
|
3286
|
+
if (!usage || typeof usage.costAmount !== "number") {
|
|
3287
|
+
return base;
|
|
3288
|
+
}
|
|
3289
|
+
const compact = formatCostCompact(usage.costAmount, usage.costCurrency);
|
|
3290
|
+
if (compact === null) {
|
|
3291
|
+
return base;
|
|
3292
|
+
}
|
|
3293
|
+
return `${base} ${compact}`;
|
|
3294
|
+
}
|
|
3295
|
+
function formatCost(amount, currency) {
|
|
3296
|
+
const sign = currency === "USD" || currency === void 0 ? "$" : "";
|
|
3297
|
+
const decimals = amount >= 1 ? 2 : 4;
|
|
3298
|
+
return `${sign}${amount.toFixed(decimals)}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
3299
|
+
}
|
|
3300
|
+
function formatCostCompact(amount, currency) {
|
|
3301
|
+
const whole = Math.round(amount);
|
|
3302
|
+
if (whole === 0) {
|
|
3303
|
+
return null;
|
|
3304
|
+
}
|
|
3305
|
+
const sign = currency === "USD" || currency === void 0 ? "$" : "";
|
|
3306
|
+
return `${sign}${whole}${currency && currency !== "USD" ? ` ${currency}` : ""}`;
|
|
3307
|
+
}
|
|
3308
|
+
var AGENT_MODEL_SEP;
|
|
3309
|
+
var init_agent_display = __esm({
|
|
3310
|
+
"src/core/agent-display.ts"() {
|
|
3311
|
+
"use strict";
|
|
3312
|
+
AGENT_MODEL_SEP = "\u2022";
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3316
|
+
// src/cli/session-row.ts
|
|
3317
|
+
function toRow(s, now = Date.now()) {
|
|
3318
|
+
return {
|
|
3319
|
+
session: stripHydraSessionPrefix(s.sessionId),
|
|
3320
|
+
upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
|
|
3321
|
+
state: formatState(s.status, s.attachedClients),
|
|
3322
|
+
agent: formatAgentCell(s.agentId, s.currentUsage),
|
|
3323
|
+
age: formatRelativeAge(s.updatedAt, now),
|
|
3324
|
+
title: s.title ?? "-",
|
|
3325
|
+
cwd: shortenHomePath(s.cwd)
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
|
|
3329
|
+
if (upstreamSessionId && upstreamSessionId.length > 0) {
|
|
3330
|
+
return upstreamSessionId;
|
|
3331
|
+
}
|
|
3332
|
+
if (importedFromMachine && importedFromMachine.length > 0) {
|
|
3333
|
+
return `\u2190 ${importedFromMachine}`;
|
|
3334
|
+
}
|
|
3335
|
+
return "-";
|
|
3336
|
+
}
|
|
3337
|
+
function formatState(status, clients) {
|
|
3338
|
+
if (status === "cold") {
|
|
3339
|
+
return "COLD";
|
|
3340
|
+
}
|
|
3341
|
+
return `LIVE(${clients})`;
|
|
3342
|
+
}
|
|
3343
|
+
function computeWidths(rows) {
|
|
3344
|
+
return {
|
|
3345
|
+
session: maxLen(HEADER.session, rows.map((r) => r.session)),
|
|
3346
|
+
upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
3347
|
+
state: maxLen(HEADER.state, rows.map((r) => r.state)),
|
|
3348
|
+
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
3349
|
+
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
3350
|
+
cwd: maxLen(HEADER.cwd, rows.map((r) => r.cwd)),
|
|
3351
|
+
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
function formatRelativeAge(iso, now) {
|
|
3355
|
+
if (!iso) {
|
|
3356
|
+
return "?";
|
|
3357
|
+
}
|
|
3358
|
+
const t = Date.parse(iso);
|
|
3359
|
+
if (Number.isNaN(t)) {
|
|
3360
|
+
return "?";
|
|
3361
|
+
}
|
|
3362
|
+
const diff = Math.max(0, now - t);
|
|
3363
|
+
const sec = Math.floor(diff / 1e3);
|
|
3364
|
+
if (sec < 60) {
|
|
3365
|
+
return "<1m";
|
|
3366
|
+
}
|
|
3367
|
+
const min = Math.floor(sec / 60);
|
|
3368
|
+
if (min < 60) {
|
|
3369
|
+
return `${min}m`;
|
|
3370
|
+
}
|
|
3371
|
+
const hr = Math.floor(min / 60);
|
|
3372
|
+
if (hr < 24) {
|
|
3373
|
+
return `${hr}h`;
|
|
3374
|
+
}
|
|
3375
|
+
const day = Math.floor(hr / 24);
|
|
3376
|
+
if (day < 14) {
|
|
3377
|
+
return `${day}d`;
|
|
3378
|
+
}
|
|
3379
|
+
const week = Math.floor(day / 7);
|
|
3380
|
+
if (week < 9) {
|
|
3381
|
+
return `${week}w`;
|
|
3382
|
+
}
|
|
3383
|
+
const month = Math.floor(day / 30);
|
|
3384
|
+
if (month < 12) {
|
|
3385
|
+
return `${month}mo`;
|
|
3386
|
+
}
|
|
3387
|
+
const year = Math.floor(day / 365);
|
|
3388
|
+
return `${year}y`;
|
|
3389
|
+
}
|
|
3390
|
+
function maxLen(headerCell, values) {
|
|
3391
|
+
let max = headerCell.length;
|
|
3392
|
+
for (const v of values) {
|
|
3393
|
+
if (v.length > max) {
|
|
3394
|
+
max = v.length;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
return max;
|
|
3398
|
+
}
|
|
3399
|
+
function formatRow(r, w, maxWidth, cwdMaxWidth = DEFAULT_CWD_MAX_WIDTH) {
|
|
3400
|
+
const fixed = [
|
|
3401
|
+
r.session.padEnd(w.session),
|
|
3402
|
+
r.upstream.padEnd(w.upstream),
|
|
3403
|
+
r.state.padEnd(w.state),
|
|
3404
|
+
r.agent.padEnd(w.agent),
|
|
3405
|
+
r.age.padStart(w.age)
|
|
3406
|
+
].join(SEP);
|
|
3407
|
+
if (maxWidth === void 0) {
|
|
3408
|
+
return [fixed, r.cwd.padEnd(w.cwd), r.title].join(SEP);
|
|
3409
|
+
}
|
|
3410
|
+
const budget = maxWidth - fixed.length - SEP.length;
|
|
3411
|
+
if (budget <= 0) {
|
|
3412
|
+
return fixed.slice(0, maxWidth);
|
|
3413
|
+
}
|
|
3414
|
+
const cwdCap = Math.min(w.cwd, cwdMaxWidth);
|
|
3415
|
+
const cwdAlloc = Math.min(cwdCap, Math.max(0, budget - SEP.length - 1));
|
|
3416
|
+
const cwdCell = truncateMiddle(r.cwd, cwdAlloc).padEnd(cwdAlloc);
|
|
3417
|
+
const titleBudget = Math.max(0, budget - cwdAlloc - SEP.length);
|
|
3418
|
+
const titleCell = truncateRight(r.title, titleBudget);
|
|
3419
|
+
return [fixed, cwdCell, titleCell].join(SEP);
|
|
3420
|
+
}
|
|
3421
|
+
function truncateRight(s, max) {
|
|
3422
|
+
if (max <= 0) {
|
|
3423
|
+
return "";
|
|
3424
|
+
}
|
|
3425
|
+
if (s.length <= max) {
|
|
3426
|
+
return s;
|
|
3427
|
+
}
|
|
3428
|
+
if (max === 1) {
|
|
3429
|
+
return "\u2026";
|
|
3430
|
+
}
|
|
3431
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
3432
|
+
}
|
|
3433
|
+
function truncateMiddle(s, max) {
|
|
3434
|
+
if (max <= 0) {
|
|
3435
|
+
return "";
|
|
3436
|
+
}
|
|
3437
|
+
if (s.length <= max) {
|
|
3438
|
+
return s;
|
|
3439
|
+
}
|
|
3440
|
+
if (max === 1) {
|
|
3441
|
+
return "\u2026";
|
|
3442
|
+
}
|
|
3443
|
+
const head = Math.ceil((max - 1) / 2);
|
|
3444
|
+
const tail = max - 1 - head;
|
|
3445
|
+
return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
|
|
3446
|
+
}
|
|
3447
|
+
var HEADER, SEP, DEFAULT_CWD_MAX_WIDTH;
|
|
3448
|
+
var init_session_row = __esm({
|
|
3449
|
+
"src/cli/session-row.ts"() {
|
|
3450
|
+
"use strict";
|
|
3451
|
+
init_agent_display();
|
|
3452
|
+
init_paths();
|
|
3453
|
+
init_session();
|
|
3454
|
+
HEADER = {
|
|
3455
|
+
session: "SESSION",
|
|
3456
|
+
upstream: "UPSTREAM",
|
|
3457
|
+
state: "STATE",
|
|
3458
|
+
agent: "AGENT",
|
|
3459
|
+
age: "AGE",
|
|
3460
|
+
title: "TITLE",
|
|
3461
|
+
cwd: "CWD"
|
|
3462
|
+
};
|
|
3463
|
+
SEP = " ";
|
|
3464
|
+
DEFAULT_CWD_MAX_WIDTH = 24;
|
|
3465
|
+
}
|
|
3466
|
+
});
|
|
3467
|
+
|
|
3468
|
+
// src/cli/commands/sessions.ts
|
|
3469
|
+
import * as fs13 from "fs/promises";
|
|
3470
|
+
import * as path8 from "path";
|
|
3471
|
+
async function runSessionsList(opts = {}) {
|
|
3472
|
+
const config = await loadConfig();
|
|
3473
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3474
|
+
const url = new URL(`${baseUrl}/v1/sessions`);
|
|
3475
|
+
const response = await fetch(url.toString(), {
|
|
3476
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
3477
|
+
});
|
|
3478
|
+
if (!response.ok) {
|
|
3479
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
3480
|
+
`);
|
|
3481
|
+
process.exit(1);
|
|
3482
|
+
}
|
|
3483
|
+
const body = await response.json();
|
|
3484
|
+
if (opts.json) {
|
|
3485
|
+
process.stdout.write(JSON.stringify(body.sessions, null, 2) + "\n");
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
if (body.sessions.length === 0) {
|
|
3489
|
+
process.stdout.write("No active sessions.\n");
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
const sorted = body.sessions.slice().sort((a, b) => {
|
|
3493
|
+
const liveDiff = (b.status === "live" ? 1 : 0) - (a.status === "live" ? 1 : 0);
|
|
3494
|
+
if (liveDiff !== 0) {
|
|
3495
|
+
return liveDiff;
|
|
3496
|
+
}
|
|
3497
|
+
return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
|
|
3498
|
+
});
|
|
3499
|
+
let visible = sorted;
|
|
3500
|
+
let truncated = 0;
|
|
3501
|
+
if (!opts.all) {
|
|
3502
|
+
const liveCount = sorted.filter((s) => s.status !== "cold").length;
|
|
3503
|
+
const limit = config.sessionListColdLimit;
|
|
3504
|
+
const coldSlice = sorted.slice(liveCount, liveCount + limit);
|
|
3505
|
+
const hiddenCold = sorted.length - liveCount - coldSlice.length;
|
|
3506
|
+
visible = [...sorted.slice(0, liveCount), ...coldSlice];
|
|
3507
|
+
truncated = hiddenCold;
|
|
3508
|
+
}
|
|
3509
|
+
const now = Date.now();
|
|
3510
|
+
const rows = visible.map((s) => toRow(s, now));
|
|
3511
|
+
const widths = computeWidths(rows);
|
|
3512
|
+
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
3513
|
+
const cwdMax = config.tui.cwdColumnMaxWidth;
|
|
3514
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdMax) + "\n");
|
|
3515
|
+
for (const r of rows) {
|
|
3516
|
+
process.stdout.write(formatRow(r, widths, maxWidth, cwdMax) + "\n");
|
|
3517
|
+
}
|
|
3518
|
+
if (truncated > 0) {
|
|
3519
|
+
process.stdout.write(
|
|
3520
|
+
`
|
|
3521
|
+
... ${truncated} more cold session${truncated === 1 ? "" : "s"} hidden. Use --all to show.
|
|
3522
|
+
`
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
async function runSessionsKill(id) {
|
|
3527
|
+
if (!id) {
|
|
3528
|
+
process.stderr.write("Usage: hydra-acp sessions kill <session-id>\n");
|
|
3529
|
+
process.exit(2);
|
|
3530
|
+
}
|
|
3531
|
+
const config = await loadConfig();
|
|
3532
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3533
|
+
const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
|
|
3534
|
+
method: "POST",
|
|
3535
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
3536
|
+
});
|
|
3537
|
+
if (!response.ok && response.status !== 204) {
|
|
3538
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
3539
|
+
`);
|
|
3540
|
+
process.exit(1);
|
|
3541
|
+
}
|
|
3542
|
+
process.stdout.write(`Killed ${id}
|
|
3543
|
+
`);
|
|
3544
|
+
}
|
|
3545
|
+
async function runSessionsRemove(id) {
|
|
3546
|
+
if (!id) {
|
|
3547
|
+
process.stderr.write("Usage: hydra-acp sessions remove <session-id>\n");
|
|
3548
|
+
process.exit(2);
|
|
3549
|
+
}
|
|
3550
|
+
const config = await loadConfig();
|
|
3551
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3552
|
+
const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
|
|
3553
|
+
method: "DELETE",
|
|
3554
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
3555
|
+
});
|
|
3556
|
+
if (!response.ok && response.status !== 204) {
|
|
3557
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
3558
|
+
`);
|
|
3559
|
+
process.exit(1);
|
|
3560
|
+
}
|
|
3561
|
+
process.stdout.write(`Removed ${id}
|
|
3562
|
+
`);
|
|
3563
|
+
}
|
|
3564
|
+
async function runSessionsExport(id, outPath) {
|
|
3565
|
+
if (!id) {
|
|
3566
|
+
process.stderr.write(
|
|
3567
|
+
"Usage: hydra-acp sessions export <session-id> [--out <file>]\n"
|
|
3568
|
+
);
|
|
3569
|
+
process.exit(2);
|
|
3570
|
+
}
|
|
3571
|
+
const config = await loadConfig();
|
|
3572
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3573
|
+
const response = await fetch(
|
|
3574
|
+
`${baseUrl}/v1/sessions/${encodeURIComponent(id)}/export`,
|
|
3575
|
+
{
|
|
3576
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
3577
|
+
}
|
|
3578
|
+
);
|
|
3579
|
+
if (!response.ok) {
|
|
3580
|
+
const text = await response.text().catch(() => "");
|
|
3581
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
3582
|
+
`);
|
|
3583
|
+
process.exit(1);
|
|
3584
|
+
}
|
|
3585
|
+
const body = await response.text();
|
|
3586
|
+
if (!outPath) {
|
|
3587
|
+
process.stdout.write(body);
|
|
3588
|
+
if (!body.endsWith("\n")) {
|
|
3589
|
+
process.stdout.write("\n");
|
|
3590
|
+
}
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
3594
|
+
await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
|
|
3595
|
+
await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
3596
|
+
process.stdout.write(`Wrote ${resolved}
|
|
3597
|
+
`);
|
|
3598
|
+
}
|
|
3599
|
+
async function runSessionsTranscript(idOrFile, outPath) {
|
|
3600
|
+
if (!idOrFile) {
|
|
3601
|
+
process.stderr.write(
|
|
3602
|
+
"Usage: hydra-acp sessions transcript <session-id>|<file> [--out <file>|.]\n"
|
|
3603
|
+
);
|
|
3604
|
+
process.exit(2);
|
|
3605
|
+
}
|
|
3606
|
+
let body;
|
|
3607
|
+
let defaultName;
|
|
3608
|
+
const localFile = await readBundleFileIfExists(idOrFile);
|
|
3609
|
+
if (localFile !== null) {
|
|
3610
|
+
const bundle = decodeBundleOrExit(localFile.raw);
|
|
3611
|
+
body = bundleToMarkdown(bundle);
|
|
3612
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3613
|
+
defaultName = `${path8.basename(idOrFile, path8.extname(idOrFile))}-${stamp}.md`;
|
|
3614
|
+
} else {
|
|
3615
|
+
const config = await loadConfig();
|
|
3616
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3617
|
+
const response = await fetch(
|
|
3618
|
+
`${baseUrl}/v1/sessions/${encodeURIComponent(idOrFile)}/transcript`,
|
|
3619
|
+
{
|
|
3620
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
3621
|
+
}
|
|
3622
|
+
);
|
|
3623
|
+
if (!response.ok) {
|
|
3624
|
+
const text = await response.text().catch(() => "");
|
|
3625
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
3626
|
+
`);
|
|
3627
|
+
process.exit(1);
|
|
3628
|
+
}
|
|
3629
|
+
body = await response.text();
|
|
3630
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3631
|
+
defaultName = `hydra-${idOrFile}-${stamp}.md`;
|
|
3632
|
+
}
|
|
3633
|
+
if (!outPath) {
|
|
3634
|
+
process.stdout.write(body);
|
|
3635
|
+
if (!body.endsWith("\n")) {
|
|
3636
|
+
process.stdout.write("\n");
|
|
3637
|
+
}
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
const resolved = outPath === "." ? defaultName : outPath;
|
|
3641
|
+
await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
|
|
3642
|
+
await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
3643
|
+
process.stdout.write(`Wrote ${resolved}
|
|
3644
|
+
`);
|
|
3645
|
+
}
|
|
3646
|
+
async function readBundleFileIfExists(arg) {
|
|
3647
|
+
try {
|
|
3648
|
+
const stat4 = await fs13.stat(arg);
|
|
3649
|
+
if (!stat4.isFile()) {
|
|
3650
|
+
return null;
|
|
3651
|
+
}
|
|
3652
|
+
} catch {
|
|
3653
|
+
return null;
|
|
3654
|
+
}
|
|
3655
|
+
const text = await fs13.readFile(arg, "utf8");
|
|
3656
|
+
try {
|
|
3657
|
+
return { raw: JSON.parse(text) };
|
|
3658
|
+
} catch (err) {
|
|
3659
|
+
process.stderr.write(`Failed to parse bundle file: ${err.message}
|
|
3660
|
+
`);
|
|
3661
|
+
process.exit(1);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
function decodeBundleOrExit(raw) {
|
|
3665
|
+
try {
|
|
3666
|
+
return decodeBundle(raw);
|
|
3667
|
+
} catch (err) {
|
|
3668
|
+
process.stderr.write(`Not a valid bundle: ${err.message}
|
|
3669
|
+
`);
|
|
3670
|
+
process.exit(1);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
async function runSessionsImport(file, opts = {}) {
|
|
3674
|
+
if (!file) {
|
|
3675
|
+
process.stderr.write(
|
|
3676
|
+
"Usage: hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]\n"
|
|
3677
|
+
);
|
|
3678
|
+
process.exit(2);
|
|
3679
|
+
}
|
|
3680
|
+
let cwdOverride;
|
|
3681
|
+
if (opts.cwd !== void 0) {
|
|
3682
|
+
const resolved = path8.resolve(opts.cwd);
|
|
3683
|
+
try {
|
|
3684
|
+
const stat4 = await fs13.stat(resolved);
|
|
3685
|
+
if (!stat4.isDirectory()) {
|
|
3686
|
+
process.stderr.write(`--cwd ${resolved} is not a directory
|
|
3687
|
+
`);
|
|
3688
|
+
process.exit(1);
|
|
3689
|
+
}
|
|
3690
|
+
} catch {
|
|
3691
|
+
process.stderr.write(`--cwd ${resolved} does not exist
|
|
3692
|
+
`);
|
|
3693
|
+
process.exit(1);
|
|
3694
|
+
}
|
|
3695
|
+
cwdOverride = resolved;
|
|
3696
|
+
}
|
|
3697
|
+
let body;
|
|
3698
|
+
if (file === "-") {
|
|
3699
|
+
body = await readStdin();
|
|
3700
|
+
} else {
|
|
3701
|
+
body = await fs13.readFile(file, "utf8");
|
|
3702
|
+
}
|
|
3703
|
+
let bundle;
|
|
3704
|
+
try {
|
|
3705
|
+
bundle = JSON.parse(body);
|
|
3706
|
+
} catch (err) {
|
|
3707
|
+
process.stderr.write(`Failed to parse bundle: ${err.message}
|
|
3708
|
+
`);
|
|
3709
|
+
process.exit(1);
|
|
3710
|
+
}
|
|
3711
|
+
if (opts.info === true) {
|
|
3712
|
+
const inspectConfig = await loadConfigReadOnly();
|
|
3713
|
+
printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
const config = await loadConfig();
|
|
3717
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
3718
|
+
const response = await fetch(`${baseUrl}/v1/sessions/import`, {
|
|
3719
|
+
method: "POST",
|
|
3720
|
+
headers: {
|
|
3721
|
+
"Content-Type": "application/json",
|
|
3722
|
+
Authorization: `Bearer ${config.daemon.authToken}`
|
|
3723
|
+
},
|
|
3724
|
+
body: JSON.stringify({
|
|
3725
|
+
bundle,
|
|
3726
|
+
replace: opts.replace === true,
|
|
3727
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
3728
|
+
})
|
|
3729
|
+
});
|
|
3730
|
+
if (response.status === 409) {
|
|
3731
|
+
const detail = await response.json().catch(() => ({}));
|
|
3732
|
+
process.stderr.write(
|
|
3733
|
+
`Bundle already imported as ${detail.existingSessionId ?? "unknown"}. Use --replace to overwrite.
|
|
3734
|
+
`
|
|
3735
|
+
);
|
|
3736
|
+
process.exit(1);
|
|
3737
|
+
}
|
|
3738
|
+
if (!response.ok) {
|
|
3739
|
+
const text = await response.text().catch(() => "");
|
|
3740
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
3741
|
+
`);
|
|
3742
|
+
process.exit(1);
|
|
3743
|
+
}
|
|
3744
|
+
const result = await response.json();
|
|
3745
|
+
process.stdout.write(
|
|
3746
|
+
result.replaced ? `Replaced ${result.sessionId} (from ${result.importedFromSessionId})
|
|
3747
|
+
` : `Imported as ${result.sessionId} (from ${result.importedFromSessionId})
|
|
3748
|
+
`
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
function bundleToSummary(parsed) {
|
|
3752
|
+
return {
|
|
3753
|
+
sessionId: parsed.session.sessionId,
|
|
3754
|
+
cwd: parsed.session.cwd,
|
|
3755
|
+
agentId: parsed.session.agentId,
|
|
3756
|
+
currentUsage: parsed.session.currentUsage,
|
|
3757
|
+
title: parsed.session.title,
|
|
3758
|
+
importedFromMachine: parsed.exportedFrom.machine,
|
|
3759
|
+
attachedClients: 0,
|
|
3760
|
+
updatedAt: parsed.session.updatedAt,
|
|
3761
|
+
status: "cold"
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
function printBundleInfo(raw, cwdColumnMaxWidth) {
|
|
3765
|
+
let parsed;
|
|
3766
|
+
try {
|
|
3767
|
+
parsed = decodeBundle(raw);
|
|
3768
|
+
} catch (err) {
|
|
3769
|
+
process.stderr.write(`Not a valid bundle: ${err.message}
|
|
3770
|
+
`);
|
|
3771
|
+
process.exit(1);
|
|
3772
|
+
}
|
|
3773
|
+
const summary = bundleToSummary(parsed);
|
|
3774
|
+
const row = toRow(summary);
|
|
3775
|
+
const widths = computeWidths([row]);
|
|
3776
|
+
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
3777
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
3778
|
+
process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
3779
|
+
const originUpstream = parsed.session.upstreamSessionId ?? "-";
|
|
3780
|
+
process.stdout.write(
|
|
3781
|
+
`
|
|
3782
|
+
lineage: ${parsed.session.lineageId}
|
|
3783
|
+
exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
|
|
3784
|
+
origin session: ${parsed.session.sessionId}
|
|
3785
|
+
origin upstream: ${originUpstream}
|
|
3786
|
+
history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
|
|
3787
|
+
` : "\n")
|
|
3788
|
+
);
|
|
3789
|
+
}
|
|
3790
|
+
async function readStdin() {
|
|
3791
|
+
const chunks = [];
|
|
3792
|
+
for await (const chunk of process.stdin) {
|
|
3793
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
3794
|
+
}
|
|
3795
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3796
|
+
}
|
|
3797
|
+
function deriveFilenameFrom(response, id) {
|
|
3798
|
+
const cd = response.headers.get("content-disposition");
|
|
3799
|
+
if (cd) {
|
|
3800
|
+
const match = cd.match(/filename="([^"]+)"/);
|
|
3801
|
+
if (match) {
|
|
3802
|
+
return match[1];
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3806
|
+
return `hydra-${id}-${stamp}.hydra`;
|
|
3807
|
+
}
|
|
3808
|
+
function httpBase(host, port, tls) {
|
|
3809
|
+
const protocol = tls ? "https" : "http";
|
|
3810
|
+
return `${protocol}://${host}:${port}`;
|
|
3811
|
+
}
|
|
3812
|
+
var init_sessions = __esm({
|
|
3813
|
+
"src/cli/commands/sessions.ts"() {
|
|
3814
|
+
"use strict";
|
|
3815
|
+
init_config();
|
|
3816
|
+
init_bundle();
|
|
3817
|
+
init_transcript();
|
|
3818
|
+
init_session_row();
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
|
|
3822
|
+
// src/shim/resilient-ws.ts
|
|
3823
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
3824
|
+
import { WebSocket } from "ws";
|
|
3825
|
+
function isResponse(msg) {
|
|
3826
|
+
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
3827
|
+
}
|
|
3828
|
+
async function openWs(url, subprotocols) {
|
|
3829
|
+
return new Promise((resolve5, reject) => {
|
|
3830
|
+
const ws = new WebSocket(url, subprotocols);
|
|
3831
|
+
const onOpen = () => {
|
|
3832
|
+
ws.off("error", onError);
|
|
3833
|
+
resolve5(wsToMessageStream(ws));
|
|
3834
|
+
};
|
|
3835
|
+
const onError = (err) => {
|
|
3836
|
+
ws.off("open", onOpen);
|
|
3107
3837
|
reject(err);
|
|
3108
3838
|
};
|
|
3109
3839
|
ws.once("open", onOpen);
|
|
@@ -3324,6 +4054,58 @@ var init_resilient_ws = __esm({
|
|
|
3324
4054
|
}
|
|
3325
4055
|
});
|
|
3326
4056
|
|
|
4057
|
+
// src/core/update-check.ts
|
|
4058
|
+
function disabled() {
|
|
4059
|
+
if (process.env.NO_UPDATE_NOTIFIER === "1") {
|
|
4060
|
+
return true;
|
|
4061
|
+
}
|
|
4062
|
+
if (process.argv.includes("--no-update-notifier")) {
|
|
4063
|
+
return true;
|
|
4064
|
+
}
|
|
4065
|
+
return false;
|
|
4066
|
+
}
|
|
4067
|
+
async function getPendingUpdate() {
|
|
4068
|
+
if (cached !== void 0) {
|
|
4069
|
+
return cached;
|
|
4070
|
+
}
|
|
4071
|
+
if (disabled()) {
|
|
4072
|
+
cached = null;
|
|
4073
|
+
return cached;
|
|
4074
|
+
}
|
|
4075
|
+
try {
|
|
4076
|
+
const mod = await import("update-notifier");
|
|
4077
|
+
const updateNotifier = mod.default ?? mod;
|
|
4078
|
+
const notifier = updateNotifier({
|
|
4079
|
+
pkg: { name: PKG_NAME, version: HYDRA_VERSION },
|
|
4080
|
+
updateCheckInterval: 1e3 * 60 * 60 * 24
|
|
4081
|
+
});
|
|
4082
|
+
const u = notifier.update;
|
|
4083
|
+
if (u && typeof u.latest === "string" && typeof u.current === "string" && u.latest !== u.current) {
|
|
4084
|
+
cached = {
|
|
4085
|
+
current: u.current,
|
|
4086
|
+
latest: u.latest,
|
|
4087
|
+
type: typeof u.type === "string" ? u.type : "unknown"
|
|
4088
|
+
};
|
|
4089
|
+
} else {
|
|
4090
|
+
cached = null;
|
|
4091
|
+
}
|
|
4092
|
+
} catch {
|
|
4093
|
+
cached = null;
|
|
4094
|
+
}
|
|
4095
|
+
return cached;
|
|
4096
|
+
}
|
|
4097
|
+
function formatUpdateNoticeLine(info) {
|
|
4098
|
+
return `hydra-acp ${info.latest} available (current ${info.current}) \xB7 run: npm update -g ${PKG_NAME}`;
|
|
4099
|
+
}
|
|
4100
|
+
var PKG_NAME, cached;
|
|
4101
|
+
var init_update_check = __esm({
|
|
4102
|
+
"src/core/update-check.ts"() {
|
|
4103
|
+
"use strict";
|
|
4104
|
+
init_hydra_version();
|
|
4105
|
+
PKG_NAME = "@hydra-acp/cli";
|
|
4106
|
+
}
|
|
4107
|
+
});
|
|
4108
|
+
|
|
3327
4109
|
// src/tui/discovery.ts
|
|
3328
4110
|
async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
3329
4111
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -4558,7 +5340,7 @@ function mapKeyName(name) {
|
|
|
4558
5340
|
return null;
|
|
4559
5341
|
}
|
|
4560
5342
|
}
|
|
4561
|
-
var
|
|
5343
|
+
var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
4562
5344
|
var init_screen = __esm({
|
|
4563
5345
|
"src/tui/screen.ts"() {
|
|
4564
5346
|
"use strict";
|
|
@@ -4566,7 +5348,7 @@ var init_screen = __esm({
|
|
|
4566
5348
|
init_paths();
|
|
4567
5349
|
init_session();
|
|
4568
5350
|
init_attachments();
|
|
4569
|
-
|
|
5351
|
+
SESSIONBAR_ROWS = 1;
|
|
4570
5352
|
BANNER_ROWS = 1;
|
|
4571
5353
|
SEPARATOR_ROWS = 1;
|
|
4572
5354
|
MAX_PROMPT_ROWS = 8;
|
|
@@ -4664,7 +5446,7 @@ var init_screen = __esm({
|
|
|
4664
5446
|
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
4665
5447
|
queued: 0
|
|
4666
5448
|
};
|
|
4667
|
-
|
|
5449
|
+
sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
|
|
4668
5450
|
lastWindowTitle = null;
|
|
4669
5451
|
resizeHandler;
|
|
4670
5452
|
keyHandler;
|
|
@@ -5015,8 +5797,8 @@ var init_screen = __esm({
|
|
|
5015
5797
|
this.trimScrollback();
|
|
5016
5798
|
this.scheduleRepaint();
|
|
5017
5799
|
}
|
|
5018
|
-
|
|
5019
|
-
this.
|
|
5800
|
+
setSessionbar(sessionbar) {
|
|
5801
|
+
this.sessionbar = { ...this.sessionbar, ...sessionbar };
|
|
5020
5802
|
this.syncWindowTitle();
|
|
5021
5803
|
this.repaint();
|
|
5022
5804
|
}
|
|
@@ -5024,8 +5806,8 @@ var init_screen = __esm({
|
|
|
5024
5806
|
// the host terminal via OSC 2. Supported by xterm/foot/iTerm2/Alacritty/
|
|
5025
5807
|
// most modern emulators; ignored harmlessly elsewhere.
|
|
5026
5808
|
syncWindowTitle() {
|
|
5027
|
-
const title = this.
|
|
5028
|
-
const fallback = shortId(this.
|
|
5809
|
+
const title = this.sessionbar.title?.trim();
|
|
5810
|
+
const fallback = shortId(this.sessionbar.sessionId) || "hydra";
|
|
5029
5811
|
const raw = title && title.length > 0 ? title : fallback;
|
|
5030
5812
|
const clean = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
5031
5813
|
if (clean === this.lastWindowTitle) {
|
|
@@ -5547,8 +6329,10 @@ var init_screen = __esm({
|
|
|
5547
6329
|
return Math.max(1, this.scrollbackVisibleRows() - 2);
|
|
5548
6330
|
}
|
|
5549
6331
|
scrollbackVisibleRows() {
|
|
5550
|
-
const top =
|
|
5551
|
-
const bottom = this.term.height - this.promptRows() -
|
|
6332
|
+
const top = 1;
|
|
6333
|
+
const bottom = this.term.height - this.promptRows() - SESSIONBAR_ROWS - SEPARATOR_ROWS - // separator between banner and sessionbar
|
|
6334
|
+
BANNER_ROWS - SEPARATOR_ROWS - // separator above prompt
|
|
6335
|
+
this.chipRows() - this.queuedRows() - this.completionRows();
|
|
5552
6336
|
return Math.max(0, bottom - top + 1);
|
|
5553
6337
|
}
|
|
5554
6338
|
maxScrollOffset() {
|
|
@@ -5625,30 +6409,32 @@ var init_screen = __esm({
|
|
|
5625
6409
|
this.lastFrameW = w;
|
|
5626
6410
|
this.lastFrameH = h;
|
|
5627
6411
|
}
|
|
5628
|
-
this.drawHeader();
|
|
5629
|
-
this.drawSeparator(HEADER_ROWS);
|
|
5630
6412
|
this.drawScrollback();
|
|
5631
6413
|
this.drawCompletionZone();
|
|
5632
6414
|
this.drawQueuedZone();
|
|
5633
6415
|
this.drawAttachmentChipZone();
|
|
5634
6416
|
const promptRows = this.promptRows();
|
|
5635
|
-
const
|
|
5636
|
-
this.drawSeparator(
|
|
6417
|
+
const separatorAbovePromptRow = h - promptRows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
|
|
6418
|
+
this.drawSeparator(separatorAbovePromptRow);
|
|
5637
6419
|
this.drawPrompt();
|
|
5638
6420
|
this.drawBanner();
|
|
6421
|
+
this.drawSeparator(h - SESSIONBAR_ROWS);
|
|
6422
|
+
this.drawSessionbar();
|
|
5639
6423
|
this.placeCursor();
|
|
5640
6424
|
this.lastPromptRows = promptRows;
|
|
5641
6425
|
}
|
|
5642
|
-
|
|
6426
|
+
drawSessionbar() {
|
|
5643
6427
|
const w = this.term.width;
|
|
5644
|
-
const
|
|
5645
|
-
const sid = shortId(this.
|
|
5646
|
-
const title = this.
|
|
5647
|
-
const agentCell = formatAgentWithModel(this.
|
|
5648
|
-
const cwdDisplay = shortenHomePath(this.
|
|
5649
|
-
const
|
|
5650
|
-
|
|
5651
|
-
|
|
6428
|
+
const row = this.term.height;
|
|
6429
|
+
const sid = shortId(this.sessionbar.sessionId);
|
|
6430
|
+
const title = this.sessionbar.title?.trim();
|
|
6431
|
+
const agentCell = formatAgentWithModel(this.sessionbar.agent, this.sessionbar.model);
|
|
6432
|
+
const cwdDisplay = shortenHomePath(this.sessionbar.cwd);
|
|
6433
|
+
const usage = formatUsage(this.sessionbar.usage);
|
|
6434
|
+
const sig = `sbar|${w}|${sid}|${agentCell}|${cwdDisplay}|${title ?? ""}|${usage ?? ""}`;
|
|
6435
|
+
this.paintRow(row, sig, () => {
|
|
6436
|
+
const usageReserve = usage ? usage.length + 3 : 0;
|
|
6437
|
+
const fixed = sid.length + " \xB7 ".length + agentCell.length + " \xB7 ".length + (title ? " \xB7 ".length : 0) + usageReserve;
|
|
5652
6438
|
const variableRoom = Math.max(8, w - fixed);
|
|
5653
6439
|
let cwdRoom;
|
|
5654
6440
|
let titleRoom;
|
|
@@ -5660,13 +6446,13 @@ var init_screen = __esm({
|
|
|
5660
6446
|
titleRoom = 0;
|
|
5661
6447
|
cwdRoom = variableRoom;
|
|
5662
6448
|
}
|
|
5663
|
-
this.term.
|
|
6449
|
+
this.term.yellow(sid)(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom));
|
|
5664
6450
|
if (title) {
|
|
5665
6451
|
this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
|
|
5666
6452
|
}
|
|
5667
6453
|
if (usage) {
|
|
5668
6454
|
const col = Math.max(1, w - usage.length + 1);
|
|
5669
|
-
this.term.moveTo(col,
|
|
6455
|
+
this.term.moveTo(col, row).eraseLineAfter();
|
|
5670
6456
|
this.term.dim.noFormat(usage);
|
|
5671
6457
|
}
|
|
5672
6458
|
});
|
|
@@ -5679,7 +6465,7 @@ var init_screen = __esm({
|
|
|
5679
6465
|
}
|
|
5680
6466
|
drawScrollback() {
|
|
5681
6467
|
const w = this.term.width;
|
|
5682
|
-
const top =
|
|
6468
|
+
const top = 1;
|
|
5683
6469
|
const visibleRows = this.scrollbackVisibleRows();
|
|
5684
6470
|
if (visibleRows <= 0) {
|
|
5685
6471
|
return;
|
|
@@ -5745,7 +6531,7 @@ var init_screen = __esm({
|
|
|
5745
6531
|
}
|
|
5746
6532
|
const w = this.term.width;
|
|
5747
6533
|
const promptRows = this.promptRows();
|
|
5748
|
-
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
6534
|
+
const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
|
|
5749
6535
|
const queuedRows = this.queuedRows();
|
|
5750
6536
|
const chipRows = this.chipRows();
|
|
5751
6537
|
const completionBottom = separatorRow - 1 - queuedRows - chipRows;
|
|
@@ -5793,7 +6579,7 @@ var init_screen = __esm({
|
|
|
5793
6579
|
}
|
|
5794
6580
|
const w = this.term.width;
|
|
5795
6581
|
const promptRows = this.promptRows();
|
|
5796
|
-
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
6582
|
+
const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
|
|
5797
6583
|
const chipBottom = separatorRow - 1;
|
|
5798
6584
|
const chipTop = chipBottom - rows + 1;
|
|
5799
6585
|
const iterm = this.isIterm2();
|
|
@@ -5840,7 +6626,7 @@ var init_screen = __esm({
|
|
|
5840
6626
|
}
|
|
5841
6627
|
const w = this.term.width;
|
|
5842
6628
|
const promptRows = this.promptRows();
|
|
5843
|
-
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
6629
|
+
const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
|
|
5844
6630
|
const chipRows = this.chipRows();
|
|
5845
6631
|
const queuedBottom = separatorRow - 1 - chipRows;
|
|
5846
6632
|
const queuedTop = queuedBottom - rows + 1;
|
|
@@ -5882,7 +6668,7 @@ var init_screen = __esm({
|
|
|
5882
6668
|
const state = this.dispatcher.state();
|
|
5883
6669
|
const visualRows = computePromptVisualRows(state.buffer, room);
|
|
5884
6670
|
const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
|
|
5885
|
-
const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
|
|
6671
|
+
const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
5886
6672
|
for (let i = 0; i < layout.rendered; i++) {
|
|
5887
6673
|
const vr = visualRows[layout.windowStart + i];
|
|
5888
6674
|
const row = top + i;
|
|
@@ -5918,7 +6704,7 @@ var init_screen = __esm({
|
|
|
5918
6704
|
return;
|
|
5919
6705
|
}
|
|
5920
6706
|
const w = this.term.width;
|
|
5921
|
-
const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
|
|
6707
|
+
const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
5922
6708
|
this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
|
|
5923
6709
|
this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
|
|
5924
6710
|
});
|
|
@@ -5933,7 +6719,7 @@ var init_screen = __esm({
|
|
|
5933
6719
|
}
|
|
5934
6720
|
const w = this.term.width;
|
|
5935
6721
|
const rows = this.permissionRows();
|
|
5936
|
-
const top = this.term.height - rows - BANNER_ROWS + 1;
|
|
6722
|
+
const top = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
5937
6723
|
let row = top;
|
|
5938
6724
|
const writeRow = (sig, paint) => {
|
|
5939
6725
|
if (row >= top + rows) {
|
|
@@ -5975,7 +6761,7 @@ var init_screen = __esm({
|
|
|
5975
6761
|
});
|
|
5976
6762
|
}
|
|
5977
6763
|
drawBanner() {
|
|
5978
|
-
const row = this.term.height;
|
|
6764
|
+
const row = this.term.height - SESSIONBAR_ROWS - SEPARATOR_ROWS;
|
|
5979
6765
|
const w = this.term.width;
|
|
5980
6766
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
5981
6767
|
const right = this.bannerRightContent();
|
|
@@ -6024,13 +6810,14 @@ var init_screen = __esm({
|
|
|
6024
6810
|
placeCursor() {
|
|
6025
6811
|
if (this.permissionPrompt) {
|
|
6026
6812
|
const rows = this.permissionRows();
|
|
6027
|
-
const top2 = this.term.height - rows - BANNER_ROWS + 1;
|
|
6813
|
+
const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
6028
6814
|
const optionRow = top2 + 3 + this.permissionPrompt.selectedIndex;
|
|
6029
|
-
|
|
6815
|
+
const lastUsableRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
|
|
6816
|
+
this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
|
|
6030
6817
|
return;
|
|
6031
6818
|
}
|
|
6032
6819
|
if (this.confirmPrompt) {
|
|
6033
|
-
const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
|
|
6820
|
+
const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
6034
6821
|
this.term.moveTo(2, top2);
|
|
6035
6822
|
return;
|
|
6036
6823
|
}
|
|
@@ -6044,12 +6831,13 @@ var init_screen = __esm({
|
|
|
6044
6831
|
const state = this.dispatcher.state();
|
|
6045
6832
|
const visualRows = computePromptVisualRows(state.buffer, room);
|
|
6046
6833
|
const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
|
|
6047
|
-
const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
|
|
6834
|
+
const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
6048
6835
|
const row = top + Math.max(0, layout.cursorVisualRow - layout.windowStart);
|
|
6049
6836
|
const col = layout.cursorVisualCol + 3;
|
|
6837
|
+
const lastPromptRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
|
|
6050
6838
|
this.term.moveTo(
|
|
6051
6839
|
Math.min(col, this.term.width),
|
|
6052
|
-
Math.min(row,
|
|
6840
|
+
Math.min(row, lastPromptRow)
|
|
6053
6841
|
);
|
|
6054
6842
|
}
|
|
6055
6843
|
promptRows() {
|
|
@@ -6741,746 +7529,461 @@ var init_input = __esm({
|
|
|
6741
7529
|
};
|
|
6742
7530
|
return [];
|
|
6743
7531
|
}
|
|
6744
|
-
const matchIndices = this.findHistoryMatches(query);
|
|
6745
|
-
if (matchIndices.length === 0) {
|
|
6746
|
-
return [{ type: "escalate-search", query }];
|
|
6747
|
-
}
|
|
6748
|
-
this.historySearch = {
|
|
6749
|
-
query,
|
|
6750
|
-
matchIndices,
|
|
6751
|
-
cursor: 0,
|
|
6752
|
-
savedDraft: {
|
|
6753
|
-
buffer: [...this.buffer],
|
|
6754
|
-
row: this.row,
|
|
6755
|
-
col: this.col
|
|
6756
|
-
}
|
|
6757
|
-
};
|
|
6758
|
-
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
6759
|
-
return [];
|
|
6760
|
-
}
|
|
6761
|
-
// ^R advance. At the oldest match with a non-empty query, falls
|
|
6762
|
-
// through to scrollback search (same escalate path as a never-
|
|
6763
|
-
// matched startHistorySearch). With an empty query at the oldest
|
|
6764
|
-
// match (i.e. the user walked all history with no filter), advance
|
|
6765
|
-
// is a no-op so the buffer stays on the oldest entry.
|
|
6766
|
-
advanceHistorySearch() {
|
|
6767
|
-
if (this.historySearch === null) {
|
|
6768
|
-
return [];
|
|
6769
|
-
}
|
|
6770
|
-
const search = this.historySearch;
|
|
6771
|
-
const atOldest = search.cursor >= search.matchIndices.length - 1;
|
|
6772
|
-
if (atOldest) {
|
|
6773
|
-
if (search.query.length === 0) {
|
|
6774
|
-
return [];
|
|
6775
|
-
}
|
|
6776
|
-
const query = search.query;
|
|
6777
|
-
const draft = search.savedDraft;
|
|
6778
|
-
this.historySearch = null;
|
|
6779
|
-
this.buffer = [...draft.buffer];
|
|
6780
|
-
this.row = draft.row;
|
|
6781
|
-
this.col = draft.col;
|
|
6782
|
-
return [{ type: "escalate-search", query }];
|
|
6783
|
-
}
|
|
6784
|
-
search.cursor += 1;
|
|
6785
|
-
const idx = search.matchIndices[search.cursor];
|
|
6786
|
-
this.loadEntry(this.history[idx] ?? "");
|
|
6787
|
-
return [];
|
|
6788
|
-
}
|
|
6789
|
-
// ^S retreat — walk toward newer matches. No-op at the newest match
|
|
6790
|
-
// (no wrap, mirroring ^R no-wrap at the oldest).
|
|
6791
|
-
retreatHistorySearch() {
|
|
6792
|
-
if (this.historySearch === null) {
|
|
6793
|
-
return;
|
|
6794
|
-
}
|
|
6795
|
-
if (this.historySearch.cursor === 0) {
|
|
6796
|
-
return;
|
|
6797
|
-
}
|
|
6798
|
-
this.historySearch.cursor -= 1;
|
|
6799
|
-
const idx = this.historySearch.matchIndices[this.historySearch.cursor];
|
|
6800
|
-
this.loadEntry(this.history[idx] ?? "");
|
|
6801
|
-
}
|
|
6802
|
-
// Backspace / typing within search mode mutates the query and
|
|
6803
|
-
// re-searches. When the new query is empty, restore the saved
|
|
6804
|
-
// draft buffer (typically empty) and stay in search mode — the
|
|
6805
|
-
// user can keep typing. When the new query has matches, load the
|
|
6806
|
-
// top one. When the new query has no matches, escalate to scrollback
|
|
6807
|
-
// search so the typed term applies there instead.
|
|
6808
|
-
mutateHistorySearchQuery(newQuery) {
|
|
6809
|
-
if (this.historySearch === null) {
|
|
6810
|
-
return [];
|
|
6811
|
-
}
|
|
6812
|
-
if (newQuery.length === 0) {
|
|
6813
|
-
this.historySearch.query = "";
|
|
6814
|
-
this.historySearch.matchIndices = [];
|
|
6815
|
-
this.historySearch.cursor = 0;
|
|
6816
|
-
const draft = this.historySearch.savedDraft;
|
|
6817
|
-
this.buffer = [...draft.buffer];
|
|
6818
|
-
this.row = draft.row;
|
|
6819
|
-
this.col = draft.col;
|
|
6820
|
-
return [];
|
|
6821
|
-
}
|
|
6822
|
-
const matchIndices = this.findHistoryMatches(newQuery);
|
|
6823
|
-
if (matchIndices.length === 0) {
|
|
6824
|
-
const draft = this.historySearch.savedDraft;
|
|
6825
|
-
this.historySearch = null;
|
|
6826
|
-
this.buffer = [...draft.buffer];
|
|
6827
|
-
this.row = draft.row;
|
|
6828
|
-
this.col = draft.col;
|
|
6829
|
-
return [{ type: "escalate-search", query: newQuery }];
|
|
6830
|
-
}
|
|
6831
|
-
this.historySearch.query = newQuery;
|
|
6832
|
-
this.historySearch.matchIndices = matchIndices;
|
|
6833
|
-
this.historySearch.cursor = 0;
|
|
6834
|
-
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
6835
|
-
return [];
|
|
6836
|
-
}
|
|
6837
|
-
findHistoryMatches(query) {
|
|
6838
|
-
const out = [];
|
|
6839
|
-
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
6840
|
-
const entry = this.history[i] ?? "";
|
|
6841
|
-
if (query.length === 0 || entry.toLowerCase().includes(query)) {
|
|
6842
|
-
out.push(i);
|
|
6843
|
-
}
|
|
6844
|
-
}
|
|
6845
|
-
return out;
|
|
6846
|
-
}
|
|
6847
|
-
cancelHistorySearch() {
|
|
6848
|
-
if (this.historySearch === null) {
|
|
6849
|
-
return;
|
|
6850
|
-
}
|
|
6851
|
-
const draft = this.historySearch.savedDraft;
|
|
6852
|
-
this.historySearch = null;
|
|
6853
|
-
this.buffer = [...draft.buffer];
|
|
6854
|
-
this.row = draft.row;
|
|
6855
|
-
this.col = draft.col;
|
|
6856
|
-
}
|
|
6857
|
-
loadEntry(text) {
|
|
6858
|
-
this.buffer = text.split("\n");
|
|
6859
|
-
if (this.buffer.length === 0) {
|
|
6860
|
-
this.buffer = [""];
|
|
6861
|
-
}
|
|
6862
|
-
this.row = this.buffer.length - 1;
|
|
6863
|
-
this.col = (this.buffer[this.row] ?? "").length;
|
|
6864
|
-
}
|
|
6865
|
-
send() {
|
|
6866
|
-
const text = this.bufferText();
|
|
6867
|
-
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
6868
|
-
const index = this.queueIndex;
|
|
6869
|
-
const attachments2 = [...this.attachments];
|
|
6870
|
-
this.clearBuffer();
|
|
6871
|
-
if (text.trim().length === 0) {
|
|
6872
|
-
return [{ type: "queue-remove", index }];
|
|
6873
|
-
}
|
|
6874
|
-
return [{ type: "queue-edit", index, text, attachments: attachments2 }];
|
|
6875
|
-
}
|
|
6876
|
-
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
6877
|
-
return [];
|
|
6878
|
-
}
|
|
6879
|
-
const planMode = this.planMode;
|
|
6880
|
-
const attachments = [...this.attachments];
|
|
6881
|
-
this.clearBuffer();
|
|
6882
|
-
return [{ type: "send", text, planMode, attachments }];
|
|
6883
|
-
}
|
|
6884
|
-
// Home: jump to the very start of the prompt buffer. If we're already
|
|
6885
|
-
// there, fall through to scrolling the scrollback to its top.
|
|
6886
|
-
handleHome() {
|
|
6887
|
-
if (this.row !== 0 || this.col !== 0) {
|
|
6888
|
-
this.row = 0;
|
|
6889
|
-
this.col = 0;
|
|
6890
|
-
return [];
|
|
7532
|
+
const matchIndices = this.findHistoryMatches(query);
|
|
7533
|
+
if (matchIndices.length === 0) {
|
|
7534
|
+
return [{ type: "escalate-search", query }];
|
|
6891
7535
|
}
|
|
6892
|
-
|
|
7536
|
+
this.historySearch = {
|
|
7537
|
+
query,
|
|
7538
|
+
matchIndices,
|
|
7539
|
+
cursor: 0,
|
|
7540
|
+
savedDraft: {
|
|
7541
|
+
buffer: [...this.buffer],
|
|
7542
|
+
row: this.row,
|
|
7543
|
+
col: this.col
|
|
7544
|
+
}
|
|
7545
|
+
};
|
|
7546
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
7547
|
+
return [];
|
|
6893
7548
|
}
|
|
6894
|
-
//
|
|
6895
|
-
//
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
this.col = lastCol;
|
|
7549
|
+
// ^R advance. At the oldest match with a non-empty query, falls
|
|
7550
|
+
// through to scrollback search (same escalate path as a never-
|
|
7551
|
+
// matched startHistorySearch). With an empty query at the oldest
|
|
7552
|
+
// match (i.e. the user walked all history with no filter), advance
|
|
7553
|
+
// is a no-op so the buffer stays on the oldest entry.
|
|
7554
|
+
advanceHistorySearch() {
|
|
7555
|
+
if (this.historySearch === null) {
|
|
6902
7556
|
return [];
|
|
6903
7557
|
}
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
7558
|
+
const search = this.historySearch;
|
|
7559
|
+
const atOldest = search.cursor >= search.matchIndices.length - 1;
|
|
7560
|
+
if (atOldest) {
|
|
7561
|
+
if (search.query.length === 0) {
|
|
7562
|
+
return [];
|
|
7563
|
+
}
|
|
7564
|
+
const query = search.query;
|
|
7565
|
+
const draft = search.savedDraft;
|
|
7566
|
+
this.historySearch = null;
|
|
7567
|
+
this.buffer = [...draft.buffer];
|
|
7568
|
+
this.row = draft.row;
|
|
7569
|
+
this.col = draft.col;
|
|
7570
|
+
return [{ type: "escalate-search", query }];
|
|
6912
7571
|
}
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
7572
|
+
search.cursor += 1;
|
|
7573
|
+
const idx = search.matchIndices[search.cursor];
|
|
7574
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
7575
|
+
return [];
|
|
7576
|
+
}
|
|
7577
|
+
// ^S retreat — walk toward newer matches. No-op at the newest match
|
|
7578
|
+
// (no wrap, mirroring ^R no-wrap at the oldest).
|
|
7579
|
+
retreatHistorySearch() {
|
|
7580
|
+
if (this.historySearch === null) {
|
|
7581
|
+
return;
|
|
6922
7582
|
}
|
|
6923
|
-
if (this.
|
|
6924
|
-
return
|
|
7583
|
+
if (this.historySearch.cursor === 0) {
|
|
7584
|
+
return;
|
|
6925
7585
|
}
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
}
|
|
6930
|
-
});
|
|
6931
|
-
|
|
6932
|
-
// src/tui/clipboard.ts
|
|
6933
|
-
import { spawn as nodeSpawn } from "child_process";
|
|
6934
|
-
import fs14 from "fs/promises";
|
|
6935
|
-
import os4 from "os";
|
|
6936
|
-
import path10 from "path";
|
|
6937
|
-
async function readClipboard(envIn = {}) {
|
|
6938
|
-
const env = { ...defaultEnv, ...envIn };
|
|
6939
|
-
if (env.platform === "darwin") {
|
|
6940
|
-
return readMacOS(env);
|
|
6941
|
-
}
|
|
6942
|
-
if (env.platform === "linux") {
|
|
6943
|
-
return readLinux(env);
|
|
6944
|
-
}
|
|
6945
|
-
return {
|
|
6946
|
-
ok: false,
|
|
6947
|
-
reason: `clipboard paste is not supported on ${env.platform}`
|
|
6948
|
-
};
|
|
6949
|
-
}
|
|
6950
|
-
async function readMacOS(env) {
|
|
6951
|
-
const tmpPath = path10.join(
|
|
6952
|
-
env.tmpdir(),
|
|
6953
|
-
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
6954
|
-
);
|
|
6955
|
-
const script = [
|
|
6956
|
-
"set png_data to the clipboard as \xABclass PNGf\xBB",
|
|
6957
|
-
`set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
|
|
6958
|
-
"write png_data to out_file",
|
|
6959
|
-
"close access out_file"
|
|
6960
|
-
];
|
|
6961
|
-
const args = [];
|
|
6962
|
-
for (const line of script) {
|
|
6963
|
-
args.push("-e", line);
|
|
6964
|
-
}
|
|
6965
|
-
try {
|
|
6966
|
-
await run2(env.spawn, "osascript", args);
|
|
6967
|
-
const img = await readFileAsAttachment(tmpPath, true);
|
|
6968
|
-
if (img.ok) {
|
|
6969
|
-
return img;
|
|
6970
|
-
}
|
|
6971
|
-
if (img.reason.startsWith("clipboard image is")) {
|
|
6972
|
-
return img;
|
|
6973
|
-
}
|
|
6974
|
-
} catch {
|
|
6975
|
-
await fs14.unlink(tmpPath).catch(() => void 0);
|
|
6976
|
-
}
|
|
6977
|
-
try {
|
|
6978
|
-
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
6979
|
-
if (buf.length === 0) {
|
|
6980
|
-
return { ok: false, reason: "clipboard is empty" };
|
|
6981
|
-
}
|
|
6982
|
-
return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
|
|
6983
|
-
} catch {
|
|
6984
|
-
return { ok: false, reason: "clipboard read failed" };
|
|
6985
|
-
}
|
|
6986
|
-
}
|
|
6987
|
-
async function readLinux(env) {
|
|
6988
|
-
const tool = await detectLinuxTool(env);
|
|
6989
|
-
if (!tool) {
|
|
6990
|
-
return {
|
|
6991
|
-
ok: false,
|
|
6992
|
-
reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
|
|
6993
|
-
};
|
|
6994
|
-
}
|
|
6995
|
-
try {
|
|
6996
|
-
const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
|
|
6997
|
-
if (buf.length > 0) {
|
|
6998
|
-
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
6999
|
-
return {
|
|
7000
|
-
ok: false,
|
|
7001
|
-
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7002
|
-
};
|
|
7586
|
+
this.historySearch.cursor -= 1;
|
|
7587
|
+
const idx = this.historySearch.matchIndices[this.historySearch.cursor];
|
|
7588
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
7003
7589
|
}
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
textArgs: ["-n"]
|
|
7039
|
-
};
|
|
7040
|
-
}
|
|
7041
|
-
if (env.env.DISPLAY && await which(env, "xclip")) {
|
|
7042
|
-
return {
|
|
7043
|
-
cmd: "xclip",
|
|
7044
|
-
imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
7045
|
-
textArgs: ["-selection", "clipboard", "-o"]
|
|
7046
|
-
};
|
|
7047
|
-
}
|
|
7048
|
-
return null;
|
|
7049
|
-
}
|
|
7050
|
-
function normalizeText(text) {
|
|
7051
|
-
return text.replace(/\r\n?/g, "\n");
|
|
7052
|
-
}
|
|
7053
|
-
async function which(env, cmd) {
|
|
7054
|
-
try {
|
|
7055
|
-
await run2(env.spawn, "which", [cmd]);
|
|
7056
|
-
return true;
|
|
7057
|
-
} catch {
|
|
7058
|
-
return false;
|
|
7059
|
-
}
|
|
7060
|
-
}
|
|
7061
|
-
async function readFileAsAttachment(p, unlinkAfter) {
|
|
7062
|
-
try {
|
|
7063
|
-
const buf = await fs14.readFile(p);
|
|
7064
|
-
if (unlinkAfter) {
|
|
7065
|
-
await fs14.unlink(p).catch(() => void 0);
|
|
7066
|
-
}
|
|
7067
|
-
if (buf.length === 0) {
|
|
7068
|
-
return { ok: false, reason: "no image on clipboard" };
|
|
7069
|
-
}
|
|
7070
|
-
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
7071
|
-
return {
|
|
7072
|
-
ok: false,
|
|
7073
|
-
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7074
|
-
};
|
|
7075
|
-
}
|
|
7076
|
-
const mimeType = mimeFromExtension(p) ?? "image/png";
|
|
7077
|
-
return {
|
|
7078
|
-
ok: true,
|
|
7079
|
-
kind: "image",
|
|
7080
|
-
attachment: {
|
|
7081
|
-
mimeType,
|
|
7082
|
-
data: buf.toString("base64"),
|
|
7083
|
-
sizeBytes: buf.length
|
|
7590
|
+
// Backspace / typing within search mode mutates the query and
|
|
7591
|
+
// re-searches. When the new query is empty, restore the saved
|
|
7592
|
+
// draft buffer (typically empty) and stay in search mode — the
|
|
7593
|
+
// user can keep typing. When the new query has matches, load the
|
|
7594
|
+
// top one. When the new query has no matches, escalate to scrollback
|
|
7595
|
+
// search so the typed term applies there instead.
|
|
7596
|
+
mutateHistorySearchQuery(newQuery) {
|
|
7597
|
+
if (this.historySearch === null) {
|
|
7598
|
+
return [];
|
|
7599
|
+
}
|
|
7600
|
+
if (newQuery.length === 0) {
|
|
7601
|
+
this.historySearch.query = "";
|
|
7602
|
+
this.historySearch.matchIndices = [];
|
|
7603
|
+
this.historySearch.cursor = 0;
|
|
7604
|
+
const draft = this.historySearch.savedDraft;
|
|
7605
|
+
this.buffer = [...draft.buffer];
|
|
7606
|
+
this.row = draft.row;
|
|
7607
|
+
this.col = draft.col;
|
|
7608
|
+
return [];
|
|
7609
|
+
}
|
|
7610
|
+
const matchIndices = this.findHistoryMatches(newQuery);
|
|
7611
|
+
if (matchIndices.length === 0) {
|
|
7612
|
+
const draft = this.historySearch.savedDraft;
|
|
7613
|
+
this.historySearch = null;
|
|
7614
|
+
this.buffer = [...draft.buffer];
|
|
7615
|
+
this.row = draft.row;
|
|
7616
|
+
this.col = draft.col;
|
|
7617
|
+
return [{ type: "escalate-search", query: newQuery }];
|
|
7618
|
+
}
|
|
7619
|
+
this.historySearch.query = newQuery;
|
|
7620
|
+
this.historySearch.matchIndices = matchIndices;
|
|
7621
|
+
this.historySearch.cursor = 0;
|
|
7622
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
7623
|
+
return [];
|
|
7084
7624
|
}
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
proc.stderr?.on("data", () => void 0);
|
|
7095
|
-
proc.on("error", reject);
|
|
7096
|
-
proc.on("close", (code) => {
|
|
7097
|
-
if (code === 0) {
|
|
7098
|
-
resolve5();
|
|
7099
|
-
} else {
|
|
7100
|
-
reject(new Error(`${cmd} exited ${code}`));
|
|
7625
|
+
findHistoryMatches(query) {
|
|
7626
|
+
const out = [];
|
|
7627
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
7628
|
+
const entry = this.history[i] ?? "";
|
|
7629
|
+
if (query.length === 0 || entry.toLowerCase().includes(query)) {
|
|
7630
|
+
out.push(i);
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7633
|
+
return out;
|
|
7101
7634
|
}
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
let settled = false;
|
|
7112
|
-
const settle = () => {
|
|
7113
|
-
if (settled || !stdoutEnded || closedCode === null) {
|
|
7114
|
-
return;
|
|
7635
|
+
cancelHistorySearch() {
|
|
7636
|
+
if (this.historySearch === null) {
|
|
7637
|
+
return;
|
|
7638
|
+
}
|
|
7639
|
+
const draft = this.historySearch.savedDraft;
|
|
7640
|
+
this.historySearch = null;
|
|
7641
|
+
this.buffer = [...draft.buffer];
|
|
7642
|
+
this.row = draft.row;
|
|
7643
|
+
this.col = draft.col;
|
|
7115
7644
|
}
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7645
|
+
loadEntry(text) {
|
|
7646
|
+
this.buffer = text.split("\n");
|
|
7647
|
+
if (this.buffer.length === 0) {
|
|
7648
|
+
this.buffer = [""];
|
|
7649
|
+
}
|
|
7650
|
+
this.row = this.buffer.length - 1;
|
|
7651
|
+
this.col = (this.buffer[this.row] ?? "").length;
|
|
7121
7652
|
}
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7653
|
+
send() {
|
|
7654
|
+
const text = this.bufferText();
|
|
7655
|
+
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
7656
|
+
const index = this.queueIndex;
|
|
7657
|
+
const attachments2 = [...this.attachments];
|
|
7658
|
+
this.clearBuffer();
|
|
7659
|
+
if (text.trim().length === 0) {
|
|
7660
|
+
return [{ type: "queue-remove", index }];
|
|
7661
|
+
}
|
|
7662
|
+
return [{ type: "queue-edit", index, text, attachments: attachments2 }];
|
|
7663
|
+
}
|
|
7664
|
+
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
7665
|
+
return [];
|
|
7666
|
+
}
|
|
7667
|
+
const planMode = this.planMode;
|
|
7668
|
+
const attachments = [...this.attachments];
|
|
7669
|
+
this.clearBuffer();
|
|
7670
|
+
return [{ type: "send", text, planMode, attachments }];
|
|
7134
7671
|
}
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
}
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
});
|
|
7157
|
-
|
|
7158
|
-
// src/tui/completion.ts
|
|
7159
|
-
function longestCommonPrefix(names) {
|
|
7160
|
-
if (names.length === 0) {
|
|
7161
|
-
return "";
|
|
7162
|
-
}
|
|
7163
|
-
let prefix = names[0] ?? "";
|
|
7164
|
-
for (let i = 1; i < names.length; i++) {
|
|
7165
|
-
const n = names[i] ?? "";
|
|
7166
|
-
let j = 0;
|
|
7167
|
-
while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
|
|
7168
|
-
j += 1;
|
|
7169
|
-
}
|
|
7170
|
-
prefix = prefix.slice(0, j);
|
|
7171
|
-
if (prefix.length === 0) {
|
|
7172
|
-
break;
|
|
7173
|
-
}
|
|
7174
|
-
}
|
|
7175
|
-
return prefix;
|
|
7176
|
-
}
|
|
7177
|
-
function computeTabCompletion(args) {
|
|
7178
|
-
const { matches, firstLine: firstLine3 } = args;
|
|
7179
|
-
if (matches.length === 0) {
|
|
7180
|
-
return null;
|
|
7181
|
-
}
|
|
7182
|
-
const space = firstLine3.indexOf(" ");
|
|
7183
|
-
const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
|
|
7184
|
-
const tail = space === -1 ? "" : firstLine3.slice(space);
|
|
7185
|
-
if (matches.length === 1) {
|
|
7186
|
-
const name = matches[0] ?? "";
|
|
7187
|
-
const suffix = tail.startsWith(" ") ? "" : " ";
|
|
7188
|
-
return name + suffix + tail;
|
|
7189
|
-
}
|
|
7190
|
-
const commonPrefix = longestCommonPrefix(matches);
|
|
7191
|
-
if (commonPrefix.length <= typedPrefix.length) {
|
|
7192
|
-
return null;
|
|
7193
|
-
}
|
|
7194
|
-
return commonPrefix + tail;
|
|
7195
|
-
}
|
|
7196
|
-
var init_completion = __esm({
|
|
7197
|
-
"src/tui/completion.ts"() {
|
|
7198
|
-
"use strict";
|
|
7199
|
-
}
|
|
7200
|
-
});
|
|
7201
|
-
|
|
7202
|
-
// src/tui/render-update.ts
|
|
7203
|
-
import stripAnsi from "strip-ansi";
|
|
7204
|
-
function sanitizeWireText(text) {
|
|
7205
|
-
return stripAnsi(text).replace(STRIP_CONTROLS, "");
|
|
7206
|
-
}
|
|
7207
|
-
function sanitizeSingleLine(text) {
|
|
7208
|
-
return sanitizeWireText(text).replace(/[\n\t]+/g, " ").replace(/ +/g, " ").trim();
|
|
7209
|
-
}
|
|
7210
|
-
function mapUpdate(update) {
|
|
7211
|
-
if (!update || typeof update !== "object") {
|
|
7212
|
-
return null;
|
|
7213
|
-
}
|
|
7214
|
-
const u = update;
|
|
7215
|
-
const tag = u.sessionUpdate ?? u.kind;
|
|
7216
|
-
if (typeof tag !== "string") {
|
|
7217
|
-
return null;
|
|
7218
|
-
}
|
|
7219
|
-
switch (tag) {
|
|
7220
|
-
case "agent_message_chunk":
|
|
7221
|
-
return mapAgentText(u);
|
|
7222
|
-
case "agent_thought_chunk":
|
|
7223
|
-
case "agent_thought":
|
|
7224
|
-
return mapAgentThought(u);
|
|
7225
|
-
case "user_message_chunk":
|
|
7226
|
-
return mapUserText(u);
|
|
7227
|
-
case "prompt_received":
|
|
7228
|
-
return mapPromptReceived(u);
|
|
7229
|
-
case "tool_call":
|
|
7230
|
-
return mapToolCall(u);
|
|
7231
|
-
case "tool_call_update":
|
|
7232
|
-
return mapToolCallUpdate(u);
|
|
7233
|
-
case "plan":
|
|
7234
|
-
return mapPlan(u);
|
|
7235
|
-
case "current_mode_update":
|
|
7236
|
-
return mapMode(u);
|
|
7237
|
-
case "current_model_update":
|
|
7238
|
-
return mapModel(u);
|
|
7239
|
-
case "turn_complete":
|
|
7240
|
-
return mapTurnComplete(u);
|
|
7241
|
-
case "usage_update":
|
|
7242
|
-
return mapUsage(u);
|
|
7243
|
-
case "available_commands_update":
|
|
7244
|
-
return mapAvailableCommands(u);
|
|
7245
|
-
case "session_info_update":
|
|
7246
|
-
return mapSessionInfo(u);
|
|
7247
|
-
default:
|
|
7248
|
-
return { kind: "unknown", sessionUpdate: tag, raw: update };
|
|
7249
|
-
}
|
|
7250
|
-
}
|
|
7251
|
-
function mapSessionInfo(u) {
|
|
7252
|
-
const rawTitle = readString(u, "title");
|
|
7253
|
-
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
7254
|
-
const meta = u._meta;
|
|
7255
|
-
let agentId;
|
|
7256
|
-
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
7257
|
-
const ns = meta["hydra-acp"];
|
|
7258
|
-
if (ns && typeof ns === "object" && !Array.isArray(ns)) {
|
|
7259
|
-
const candidate = ns.agentId;
|
|
7260
|
-
if (typeof candidate === "string") {
|
|
7261
|
-
agentId = candidate;
|
|
7672
|
+
// Home: jump to the very start of the prompt buffer. If we're already
|
|
7673
|
+
// there, fall through to scrolling the scrollback to its top.
|
|
7674
|
+
handleHome() {
|
|
7675
|
+
if (this.row !== 0 || this.col !== 0) {
|
|
7676
|
+
this.row = 0;
|
|
7677
|
+
this.col = 0;
|
|
7678
|
+
return [];
|
|
7679
|
+
}
|
|
7680
|
+
return [{ type: "scroll-to-top" }];
|
|
7681
|
+
}
|
|
7682
|
+
// End: jump to the end of the last line of the prompt buffer. Already
|
|
7683
|
+
// there → scroll the scrollback to the bottom (newest).
|
|
7684
|
+
handleEnd() {
|
|
7685
|
+
const lastRow = this.buffer.length - 1;
|
|
7686
|
+
const lastCol = (this.buffer[lastRow] ?? "").length;
|
|
7687
|
+
if (this.row !== lastRow || this.col !== lastCol) {
|
|
7688
|
+
this.row = lastRow;
|
|
7689
|
+
this.col = lastCol;
|
|
7690
|
+
return [];
|
|
7691
|
+
}
|
|
7692
|
+
return [{ type: "scroll-to-bottom" }];
|
|
7262
7693
|
}
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7694
|
+
handleCtrlC() {
|
|
7695
|
+
if (this.queueIndex >= 0) {
|
|
7696
|
+
const index = this.queueIndex;
|
|
7697
|
+
this.queueIndex = -1;
|
|
7698
|
+
this.restoreDraft();
|
|
7699
|
+
return [{ type: "queue-remove", index }];
|
|
7700
|
+
}
|
|
7701
|
+
if (!this.bufferIsEmpty() || this.attachments.length > 0) {
|
|
7702
|
+
this.buffer = [""];
|
|
7703
|
+
this.row = 0;
|
|
7704
|
+
this.col = 0;
|
|
7705
|
+
this.attachments = [];
|
|
7706
|
+
this.historyIndex = -1;
|
|
7707
|
+
this.savedDraft = null;
|
|
7708
|
+
this.savedAttachments = null;
|
|
7709
|
+
return [];
|
|
7710
|
+
}
|
|
7711
|
+
if (this.turnRunning) {
|
|
7712
|
+
return [{ type: "cancel" }];
|
|
7713
|
+
}
|
|
7714
|
+
return [{ type: "exit" }];
|
|
7715
|
+
}
|
|
7716
|
+
};
|
|
7267
7717
|
}
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7718
|
+
});
|
|
7719
|
+
|
|
7720
|
+
// src/tui/clipboard.ts
|
|
7721
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
7722
|
+
import fs14 from "fs/promises";
|
|
7723
|
+
import os4 from "os";
|
|
7724
|
+
import path10 from "path";
|
|
7725
|
+
async function readClipboard(envIn = {}) {
|
|
7726
|
+
const env = { ...defaultEnv, ...envIn };
|
|
7727
|
+
if (env.platform === "darwin") {
|
|
7728
|
+
return readMacOS(env);
|
|
7271
7729
|
}
|
|
7272
|
-
if (
|
|
7273
|
-
|
|
7730
|
+
if (env.platform === "linux") {
|
|
7731
|
+
return readLinux(env);
|
|
7274
7732
|
}
|
|
7275
|
-
return
|
|
7733
|
+
return {
|
|
7734
|
+
ok: false,
|
|
7735
|
+
reason: `clipboard paste is not supported on ${env.platform}`
|
|
7736
|
+
};
|
|
7276
7737
|
}
|
|
7277
|
-
function
|
|
7278
|
-
|
|
7279
|
-
|
|
7738
|
+
async function readMacOS(env) {
|
|
7739
|
+
const tmpPath = path10.join(
|
|
7740
|
+
env.tmpdir(),
|
|
7741
|
+
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
7742
|
+
);
|
|
7743
|
+
const script = [
|
|
7744
|
+
"set png_data to the clipboard as \xABclass PNGf\xBB",
|
|
7745
|
+
`set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
|
|
7746
|
+
"write png_data to out_file",
|
|
7747
|
+
"close access out_file"
|
|
7748
|
+
];
|
|
7749
|
+
const args = [];
|
|
7750
|
+
for (const line of script) {
|
|
7751
|
+
args.push("-e", line);
|
|
7280
7752
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
const c = raw;
|
|
7287
|
-
if (typeof c.name !== "string" || c.name.length === 0) {
|
|
7288
|
-
continue;
|
|
7753
|
+
try {
|
|
7754
|
+
await run2(env.spawn, "osascript", args);
|
|
7755
|
+
const img = await readFileAsAttachment(tmpPath, true);
|
|
7756
|
+
if (img.ok) {
|
|
7757
|
+
return img;
|
|
7289
7758
|
}
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
if (typeof c.description === "string") {
|
|
7293
|
-
cmd.description = sanitizeSingleLine(c.description);
|
|
7759
|
+
if (img.reason.startsWith("clipboard image is")) {
|
|
7760
|
+
return img;
|
|
7294
7761
|
}
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
return out;
|
|
7298
|
-
}
|
|
7299
|
-
function mapAvailableCommands(u) {
|
|
7300
|
-
const list = u.availableCommands ?? u.commands;
|
|
7301
|
-
if (!Array.isArray(list)) {
|
|
7302
|
-
return null;
|
|
7303
|
-
}
|
|
7304
|
-
return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
|
|
7305
|
-
}
|
|
7306
|
-
function mapUsage(u) {
|
|
7307
|
-
const event = { kind: "usage-update" };
|
|
7308
|
-
if (typeof u.used === "number") {
|
|
7309
|
-
event.used = u.used;
|
|
7310
|
-
}
|
|
7311
|
-
if (typeof u.size === "number") {
|
|
7312
|
-
event.size = u.size;
|
|
7762
|
+
} catch {
|
|
7763
|
+
await fs14.unlink(tmpPath).catch(() => void 0);
|
|
7313
7764
|
}
|
|
7314
|
-
|
|
7315
|
-
const
|
|
7316
|
-
if (
|
|
7317
|
-
|
|
7318
|
-
}
|
|
7319
|
-
if (typeof cost.currency === "string") {
|
|
7320
|
-
event.costCurrency = cost.currency;
|
|
7765
|
+
try {
|
|
7766
|
+
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
7767
|
+
if (buf.length === 0) {
|
|
7768
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
7321
7769
|
}
|
|
7770
|
+
return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
|
|
7771
|
+
} catch {
|
|
7772
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
7322
7773
|
}
|
|
7323
|
-
return event;
|
|
7324
|
-
}
|
|
7325
|
-
function mapAgentText(u) {
|
|
7326
|
-
const text = extractContentText(u.content);
|
|
7327
|
-
if (text === null) {
|
|
7328
|
-
return null;
|
|
7329
|
-
}
|
|
7330
|
-
return { kind: "agent-text", text };
|
|
7331
7774
|
}
|
|
7332
|
-
function
|
|
7333
|
-
const
|
|
7334
|
-
if (
|
|
7335
|
-
return
|
|
7775
|
+
async function readLinux(env) {
|
|
7776
|
+
const tool = await detectLinuxTool(env);
|
|
7777
|
+
if (!tool) {
|
|
7778
|
+
return {
|
|
7779
|
+
ok: false,
|
|
7780
|
+
reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
|
|
7781
|
+
};
|
|
7336
7782
|
}
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7783
|
+
try {
|
|
7784
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
|
|
7785
|
+
if (buf.length > 0) {
|
|
7786
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
7787
|
+
return {
|
|
7788
|
+
ok: false,
|
|
7789
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7790
|
+
};
|
|
7791
|
+
}
|
|
7792
|
+
return {
|
|
7793
|
+
ok: true,
|
|
7794
|
+
kind: "image",
|
|
7795
|
+
attachment: {
|
|
7796
|
+
mimeType: "image/png",
|
|
7797
|
+
data: buf.toString("base64"),
|
|
7798
|
+
sizeBytes: buf.length
|
|
7799
|
+
}
|
|
7800
|
+
};
|
|
7345
7801
|
}
|
|
7802
|
+
} catch {
|
|
7346
7803
|
}
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7804
|
+
try {
|
|
7805
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
|
|
7806
|
+
if (buf.length === 0) {
|
|
7807
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
7808
|
+
}
|
|
7809
|
+
return {
|
|
7810
|
+
ok: true,
|
|
7811
|
+
kind: "text",
|
|
7812
|
+
text: normalizeText(buf.toString("utf-8"))
|
|
7813
|
+
};
|
|
7814
|
+
} catch {
|
|
7815
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
7357
7816
|
}
|
|
7358
|
-
return { kind: "user-text", text: promptText };
|
|
7359
7817
|
}
|
|
7360
|
-
function
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
if (status !== void 0) {
|
|
7371
|
-
event.status = status;
|
|
7818
|
+
async function detectLinuxTool(env) {
|
|
7819
|
+
if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
|
|
7820
|
+
return {
|
|
7821
|
+
cmd: "wl-paste",
|
|
7822
|
+
imageArgs: ["-t", "image/png"],
|
|
7823
|
+
// -n: drop trailing newline wl-paste adds by default. We further
|
|
7824
|
+
// normalize line endings below, but this avoids a spurious
|
|
7825
|
+
// empty trailing row from a single-line clipboard text.
|
|
7826
|
+
textArgs: ["-n"]
|
|
7827
|
+
};
|
|
7372
7828
|
}
|
|
7373
|
-
if (
|
|
7374
|
-
|
|
7829
|
+
if (env.env.DISPLAY && await which(env, "xclip")) {
|
|
7830
|
+
return {
|
|
7831
|
+
cmd: "xclip",
|
|
7832
|
+
imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
7833
|
+
textArgs: ["-selection", "clipboard", "-o"]
|
|
7834
|
+
};
|
|
7375
7835
|
}
|
|
7376
|
-
return
|
|
7836
|
+
return null;
|
|
7377
7837
|
}
|
|
7378
|
-
function
|
|
7379
|
-
|
|
7380
|
-
if (!toolCallId) {
|
|
7381
|
-
return null;
|
|
7382
|
-
}
|
|
7383
|
-
const rawTitle = readString(u, "title");
|
|
7384
|
-
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
7385
|
-
const status = readString(u, "status");
|
|
7386
|
-
const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
|
|
7387
|
-
if (!meaningful) {
|
|
7388
|
-
return null;
|
|
7389
|
-
}
|
|
7390
|
-
const event = { kind: "tool-call-update", toolCallId };
|
|
7391
|
-
if (title !== void 0) {
|
|
7392
|
-
event.title = title;
|
|
7393
|
-
}
|
|
7394
|
-
if (status !== void 0) {
|
|
7395
|
-
event.status = status;
|
|
7396
|
-
}
|
|
7397
|
-
return event;
|
|
7838
|
+
function normalizeText(text) {
|
|
7839
|
+
return text.replace(/\r\n?/g, "\n");
|
|
7398
7840
|
}
|
|
7399
|
-
function
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
return
|
|
7841
|
+
async function which(env, cmd) {
|
|
7842
|
+
try {
|
|
7843
|
+
await run2(env.spawn, "which", [cmd]);
|
|
7844
|
+
return true;
|
|
7845
|
+
} catch {
|
|
7846
|
+
return false;
|
|
7403
7847
|
}
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
|
|
7411
|
-
if (!content) {
|
|
7412
|
-
continue;
|
|
7848
|
+
}
|
|
7849
|
+
async function readFileAsAttachment(p, unlinkAfter) {
|
|
7850
|
+
try {
|
|
7851
|
+
const buf = await fs14.readFile(p);
|
|
7852
|
+
if (unlinkAfter) {
|
|
7853
|
+
await fs14.unlink(p).catch(() => void 0);
|
|
7413
7854
|
}
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
entry.status = e.status;
|
|
7855
|
+
if (buf.length === 0) {
|
|
7856
|
+
return { ok: false, reason: "no image on clipboard" };
|
|
7417
7857
|
}
|
|
7418
|
-
if (
|
|
7419
|
-
|
|
7858
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
7859
|
+
return {
|
|
7860
|
+
ok: false,
|
|
7861
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7862
|
+
};
|
|
7420
7863
|
}
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7864
|
+
const mimeType = mimeFromExtension(p) ?? "image/png";
|
|
7865
|
+
return {
|
|
7866
|
+
ok: true,
|
|
7867
|
+
kind: "image",
|
|
7868
|
+
attachment: {
|
|
7869
|
+
mimeType,
|
|
7870
|
+
data: buf.toString("base64"),
|
|
7871
|
+
sizeBytes: buf.length
|
|
7872
|
+
}
|
|
7873
|
+
};
|
|
7874
|
+
} catch {
|
|
7875
|
+
return { ok: false, reason: "failed to read clipboard image" };
|
|
7429
7876
|
}
|
|
7430
|
-
return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
|
|
7431
7877
|
}
|
|
7432
|
-
function
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7878
|
+
function run2(spawn6, cmd, args) {
|
|
7879
|
+
return new Promise((resolve5, reject) => {
|
|
7880
|
+
const proc = spawn6(cmd, args);
|
|
7881
|
+
proc.stdout?.on("data", () => void 0);
|
|
7882
|
+
proc.stderr?.on("data", () => void 0);
|
|
7883
|
+
proc.on("error", reject);
|
|
7884
|
+
proc.on("close", (code) => {
|
|
7885
|
+
if (code === 0) {
|
|
7886
|
+
resolve5();
|
|
7887
|
+
} else {
|
|
7888
|
+
reject(new Error(`${cmd} exited ${code}`));
|
|
7889
|
+
}
|
|
7890
|
+
});
|
|
7891
|
+
});
|
|
7438
7892
|
}
|
|
7439
|
-
function
|
|
7440
|
-
|
|
7441
|
-
|
|
7893
|
+
function runCapture(spawn6, cmd, args) {
|
|
7894
|
+
return new Promise((resolve5, reject) => {
|
|
7895
|
+
const proc = spawn6(cmd, args);
|
|
7896
|
+
const chunks = [];
|
|
7897
|
+
let stdoutEnded = proc.stdout === null;
|
|
7898
|
+
let closedCode = null;
|
|
7899
|
+
let settled = false;
|
|
7900
|
+
const settle = () => {
|
|
7901
|
+
if (settled || !stdoutEnded || closedCode === null) {
|
|
7902
|
+
return;
|
|
7903
|
+
}
|
|
7904
|
+
settled = true;
|
|
7905
|
+
if (closedCode === 0) {
|
|
7906
|
+
resolve5(Buffer.concat(chunks));
|
|
7907
|
+
} else {
|
|
7908
|
+
reject(new Error(`${cmd} exited ${closedCode}`));
|
|
7909
|
+
}
|
|
7910
|
+
};
|
|
7911
|
+
proc.stdout?.on("data", (chunk) => {
|
|
7912
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
7913
|
+
});
|
|
7914
|
+
proc.stdout?.on("end", () => {
|
|
7915
|
+
stdoutEnded = true;
|
|
7916
|
+
settle();
|
|
7917
|
+
});
|
|
7918
|
+
proc.stderr?.on("data", () => void 0);
|
|
7919
|
+
proc.on("error", (err) => {
|
|
7920
|
+
if (settled) {
|
|
7921
|
+
return;
|
|
7922
|
+
}
|
|
7923
|
+
settled = true;
|
|
7924
|
+
reject(err);
|
|
7925
|
+
});
|
|
7926
|
+
proc.on("close", (code) => {
|
|
7927
|
+
closedCode = code ?? 0;
|
|
7928
|
+
settle();
|
|
7929
|
+
});
|
|
7930
|
+
});
|
|
7442
7931
|
}
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7932
|
+
var defaultEnv;
|
|
7933
|
+
var init_clipboard = __esm({
|
|
7934
|
+
"src/tui/clipboard.ts"() {
|
|
7935
|
+
"use strict";
|
|
7936
|
+
init_attachments();
|
|
7937
|
+
defaultEnv = {
|
|
7938
|
+
platform: process.platform,
|
|
7939
|
+
env: process.env,
|
|
7940
|
+
spawn: nodeSpawn,
|
|
7941
|
+
tmpdir: os4.tmpdir
|
|
7942
|
+
};
|
|
7449
7943
|
}
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7944
|
+
});
|
|
7945
|
+
|
|
7946
|
+
// src/tui/completion.ts
|
|
7947
|
+
function longestCommonPrefix(names) {
|
|
7948
|
+
if (names.length === 0) {
|
|
7949
|
+
return "";
|
|
7453
7950
|
}
|
|
7454
|
-
|
|
7455
|
-
|
|
7951
|
+
let prefix = names[0] ?? "";
|
|
7952
|
+
for (let i = 1; i < names.length; i++) {
|
|
7953
|
+
const n = names[i] ?? "";
|
|
7954
|
+
let j = 0;
|
|
7955
|
+
while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
|
|
7956
|
+
j += 1;
|
|
7957
|
+
}
|
|
7958
|
+
prefix = prefix.slice(0, j);
|
|
7959
|
+
if (prefix.length === 0) {
|
|
7960
|
+
break;
|
|
7961
|
+
}
|
|
7456
7962
|
}
|
|
7457
|
-
return
|
|
7963
|
+
return prefix;
|
|
7458
7964
|
}
|
|
7459
|
-
function
|
|
7460
|
-
|
|
7965
|
+
function computeTabCompletion(args) {
|
|
7966
|
+
const { matches, firstLine: firstLine3 } = args;
|
|
7967
|
+
if (matches.length === 0) {
|
|
7461
7968
|
return null;
|
|
7462
7969
|
}
|
|
7463
|
-
const
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7970
|
+
const space = firstLine3.indexOf(" ");
|
|
7971
|
+
const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
|
|
7972
|
+
const tail = space === -1 ? "" : firstLine3.slice(space);
|
|
7973
|
+
if (matches.length === 1) {
|
|
7974
|
+
const name = matches[0] ?? "";
|
|
7975
|
+
const suffix = tail.startsWith(" ") ? "" : " ";
|
|
7976
|
+
return name + suffix + tail;
|
|
7469
7977
|
}
|
|
7470
|
-
|
|
7978
|
+
const commonPrefix = longestCommonPrefix(matches);
|
|
7979
|
+
if (commonPrefix.length <= typedPrefix.length) {
|
|
7471
7980
|
return null;
|
|
7472
7981
|
}
|
|
7473
|
-
return
|
|
7474
|
-
}
|
|
7475
|
-
function readString(u, key) {
|
|
7476
|
-
const v = u[key];
|
|
7477
|
-
return typeof v === "string" ? v : void 0;
|
|
7982
|
+
return commonPrefix + tail;
|
|
7478
7983
|
}
|
|
7479
|
-
var
|
|
7480
|
-
|
|
7481
|
-
"src/tui/render-update.ts"() {
|
|
7984
|
+
var init_completion = __esm({
|
|
7985
|
+
"src/tui/completion.ts"() {
|
|
7482
7986
|
"use strict";
|
|
7483
|
-
STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
7484
7987
|
}
|
|
7485
7988
|
});
|
|
7486
7989
|
|
|
@@ -7693,7 +8196,7 @@ function formatBlock(text, prefix, bodyStyle, prefixStyle, sentBy, fillRow) {
|
|
|
7693
8196
|
}
|
|
7694
8197
|
return out;
|
|
7695
8198
|
}
|
|
7696
|
-
function
|
|
8199
|
+
function formatToolLine2(state) {
|
|
7697
8200
|
const initial = state.initialTitle;
|
|
7698
8201
|
const latest = state.latestTitle;
|
|
7699
8202
|
const initialLc = initial.toLowerCase();
|
|
@@ -8096,6 +8599,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8096
8599
|
let initialModel;
|
|
8097
8600
|
let initialMode;
|
|
8098
8601
|
let initialCommands;
|
|
8602
|
+
let initialUsage;
|
|
8099
8603
|
let initialTurnStartedAt;
|
|
8100
8604
|
if (ctx.sessionId === "__new__") {
|
|
8101
8605
|
const hydraNewMeta = {};
|
|
@@ -8125,6 +8629,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8125
8629
|
}
|
|
8126
8630
|
initialModel = hydraMeta.currentModel;
|
|
8127
8631
|
initialMode = hydraMeta.currentMode;
|
|
8632
|
+
initialUsage = hydraMeta.currentUsage;
|
|
8128
8633
|
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
8129
8634
|
if (hydraMeta.availableCommands) {
|
|
8130
8635
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
@@ -8150,6 +8655,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8150
8655
|
}
|
|
8151
8656
|
initialModel = hydraMeta.currentModel;
|
|
8152
8657
|
initialMode = hydraMeta.currentMode;
|
|
8658
|
+
initialUsage = hydraMeta.currentUsage;
|
|
8153
8659
|
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
8154
8660
|
if (hydraMeta.availableCommands) {
|
|
8155
8661
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
@@ -8356,18 +8862,25 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8356
8862
|
}
|
|
8357
8863
|
return true;
|
|
8358
8864
|
};
|
|
8359
|
-
const
|
|
8865
|
+
const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
|
|
8866
|
+
const usage = { ...initialUsage ?? {} };
|
|
8360
8867
|
screen.start();
|
|
8361
|
-
screen.
|
|
8362
|
-
agent:
|
|
8868
|
+
screen.setSessionbar({
|
|
8869
|
+
agent: sessionbarAgent,
|
|
8363
8870
|
cwd: resolvedCwd,
|
|
8364
8871
|
sessionId: resolvedSessionId,
|
|
8365
8872
|
title: resolvedTitle,
|
|
8366
|
-
model: initialModel
|
|
8873
|
+
model: initialModel,
|
|
8874
|
+
usage: { ...usage }
|
|
8367
8875
|
});
|
|
8368
8876
|
if (initialMode) {
|
|
8369
8877
|
screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
|
|
8370
8878
|
}
|
|
8879
|
+
void getPendingUpdate().then((info) => {
|
|
8880
|
+
if (info) {
|
|
8881
|
+
screen.notify(`\u2728 ${formatUpdateNoticeLine(info)}`, 3e4);
|
|
8882
|
+
}
|
|
8883
|
+
});
|
|
8371
8884
|
let finishSession = null;
|
|
8372
8885
|
const sessionDone = new Promise((resolve5) => {
|
|
8373
8886
|
finishSession = resolve5;
|
|
@@ -8929,7 +9442,6 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8929
9442
|
);
|
|
8930
9443
|
}
|
|
8931
9444
|
};
|
|
8932
|
-
const usage = {};
|
|
8933
9445
|
const toolStates = /* @__PURE__ */ new Map();
|
|
8934
9446
|
const toolCallOrder = [];
|
|
8935
9447
|
let toolsExpanded = false;
|
|
@@ -9012,7 +9524,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9012
9524
|
for (const id of visibleIds) {
|
|
9013
9525
|
const state = toolStates.get(id);
|
|
9014
9526
|
if (state) {
|
|
9015
|
-
lines.push(
|
|
9527
|
+
lines.push(formatToolLine2(state));
|
|
9016
9528
|
}
|
|
9017
9529
|
}
|
|
9018
9530
|
screen.upsertLines("tools", lines);
|
|
@@ -9058,11 +9570,11 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9058
9570
|
}
|
|
9059
9571
|
if (event.kind === "session-info") {
|
|
9060
9572
|
if (event.title !== void 0) {
|
|
9061
|
-
screen.
|
|
9573
|
+
screen.setSessionbar({ title: event.title });
|
|
9062
9574
|
}
|
|
9063
9575
|
if (event.agentId !== void 0 && event.agentId !== resolvedAgentId) {
|
|
9064
9576
|
resolvedAgentId = event.agentId;
|
|
9065
|
-
screen.
|
|
9577
|
+
screen.setSessionbar({ agent: event.agentId });
|
|
9066
9578
|
}
|
|
9067
9579
|
return;
|
|
9068
9580
|
}
|
|
@@ -9085,7 +9597,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9085
9597
|
changed = true;
|
|
9086
9598
|
}
|
|
9087
9599
|
if (changed) {
|
|
9088
|
-
screen.
|
|
9600
|
+
screen.setSessionbar({ usage: { ...usage } });
|
|
9089
9601
|
}
|
|
9090
9602
|
return;
|
|
9091
9603
|
}
|
|
@@ -9136,7 +9648,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9136
9648
|
return;
|
|
9137
9649
|
}
|
|
9138
9650
|
if (event.kind === "model-changed") {
|
|
9139
|
-
screen.
|
|
9651
|
+
screen.setSessionbar({ model: event.model });
|
|
9140
9652
|
}
|
|
9141
9653
|
const formatted = formatEvent(event);
|
|
9142
9654
|
if (formatted.length > 0) {
|
|
@@ -9387,6 +9899,7 @@ var init_app = __esm({
|
|
|
9387
9899
|
init_session();
|
|
9388
9900
|
init_paths();
|
|
9389
9901
|
init_hydra_version();
|
|
9902
|
+
init_update_check();
|
|
9390
9903
|
init_history();
|
|
9391
9904
|
init_discovery();
|
|
9392
9905
|
init_picker();
|
|
@@ -9425,6 +9938,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
9425
9938
|
"foreground",
|
|
9426
9939
|
"help",
|
|
9427
9940
|
"info",
|
|
9941
|
+
"json",
|
|
9428
9942
|
"new",
|
|
9429
9943
|
"reattach",
|
|
9430
9944
|
"replace",
|
|
@@ -11700,6 +12214,7 @@ function constantTimeEqual(a, b) {
|
|
|
11700
12214
|
// src/daemon/routes/sessions.ts
|
|
11701
12215
|
init_config();
|
|
11702
12216
|
init_bundle();
|
|
12217
|
+
init_transcript();
|
|
11703
12218
|
init_types();
|
|
11704
12219
|
init_hydra_version();
|
|
11705
12220
|
import * as os3 from "os";
|
|
@@ -11782,6 +12297,24 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11782
12297
|
);
|
|
11783
12298
|
reply.code(200).send(bundle);
|
|
11784
12299
|
});
|
|
12300
|
+
app.get("/v1/sessions/:id/transcript", async (request, reply) => {
|
|
12301
|
+
const raw = request.params.id;
|
|
12302
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
12303
|
+
const exported = await manager.exportBundle(id);
|
|
12304
|
+
if (!exported) {
|
|
12305
|
+
reply.code(404).send({ error: "session not found" });
|
|
12306
|
+
return;
|
|
12307
|
+
}
|
|
12308
|
+
const bundle = encodeBundle({
|
|
12309
|
+
record: exported.record,
|
|
12310
|
+
history: exported.history,
|
|
12311
|
+
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
12312
|
+
hydraVersion: HYDRA_VERSION,
|
|
12313
|
+
machine: os3.hostname()
|
|
12314
|
+
});
|
|
12315
|
+
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
12316
|
+
reply.code(200).send(bundleToMarkdown(bundle));
|
|
12317
|
+
});
|
|
11785
12318
|
app.post("/v1/sessions/import", async (request, reply) => {
|
|
11786
12319
|
const body = request.body ?? {};
|
|
11787
12320
|
if (body.bundle === void 0) {
|
|
@@ -12096,11 +12629,18 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12096
12629
|
model: hydraMeta.model
|
|
12097
12630
|
});
|
|
12098
12631
|
const client = bindClientToSession(connection, session, state);
|
|
12099
|
-
await session.attach(client, "full");
|
|
12632
|
+
const { entries: replay } = await session.attach(client, "full");
|
|
12100
12633
|
state.attached.set(session.sessionId, {
|
|
12101
12634
|
sessionId: session.sessionId,
|
|
12102
12635
|
clientId: client.clientId
|
|
12103
12636
|
});
|
|
12637
|
+
setImmediate(() => {
|
|
12638
|
+
void (async () => {
|
|
12639
|
+
for (const note of replay) {
|
|
12640
|
+
await connection.notify(note.method, note.params).catch(() => void 0);
|
|
12641
|
+
}
|
|
12642
|
+
})();
|
|
12643
|
+
});
|
|
12104
12644
|
return {
|
|
12105
12645
|
sessionId: session.sessionId,
|
|
12106
12646
|
_meta: buildResponseMeta(session)
|
|
@@ -12314,6 +12854,9 @@ function buildResponseMeta(session) {
|
|
|
12314
12854
|
if (session.currentMode !== void 0) {
|
|
12315
12855
|
ours.currentMode = session.currentMode;
|
|
12316
12856
|
}
|
|
12857
|
+
if (session.currentUsage !== void 0) {
|
|
12858
|
+
ours.currentUsage = session.currentUsage;
|
|
12859
|
+
}
|
|
12317
12860
|
const commands = session.mergedAvailableCommands();
|
|
12318
12861
|
if (commands.length > 0) {
|
|
12319
12862
|
ours.availableCommands = commands;
|
|
@@ -13714,54 +14257,8 @@ function injectHydraMeta(msg, additions) {
|
|
|
13714
14257
|
};
|
|
13715
14258
|
}
|
|
13716
14259
|
|
|
13717
|
-
// src/core/update-check.ts
|
|
13718
|
-
init_hydra_version();
|
|
13719
|
-
var PKG_NAME = "@hydra-acp/cli";
|
|
13720
|
-
var cached;
|
|
13721
|
-
function disabled() {
|
|
13722
|
-
if (process.env.NO_UPDATE_NOTIFIER === "1") {
|
|
13723
|
-
return true;
|
|
13724
|
-
}
|
|
13725
|
-
if (process.argv.includes("--no-update-notifier")) {
|
|
13726
|
-
return true;
|
|
13727
|
-
}
|
|
13728
|
-
return false;
|
|
13729
|
-
}
|
|
13730
|
-
async function getPendingUpdate() {
|
|
13731
|
-
if (cached !== void 0) {
|
|
13732
|
-
return cached;
|
|
13733
|
-
}
|
|
13734
|
-
if (disabled()) {
|
|
13735
|
-
cached = null;
|
|
13736
|
-
return cached;
|
|
13737
|
-
}
|
|
13738
|
-
try {
|
|
13739
|
-
const mod = await import("update-notifier");
|
|
13740
|
-
const updateNotifier = mod.default ?? mod;
|
|
13741
|
-
const notifier = updateNotifier({
|
|
13742
|
-
pkg: { name: PKG_NAME, version: HYDRA_VERSION },
|
|
13743
|
-
updateCheckInterval: 1e3 * 60 * 60 * 24
|
|
13744
|
-
});
|
|
13745
|
-
const u = notifier.update;
|
|
13746
|
-
if (u && typeof u.latest === "string" && typeof u.current === "string" && u.latest !== u.current) {
|
|
13747
|
-
cached = {
|
|
13748
|
-
current: u.current,
|
|
13749
|
-
latest: u.latest,
|
|
13750
|
-
type: typeof u.type === "string" ? u.type : "unknown"
|
|
13751
|
-
};
|
|
13752
|
-
} else {
|
|
13753
|
-
cached = null;
|
|
13754
|
-
}
|
|
13755
|
-
} catch {
|
|
13756
|
-
cached = null;
|
|
13757
|
-
}
|
|
13758
|
-
return cached;
|
|
13759
|
-
}
|
|
13760
|
-
function formatUpdateNoticeLine(info) {
|
|
13761
|
-
return `hydra-acp ${info.latest} available (current ${info.current}) \xB7 run: npm update -g ${PKG_NAME}`;
|
|
13762
|
-
}
|
|
13763
|
-
|
|
13764
14260
|
// src/cli.ts
|
|
14261
|
+
init_update_check();
|
|
13765
14262
|
var suppressUpdateNotice = false;
|
|
13766
14263
|
async function main() {
|
|
13767
14264
|
const argv = process.argv.slice(2);
|
|
@@ -13872,7 +14369,10 @@ async function main() {
|
|
|
13872
14369
|
case "sessions": {
|
|
13873
14370
|
const sub = positional[1];
|
|
13874
14371
|
if (sub === void 0 || sub === "list") {
|
|
13875
|
-
await runSessionsList({
|
|
14372
|
+
await runSessionsList({
|
|
14373
|
+
all: flags.all === true,
|
|
14374
|
+
json: flags.json === true
|
|
14375
|
+
});
|
|
13876
14376
|
return;
|
|
13877
14377
|
}
|
|
13878
14378
|
if (sub === "kill") {
|
|
@@ -13888,6 +14388,11 @@ async function main() {
|
|
|
13888
14388
|
await runSessionsExport(positional[2], out);
|
|
13889
14389
|
return;
|
|
13890
14390
|
}
|
|
14391
|
+
if (sub === "transcript") {
|
|
14392
|
+
const out = resolveOption(flags, "out");
|
|
14393
|
+
await runSessionsTranscript(positional[2], out);
|
|
14394
|
+
return;
|
|
14395
|
+
}
|
|
13891
14396
|
if (sub === "import") {
|
|
13892
14397
|
const cwd = resolveOption(flags, "cwd");
|
|
13893
14398
|
await runSessionsImport(positional[2], {
|
|
@@ -14031,11 +14536,14 @@ function printHelp() {
|
|
|
14031
14536
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
14032
14537
|
" hydra-acp daemon stop|restart|status",
|
|
14033
14538
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
14034
|
-
" hydra-acp sessions [list] [--all]
|
|
14539
|
+
" hydra-acp sessions [list] [--all] [--json]",
|
|
14540
|
+
" List sessions (live + 20 most-recent cold; --all for everything; --json emits the raw daemon response as JSON for scripts)",
|
|
14035
14541
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
14036
14542
|
" hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
|
|
14037
14543
|
" hydra-acp sessions export <id> [--out <file>|.]",
|
|
14038
14544
|
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
14545
|
+
" hydra-acp sessions transcript <id>|<file> [--out <file>|.]",
|
|
14546
|
+
" Render a session as a markdown transcript. Accepts a session id (renders via the daemon) or a local .hydra bundle file (rendered in-process). Writes to <file>, to a default-named file when --out=., or to stdout",
|
|
14039
14547
|
" hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
|
|
14040
14548
|
" 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",
|
|
14041
14549
|
" hydra-acp extensions list List configured extensions and live state",
|