@hydra-acp/cli 0.1.15 → 0.1.17
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 +1846 -1332
- 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,59 @@ 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
|
+
notifier.check();
|
|
4083
|
+
const u = notifier.update;
|
|
4084
|
+
if (u && typeof u.latest === "string" && typeof u.current === "string" && u.latest !== u.current) {
|
|
4085
|
+
cached = {
|
|
4086
|
+
current: u.current,
|
|
4087
|
+
latest: u.latest,
|
|
4088
|
+
type: typeof u.type === "string" ? u.type : "unknown"
|
|
4089
|
+
};
|
|
4090
|
+
} else {
|
|
4091
|
+
cached = null;
|
|
4092
|
+
}
|
|
4093
|
+
} catch {
|
|
4094
|
+
cached = null;
|
|
4095
|
+
}
|
|
4096
|
+
return cached;
|
|
4097
|
+
}
|
|
4098
|
+
function formatUpdateNoticeLine(info) {
|
|
4099
|
+
return `hydra-acp ${info.latest} available (current ${info.current}) \xB7 run: npm update -g ${PKG_NAME}`;
|
|
4100
|
+
}
|
|
4101
|
+
var PKG_NAME, cached;
|
|
4102
|
+
var init_update_check = __esm({
|
|
4103
|
+
"src/core/update-check.ts"() {
|
|
4104
|
+
"use strict";
|
|
4105
|
+
init_hydra_version();
|
|
4106
|
+
PKG_NAME = "@hydra-acp/cli";
|
|
4107
|
+
}
|
|
4108
|
+
});
|
|
4109
|
+
|
|
3327
4110
|
// src/tui/discovery.ts
|
|
3328
4111
|
async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
3329
4112
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -4558,7 +5341,7 @@ function mapKeyName(name) {
|
|
|
4558
5341
|
return null;
|
|
4559
5342
|
}
|
|
4560
5343
|
}
|
|
4561
|
-
var
|
|
5344
|
+
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
5345
|
var init_screen = __esm({
|
|
4563
5346
|
"src/tui/screen.ts"() {
|
|
4564
5347
|
"use strict";
|
|
@@ -4566,7 +5349,7 @@ var init_screen = __esm({
|
|
|
4566
5349
|
init_paths();
|
|
4567
5350
|
init_session();
|
|
4568
5351
|
init_attachments();
|
|
4569
|
-
|
|
5352
|
+
SESSIONBAR_ROWS = 1;
|
|
4570
5353
|
BANNER_ROWS = 1;
|
|
4571
5354
|
SEPARATOR_ROWS = 1;
|
|
4572
5355
|
MAX_PROMPT_ROWS = 8;
|
|
@@ -4664,7 +5447,7 @@ var init_screen = __esm({
|
|
|
4664
5447
|
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
4665
5448
|
queued: 0
|
|
4666
5449
|
};
|
|
4667
|
-
|
|
5450
|
+
sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
|
|
4668
5451
|
lastWindowTitle = null;
|
|
4669
5452
|
resizeHandler;
|
|
4670
5453
|
keyHandler;
|
|
@@ -5015,8 +5798,8 @@ var init_screen = __esm({
|
|
|
5015
5798
|
this.trimScrollback();
|
|
5016
5799
|
this.scheduleRepaint();
|
|
5017
5800
|
}
|
|
5018
|
-
|
|
5019
|
-
this.
|
|
5801
|
+
setSessionbar(sessionbar) {
|
|
5802
|
+
this.sessionbar = { ...this.sessionbar, ...sessionbar };
|
|
5020
5803
|
this.syncWindowTitle();
|
|
5021
5804
|
this.repaint();
|
|
5022
5805
|
}
|
|
@@ -5024,8 +5807,8 @@ var init_screen = __esm({
|
|
|
5024
5807
|
// the host terminal via OSC 2. Supported by xterm/foot/iTerm2/Alacritty/
|
|
5025
5808
|
// most modern emulators; ignored harmlessly elsewhere.
|
|
5026
5809
|
syncWindowTitle() {
|
|
5027
|
-
const title = this.
|
|
5028
|
-
const fallback = shortId(this.
|
|
5810
|
+
const title = this.sessionbar.title?.trim();
|
|
5811
|
+
const fallback = shortId(this.sessionbar.sessionId) || "hydra";
|
|
5029
5812
|
const raw = title && title.length > 0 ? title : fallback;
|
|
5030
5813
|
const clean = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
5031
5814
|
if (clean === this.lastWindowTitle) {
|
|
@@ -5547,8 +6330,10 @@ var init_screen = __esm({
|
|
|
5547
6330
|
return Math.max(1, this.scrollbackVisibleRows() - 2);
|
|
5548
6331
|
}
|
|
5549
6332
|
scrollbackVisibleRows() {
|
|
5550
|
-
const top =
|
|
5551
|
-
const bottom = this.term.height - this.promptRows() -
|
|
6333
|
+
const top = 1;
|
|
6334
|
+
const bottom = this.term.height - this.promptRows() - SESSIONBAR_ROWS - SEPARATOR_ROWS - // separator between banner and sessionbar
|
|
6335
|
+
BANNER_ROWS - SEPARATOR_ROWS - // separator above prompt
|
|
6336
|
+
this.chipRows() - this.queuedRows() - this.completionRows();
|
|
5552
6337
|
return Math.max(0, bottom - top + 1);
|
|
5553
6338
|
}
|
|
5554
6339
|
maxScrollOffset() {
|
|
@@ -5625,30 +6410,32 @@ var init_screen = __esm({
|
|
|
5625
6410
|
this.lastFrameW = w;
|
|
5626
6411
|
this.lastFrameH = h;
|
|
5627
6412
|
}
|
|
5628
|
-
this.drawHeader();
|
|
5629
|
-
this.drawSeparator(HEADER_ROWS);
|
|
5630
6413
|
this.drawScrollback();
|
|
5631
6414
|
this.drawCompletionZone();
|
|
5632
6415
|
this.drawQueuedZone();
|
|
5633
6416
|
this.drawAttachmentChipZone();
|
|
5634
6417
|
const promptRows = this.promptRows();
|
|
5635
|
-
const
|
|
5636
|
-
this.drawSeparator(
|
|
6418
|
+
const separatorAbovePromptRow = h - promptRows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
|
|
6419
|
+
this.drawSeparator(separatorAbovePromptRow);
|
|
5637
6420
|
this.drawPrompt();
|
|
5638
6421
|
this.drawBanner();
|
|
6422
|
+
this.drawSeparator(h - SESSIONBAR_ROWS);
|
|
6423
|
+
this.drawSessionbar();
|
|
5639
6424
|
this.placeCursor();
|
|
5640
6425
|
this.lastPromptRows = promptRows;
|
|
5641
6426
|
}
|
|
5642
|
-
|
|
6427
|
+
drawSessionbar() {
|
|
5643
6428
|
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
|
-
|
|
6429
|
+
const row = this.term.height;
|
|
6430
|
+
const sid = shortId(this.sessionbar.sessionId);
|
|
6431
|
+
const title = this.sessionbar.title?.trim();
|
|
6432
|
+
const agentCell = formatAgentWithModel(this.sessionbar.agent, this.sessionbar.model);
|
|
6433
|
+
const cwdDisplay = shortenHomePath(this.sessionbar.cwd);
|
|
6434
|
+
const usage = formatUsage(this.sessionbar.usage);
|
|
6435
|
+
const sig = `sbar|${w}|${sid}|${agentCell}|${cwdDisplay}|${title ?? ""}|${usage ?? ""}`;
|
|
6436
|
+
this.paintRow(row, sig, () => {
|
|
6437
|
+
const usageReserve = usage ? usage.length + 3 : 0;
|
|
6438
|
+
const fixed = sid.length + " \xB7 ".length + agentCell.length + " \xB7 ".length + (title ? " \xB7 ".length : 0) + usageReserve;
|
|
5652
6439
|
const variableRoom = Math.max(8, w - fixed);
|
|
5653
6440
|
let cwdRoom;
|
|
5654
6441
|
let titleRoom;
|
|
@@ -5660,13 +6447,13 @@ var init_screen = __esm({
|
|
|
5660
6447
|
titleRoom = 0;
|
|
5661
6448
|
cwdRoom = variableRoom;
|
|
5662
6449
|
}
|
|
5663
|
-
this.term.
|
|
6450
|
+
this.term.yellow(sid)(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom));
|
|
5664
6451
|
if (title) {
|
|
5665
6452
|
this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
|
|
5666
6453
|
}
|
|
5667
6454
|
if (usage) {
|
|
5668
6455
|
const col = Math.max(1, w - usage.length + 1);
|
|
5669
|
-
this.term.moveTo(col,
|
|
6456
|
+
this.term.moveTo(col, row).eraseLineAfter();
|
|
5670
6457
|
this.term.dim.noFormat(usage);
|
|
5671
6458
|
}
|
|
5672
6459
|
});
|
|
@@ -5679,7 +6466,7 @@ var init_screen = __esm({
|
|
|
5679
6466
|
}
|
|
5680
6467
|
drawScrollback() {
|
|
5681
6468
|
const w = this.term.width;
|
|
5682
|
-
const top =
|
|
6469
|
+
const top = 1;
|
|
5683
6470
|
const visibleRows = this.scrollbackVisibleRows();
|
|
5684
6471
|
if (visibleRows <= 0) {
|
|
5685
6472
|
return;
|
|
@@ -5745,7 +6532,7 @@ var init_screen = __esm({
|
|
|
5745
6532
|
}
|
|
5746
6533
|
const w = this.term.width;
|
|
5747
6534
|
const promptRows = this.promptRows();
|
|
5748
|
-
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
6535
|
+
const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
|
|
5749
6536
|
const queuedRows = this.queuedRows();
|
|
5750
6537
|
const chipRows = this.chipRows();
|
|
5751
6538
|
const completionBottom = separatorRow - 1 - queuedRows - chipRows;
|
|
@@ -5793,7 +6580,7 @@ var init_screen = __esm({
|
|
|
5793
6580
|
}
|
|
5794
6581
|
const w = this.term.width;
|
|
5795
6582
|
const promptRows = this.promptRows();
|
|
5796
|
-
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
6583
|
+
const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
|
|
5797
6584
|
const chipBottom = separatorRow - 1;
|
|
5798
6585
|
const chipTop = chipBottom - rows + 1;
|
|
5799
6586
|
const iterm = this.isIterm2();
|
|
@@ -5840,7 +6627,7 @@ var init_screen = __esm({
|
|
|
5840
6627
|
}
|
|
5841
6628
|
const w = this.term.width;
|
|
5842
6629
|
const promptRows = this.promptRows();
|
|
5843
|
-
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
6630
|
+
const separatorRow = this.term.height - promptRows - SESSIONBAR_ROWS - SEPARATOR_ROWS - BANNER_ROWS;
|
|
5844
6631
|
const chipRows = this.chipRows();
|
|
5845
6632
|
const queuedBottom = separatorRow - 1 - chipRows;
|
|
5846
6633
|
const queuedTop = queuedBottom - rows + 1;
|
|
@@ -5882,7 +6669,7 @@ var init_screen = __esm({
|
|
|
5882
6669
|
const state = this.dispatcher.state();
|
|
5883
6670
|
const visualRows = computePromptVisualRows(state.buffer, room);
|
|
5884
6671
|
const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
|
|
5885
|
-
const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
|
|
6672
|
+
const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
5886
6673
|
for (let i = 0; i < layout.rendered; i++) {
|
|
5887
6674
|
const vr = visualRows[layout.windowStart + i];
|
|
5888
6675
|
const row = top + i;
|
|
@@ -5918,7 +6705,7 @@ var init_screen = __esm({
|
|
|
5918
6705
|
return;
|
|
5919
6706
|
}
|
|
5920
6707
|
const w = this.term.width;
|
|
5921
|
-
const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
|
|
6708
|
+
const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
5922
6709
|
this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
|
|
5923
6710
|
this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
|
|
5924
6711
|
});
|
|
@@ -5933,7 +6720,7 @@ var init_screen = __esm({
|
|
|
5933
6720
|
}
|
|
5934
6721
|
const w = this.term.width;
|
|
5935
6722
|
const rows = this.permissionRows();
|
|
5936
|
-
const top = this.term.height - rows - BANNER_ROWS + 1;
|
|
6723
|
+
const top = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
5937
6724
|
let row = top;
|
|
5938
6725
|
const writeRow = (sig, paint) => {
|
|
5939
6726
|
if (row >= top + rows) {
|
|
@@ -5975,7 +6762,7 @@ var init_screen = __esm({
|
|
|
5975
6762
|
});
|
|
5976
6763
|
}
|
|
5977
6764
|
drawBanner() {
|
|
5978
|
-
const row = this.term.height;
|
|
6765
|
+
const row = this.term.height - SESSIONBAR_ROWS - SEPARATOR_ROWS;
|
|
5979
6766
|
const w = this.term.width;
|
|
5980
6767
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
5981
6768
|
const right = this.bannerRightContent();
|
|
@@ -6024,13 +6811,14 @@ var init_screen = __esm({
|
|
|
6024
6811
|
placeCursor() {
|
|
6025
6812
|
if (this.permissionPrompt) {
|
|
6026
6813
|
const rows = this.permissionRows();
|
|
6027
|
-
const top2 = this.term.height - rows - BANNER_ROWS + 1;
|
|
6814
|
+
const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
6028
6815
|
const optionRow = top2 + 3 + this.permissionPrompt.selectedIndex;
|
|
6029
|
-
|
|
6816
|
+
const lastUsableRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
|
|
6817
|
+
this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
|
|
6030
6818
|
return;
|
|
6031
6819
|
}
|
|
6032
6820
|
if (this.confirmPrompt) {
|
|
6033
|
-
const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
|
|
6821
|
+
const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
6034
6822
|
this.term.moveTo(2, top2);
|
|
6035
6823
|
return;
|
|
6036
6824
|
}
|
|
@@ -6044,12 +6832,13 @@ var init_screen = __esm({
|
|
|
6044
6832
|
const state = this.dispatcher.state();
|
|
6045
6833
|
const visualRows = computePromptVisualRows(state.buffer, room);
|
|
6046
6834
|
const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
|
|
6047
|
-
const top = this.term.height - layout.rendered - BANNER_ROWS + 1;
|
|
6835
|
+
const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
|
|
6048
6836
|
const row = top + Math.max(0, layout.cursorVisualRow - layout.windowStart);
|
|
6049
6837
|
const col = layout.cursorVisualCol + 3;
|
|
6838
|
+
const lastPromptRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
|
|
6050
6839
|
this.term.moveTo(
|
|
6051
6840
|
Math.min(col, this.term.width),
|
|
6052
|
-
Math.min(row,
|
|
6841
|
+
Math.min(row, lastPromptRow)
|
|
6053
6842
|
);
|
|
6054
6843
|
}
|
|
6055
6844
|
promptRows() {
|
|
@@ -6741,746 +7530,461 @@ var init_input = __esm({
|
|
|
6741
7530
|
};
|
|
6742
7531
|
return [];
|
|
6743
7532
|
}
|
|
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 [];
|
|
7533
|
+
const matchIndices = this.findHistoryMatches(query);
|
|
7534
|
+
if (matchIndices.length === 0) {
|
|
7535
|
+
return [{ type: "escalate-search", query }];
|
|
6891
7536
|
}
|
|
6892
|
-
|
|
7537
|
+
this.historySearch = {
|
|
7538
|
+
query,
|
|
7539
|
+
matchIndices,
|
|
7540
|
+
cursor: 0,
|
|
7541
|
+
savedDraft: {
|
|
7542
|
+
buffer: [...this.buffer],
|
|
7543
|
+
row: this.row,
|
|
7544
|
+
col: this.col
|
|
7545
|
+
}
|
|
7546
|
+
};
|
|
7547
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
7548
|
+
return [];
|
|
6893
7549
|
}
|
|
6894
|
-
//
|
|
6895
|
-
//
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
this.col = lastCol;
|
|
7550
|
+
// ^R advance. At the oldest match with a non-empty query, falls
|
|
7551
|
+
// through to scrollback search (same escalate path as a never-
|
|
7552
|
+
// matched startHistorySearch). With an empty query at the oldest
|
|
7553
|
+
// match (i.e. the user walked all history with no filter), advance
|
|
7554
|
+
// is a no-op so the buffer stays on the oldest entry.
|
|
7555
|
+
advanceHistorySearch() {
|
|
7556
|
+
if (this.historySearch === null) {
|
|
6902
7557
|
return [];
|
|
6903
7558
|
}
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
7559
|
+
const search = this.historySearch;
|
|
7560
|
+
const atOldest = search.cursor >= search.matchIndices.length - 1;
|
|
7561
|
+
if (atOldest) {
|
|
7562
|
+
if (search.query.length === 0) {
|
|
7563
|
+
return [];
|
|
7564
|
+
}
|
|
7565
|
+
const query = search.query;
|
|
7566
|
+
const draft = search.savedDraft;
|
|
7567
|
+
this.historySearch = null;
|
|
7568
|
+
this.buffer = [...draft.buffer];
|
|
7569
|
+
this.row = draft.row;
|
|
7570
|
+
this.col = draft.col;
|
|
7571
|
+
return [{ type: "escalate-search", query }];
|
|
6912
7572
|
}
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
7573
|
+
search.cursor += 1;
|
|
7574
|
+
const idx = search.matchIndices[search.cursor];
|
|
7575
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
7576
|
+
return [];
|
|
7577
|
+
}
|
|
7578
|
+
// ^S retreat — walk toward newer matches. No-op at the newest match
|
|
7579
|
+
// (no wrap, mirroring ^R no-wrap at the oldest).
|
|
7580
|
+
retreatHistorySearch() {
|
|
7581
|
+
if (this.historySearch === null) {
|
|
7582
|
+
return;
|
|
6922
7583
|
}
|
|
6923
|
-
if (this.
|
|
6924
|
-
return
|
|
7584
|
+
if (this.historySearch.cursor === 0) {
|
|
7585
|
+
return;
|
|
6925
7586
|
}
|
|
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
|
-
};
|
|
7587
|
+
this.historySearch.cursor -= 1;
|
|
7588
|
+
const idx = this.historySearch.matchIndices[this.historySearch.cursor];
|
|
7589
|
+
this.loadEntry(this.history[idx] ?? "");
|
|
7003
7590
|
}
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7591
|
+
// Backspace / typing within search mode mutates the query and
|
|
7592
|
+
// re-searches. When the new query is empty, restore the saved
|
|
7593
|
+
// draft buffer (typically empty) and stay in search mode — the
|
|
7594
|
+
// user can keep typing. When the new query has matches, load the
|
|
7595
|
+
// top one. When the new query has no matches, escalate to scrollback
|
|
7596
|
+
// search so the typed term applies there instead.
|
|
7597
|
+
mutateHistorySearchQuery(newQuery) {
|
|
7598
|
+
if (this.historySearch === null) {
|
|
7599
|
+
return [];
|
|
7011
7600
|
}
|
|
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
|
-
// normalize line endings below, but this avoids a spurious
|
|
7037
|
-
// empty trailing row from a single-line clipboard text.
|
|
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
|
|
7601
|
+
if (newQuery.length === 0) {
|
|
7602
|
+
this.historySearch.query = "";
|
|
7603
|
+
this.historySearch.matchIndices = [];
|
|
7604
|
+
this.historySearch.cursor = 0;
|
|
7605
|
+
const draft = this.historySearch.savedDraft;
|
|
7606
|
+
this.buffer = [...draft.buffer];
|
|
7607
|
+
this.row = draft.row;
|
|
7608
|
+
this.col = draft.col;
|
|
7609
|
+
return [];
|
|
7610
|
+
}
|
|
7611
|
+
const matchIndices = this.findHistoryMatches(newQuery);
|
|
7612
|
+
if (matchIndices.length === 0) {
|
|
7613
|
+
const draft = this.historySearch.savedDraft;
|
|
7614
|
+
this.historySearch = null;
|
|
7615
|
+
this.buffer = [...draft.buffer];
|
|
7616
|
+
this.row = draft.row;
|
|
7617
|
+
this.col = draft.col;
|
|
7618
|
+
return [{ type: "escalate-search", query: newQuery }];
|
|
7619
|
+
}
|
|
7620
|
+
this.historySearch.query = newQuery;
|
|
7621
|
+
this.historySearch.matchIndices = matchIndices;
|
|
7622
|
+
this.historySearch.cursor = 0;
|
|
7623
|
+
this.loadEntry(this.history[matchIndices[0]] ?? "");
|
|
7624
|
+
return [];
|
|
7084
7625
|
}
|
|
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}`));
|
|
7626
|
+
findHistoryMatches(query) {
|
|
7627
|
+
const out = [];
|
|
7628
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
7629
|
+
const entry = this.history[i] ?? "";
|
|
7630
|
+
if (query.length === 0 || entry.toLowerCase().includes(query)) {
|
|
7631
|
+
out.push(i);
|
|
7632
|
+
}
|
|
7633
|
+
}
|
|
7634
|
+
return out;
|
|
7101
7635
|
}
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
let settled = false;
|
|
7112
|
-
const settle = () => {
|
|
7113
|
-
if (settled || !stdoutEnded || closedCode === null) {
|
|
7114
|
-
return;
|
|
7636
|
+
cancelHistorySearch() {
|
|
7637
|
+
if (this.historySearch === null) {
|
|
7638
|
+
return;
|
|
7639
|
+
}
|
|
7640
|
+
const draft = this.historySearch.savedDraft;
|
|
7641
|
+
this.historySearch = null;
|
|
7642
|
+
this.buffer = [...draft.buffer];
|
|
7643
|
+
this.row = draft.row;
|
|
7644
|
+
this.col = draft.col;
|
|
7115
7645
|
}
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7646
|
+
loadEntry(text) {
|
|
7647
|
+
this.buffer = text.split("\n");
|
|
7648
|
+
if (this.buffer.length === 0) {
|
|
7649
|
+
this.buffer = [""];
|
|
7650
|
+
}
|
|
7651
|
+
this.row = this.buffer.length - 1;
|
|
7652
|
+
this.col = (this.buffer[this.row] ?? "").length;
|
|
7121
7653
|
}
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7654
|
+
send() {
|
|
7655
|
+
const text = this.bufferText();
|
|
7656
|
+
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
7657
|
+
const index = this.queueIndex;
|
|
7658
|
+
const attachments2 = [...this.attachments];
|
|
7659
|
+
this.clearBuffer();
|
|
7660
|
+
if (text.trim().length === 0) {
|
|
7661
|
+
return [{ type: "queue-remove", index }];
|
|
7662
|
+
}
|
|
7663
|
+
return [{ type: "queue-edit", index, text, attachments: attachments2 }];
|
|
7664
|
+
}
|
|
7665
|
+
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
7666
|
+
return [];
|
|
7667
|
+
}
|
|
7668
|
+
const planMode = this.planMode;
|
|
7669
|
+
const attachments = [...this.attachments];
|
|
7670
|
+
this.clearBuffer();
|
|
7671
|
+
return [{ type: "send", text, planMode, attachments }];
|
|
7134
7672
|
}
|
|
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;
|
|
7673
|
+
// Home: jump to the very start of the prompt buffer. If we're already
|
|
7674
|
+
// there, fall through to scrolling the scrollback to its top.
|
|
7675
|
+
handleHome() {
|
|
7676
|
+
if (this.row !== 0 || this.col !== 0) {
|
|
7677
|
+
this.row = 0;
|
|
7678
|
+
this.col = 0;
|
|
7679
|
+
return [];
|
|
7680
|
+
}
|
|
7681
|
+
return [{ type: "scroll-to-top" }];
|
|
7682
|
+
}
|
|
7683
|
+
// End: jump to the end of the last line of the prompt buffer. Already
|
|
7684
|
+
// there → scroll the scrollback to the bottom (newest).
|
|
7685
|
+
handleEnd() {
|
|
7686
|
+
const lastRow = this.buffer.length - 1;
|
|
7687
|
+
const lastCol = (this.buffer[lastRow] ?? "").length;
|
|
7688
|
+
if (this.row !== lastRow || this.col !== lastCol) {
|
|
7689
|
+
this.row = lastRow;
|
|
7690
|
+
this.col = lastCol;
|
|
7691
|
+
return [];
|
|
7692
|
+
}
|
|
7693
|
+
return [{ type: "scroll-to-bottom" }];
|
|
7262
7694
|
}
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7695
|
+
handleCtrlC() {
|
|
7696
|
+
if (this.queueIndex >= 0) {
|
|
7697
|
+
const index = this.queueIndex;
|
|
7698
|
+
this.queueIndex = -1;
|
|
7699
|
+
this.restoreDraft();
|
|
7700
|
+
return [{ type: "queue-remove", index }];
|
|
7701
|
+
}
|
|
7702
|
+
if (!this.bufferIsEmpty() || this.attachments.length > 0) {
|
|
7703
|
+
this.buffer = [""];
|
|
7704
|
+
this.row = 0;
|
|
7705
|
+
this.col = 0;
|
|
7706
|
+
this.attachments = [];
|
|
7707
|
+
this.historyIndex = -1;
|
|
7708
|
+
this.savedDraft = null;
|
|
7709
|
+
this.savedAttachments = null;
|
|
7710
|
+
return [];
|
|
7711
|
+
}
|
|
7712
|
+
if (this.turnRunning) {
|
|
7713
|
+
return [{ type: "cancel" }];
|
|
7714
|
+
}
|
|
7715
|
+
return [{ type: "exit" }];
|
|
7716
|
+
}
|
|
7717
|
+
};
|
|
7267
7718
|
}
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7719
|
+
});
|
|
7720
|
+
|
|
7721
|
+
// src/tui/clipboard.ts
|
|
7722
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
7723
|
+
import fs14 from "fs/promises";
|
|
7724
|
+
import os4 from "os";
|
|
7725
|
+
import path10 from "path";
|
|
7726
|
+
async function readClipboard(envIn = {}) {
|
|
7727
|
+
const env = { ...defaultEnv, ...envIn };
|
|
7728
|
+
if (env.platform === "darwin") {
|
|
7729
|
+
return readMacOS(env);
|
|
7271
7730
|
}
|
|
7272
|
-
if (
|
|
7273
|
-
|
|
7731
|
+
if (env.platform === "linux") {
|
|
7732
|
+
return readLinux(env);
|
|
7274
7733
|
}
|
|
7275
|
-
return
|
|
7734
|
+
return {
|
|
7735
|
+
ok: false,
|
|
7736
|
+
reason: `clipboard paste is not supported on ${env.platform}`
|
|
7737
|
+
};
|
|
7276
7738
|
}
|
|
7277
|
-
function
|
|
7278
|
-
|
|
7279
|
-
|
|
7739
|
+
async function readMacOS(env) {
|
|
7740
|
+
const tmpPath = path10.join(
|
|
7741
|
+
env.tmpdir(),
|
|
7742
|
+
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
7743
|
+
);
|
|
7744
|
+
const script = [
|
|
7745
|
+
"set png_data to the clipboard as \xABclass PNGf\xBB",
|
|
7746
|
+
`set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
|
|
7747
|
+
"write png_data to out_file",
|
|
7748
|
+
"close access out_file"
|
|
7749
|
+
];
|
|
7750
|
+
const args = [];
|
|
7751
|
+
for (const line of script) {
|
|
7752
|
+
args.push("-e", line);
|
|
7280
7753
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
const c = raw;
|
|
7287
|
-
if (typeof c.name !== "string" || c.name.length === 0) {
|
|
7288
|
-
continue;
|
|
7754
|
+
try {
|
|
7755
|
+
await run2(env.spawn, "osascript", args);
|
|
7756
|
+
const img = await readFileAsAttachment(tmpPath, true);
|
|
7757
|
+
if (img.ok) {
|
|
7758
|
+
return img;
|
|
7289
7759
|
}
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
if (typeof c.description === "string") {
|
|
7293
|
-
cmd.description = sanitizeSingleLine(c.description);
|
|
7760
|
+
if (img.reason.startsWith("clipboard image is")) {
|
|
7761
|
+
return img;
|
|
7294
7762
|
}
|
|
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;
|
|
7763
|
+
} catch {
|
|
7764
|
+
await fs14.unlink(tmpPath).catch(() => void 0);
|
|
7313
7765
|
}
|
|
7314
|
-
|
|
7315
|
-
const
|
|
7316
|
-
if (
|
|
7317
|
-
|
|
7318
|
-
}
|
|
7319
|
-
if (typeof cost.currency === "string") {
|
|
7320
|
-
event.costCurrency = cost.currency;
|
|
7766
|
+
try {
|
|
7767
|
+
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
7768
|
+
if (buf.length === 0) {
|
|
7769
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
7321
7770
|
}
|
|
7771
|
+
return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
|
|
7772
|
+
} catch {
|
|
7773
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
7322
7774
|
}
|
|
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
7775
|
}
|
|
7332
|
-
function
|
|
7333
|
-
const
|
|
7334
|
-
if (
|
|
7335
|
-
return
|
|
7776
|
+
async function readLinux(env) {
|
|
7777
|
+
const tool = await detectLinuxTool(env);
|
|
7778
|
+
if (!tool) {
|
|
7779
|
+
return {
|
|
7780
|
+
ok: false,
|
|
7781
|
+
reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
|
|
7782
|
+
};
|
|
7336
7783
|
}
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7784
|
+
try {
|
|
7785
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
|
|
7786
|
+
if (buf.length > 0) {
|
|
7787
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
7788
|
+
return {
|
|
7789
|
+
ok: false,
|
|
7790
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7791
|
+
};
|
|
7792
|
+
}
|
|
7793
|
+
return {
|
|
7794
|
+
ok: true,
|
|
7795
|
+
kind: "image",
|
|
7796
|
+
attachment: {
|
|
7797
|
+
mimeType: "image/png",
|
|
7798
|
+
data: buf.toString("base64"),
|
|
7799
|
+
sizeBytes: buf.length
|
|
7800
|
+
}
|
|
7801
|
+
};
|
|
7345
7802
|
}
|
|
7803
|
+
} catch {
|
|
7346
7804
|
}
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7805
|
+
try {
|
|
7806
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
|
|
7807
|
+
if (buf.length === 0) {
|
|
7808
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
7809
|
+
}
|
|
7810
|
+
return {
|
|
7811
|
+
ok: true,
|
|
7812
|
+
kind: "text",
|
|
7813
|
+
text: normalizeText(buf.toString("utf-8"))
|
|
7814
|
+
};
|
|
7815
|
+
} catch {
|
|
7816
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
7357
7817
|
}
|
|
7358
|
-
return { kind: "user-text", text: promptText };
|
|
7359
7818
|
}
|
|
7360
|
-
function
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
if (status !== void 0) {
|
|
7371
|
-
event.status = status;
|
|
7819
|
+
async function detectLinuxTool(env) {
|
|
7820
|
+
if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
|
|
7821
|
+
return {
|
|
7822
|
+
cmd: "wl-paste",
|
|
7823
|
+
imageArgs: ["-t", "image/png"],
|
|
7824
|
+
// -n: drop trailing newline wl-paste adds by default. We further
|
|
7825
|
+
// normalize line endings below, but this avoids a spurious
|
|
7826
|
+
// empty trailing row from a single-line clipboard text.
|
|
7827
|
+
textArgs: ["-n"]
|
|
7828
|
+
};
|
|
7372
7829
|
}
|
|
7373
|
-
if (
|
|
7374
|
-
|
|
7830
|
+
if (env.env.DISPLAY && await which(env, "xclip")) {
|
|
7831
|
+
return {
|
|
7832
|
+
cmd: "xclip",
|
|
7833
|
+
imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
7834
|
+
textArgs: ["-selection", "clipboard", "-o"]
|
|
7835
|
+
};
|
|
7375
7836
|
}
|
|
7376
|
-
return
|
|
7837
|
+
return null;
|
|
7377
7838
|
}
|
|
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;
|
|
7839
|
+
function normalizeText(text) {
|
|
7840
|
+
return text.replace(/\r\n?/g, "\n");
|
|
7398
7841
|
}
|
|
7399
|
-
function
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
return
|
|
7842
|
+
async function which(env, cmd) {
|
|
7843
|
+
try {
|
|
7844
|
+
await run2(env.spawn, "which", [cmd]);
|
|
7845
|
+
return true;
|
|
7846
|
+
} catch {
|
|
7847
|
+
return false;
|
|
7403
7848
|
}
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
const content = typeof e.content === "string" ? sanitizeSingleLine(e.content) : void 0;
|
|
7411
|
-
if (!content) {
|
|
7412
|
-
continue;
|
|
7849
|
+
}
|
|
7850
|
+
async function readFileAsAttachment(p, unlinkAfter) {
|
|
7851
|
+
try {
|
|
7852
|
+
const buf = await fs14.readFile(p);
|
|
7853
|
+
if (unlinkAfter) {
|
|
7854
|
+
await fs14.unlink(p).catch(() => void 0);
|
|
7413
7855
|
}
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
entry.status = e.status;
|
|
7856
|
+
if (buf.length === 0) {
|
|
7857
|
+
return { ok: false, reason: "no image on clipboard" };
|
|
7417
7858
|
}
|
|
7418
|
-
if (
|
|
7419
|
-
|
|
7859
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
7860
|
+
return {
|
|
7861
|
+
ok: false,
|
|
7862
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7863
|
+
};
|
|
7420
7864
|
}
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7865
|
+
const mimeType = mimeFromExtension(p) ?? "image/png";
|
|
7866
|
+
return {
|
|
7867
|
+
ok: true,
|
|
7868
|
+
kind: "image",
|
|
7869
|
+
attachment: {
|
|
7870
|
+
mimeType,
|
|
7871
|
+
data: buf.toString("base64"),
|
|
7872
|
+
sizeBytes: buf.length
|
|
7873
|
+
}
|
|
7874
|
+
};
|
|
7875
|
+
} catch {
|
|
7876
|
+
return { ok: false, reason: "failed to read clipboard image" };
|
|
7429
7877
|
}
|
|
7430
|
-
return { kind: "mode-changed", mode: sanitizeSingleLine(mode) };
|
|
7431
7878
|
}
|
|
7432
|
-
function
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7879
|
+
function run2(spawn6, cmd, args) {
|
|
7880
|
+
return new Promise((resolve5, reject) => {
|
|
7881
|
+
const proc = spawn6(cmd, args);
|
|
7882
|
+
proc.stdout?.on("data", () => void 0);
|
|
7883
|
+
proc.stderr?.on("data", () => void 0);
|
|
7884
|
+
proc.on("error", reject);
|
|
7885
|
+
proc.on("close", (code) => {
|
|
7886
|
+
if (code === 0) {
|
|
7887
|
+
resolve5();
|
|
7888
|
+
} else {
|
|
7889
|
+
reject(new Error(`${cmd} exited ${code}`));
|
|
7890
|
+
}
|
|
7891
|
+
});
|
|
7892
|
+
});
|
|
7438
7893
|
}
|
|
7439
|
-
function
|
|
7440
|
-
|
|
7441
|
-
|
|
7894
|
+
function runCapture(spawn6, cmd, args) {
|
|
7895
|
+
return new Promise((resolve5, reject) => {
|
|
7896
|
+
const proc = spawn6(cmd, args);
|
|
7897
|
+
const chunks = [];
|
|
7898
|
+
let stdoutEnded = proc.stdout === null;
|
|
7899
|
+
let closedCode = null;
|
|
7900
|
+
let settled = false;
|
|
7901
|
+
const settle = () => {
|
|
7902
|
+
if (settled || !stdoutEnded || closedCode === null) {
|
|
7903
|
+
return;
|
|
7904
|
+
}
|
|
7905
|
+
settled = true;
|
|
7906
|
+
if (closedCode === 0) {
|
|
7907
|
+
resolve5(Buffer.concat(chunks));
|
|
7908
|
+
} else {
|
|
7909
|
+
reject(new Error(`${cmd} exited ${closedCode}`));
|
|
7910
|
+
}
|
|
7911
|
+
};
|
|
7912
|
+
proc.stdout?.on("data", (chunk) => {
|
|
7913
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
7914
|
+
});
|
|
7915
|
+
proc.stdout?.on("end", () => {
|
|
7916
|
+
stdoutEnded = true;
|
|
7917
|
+
settle();
|
|
7918
|
+
});
|
|
7919
|
+
proc.stderr?.on("data", () => void 0);
|
|
7920
|
+
proc.on("error", (err) => {
|
|
7921
|
+
if (settled) {
|
|
7922
|
+
return;
|
|
7923
|
+
}
|
|
7924
|
+
settled = true;
|
|
7925
|
+
reject(err);
|
|
7926
|
+
});
|
|
7927
|
+
proc.on("close", (code) => {
|
|
7928
|
+
closedCode = code ?? 0;
|
|
7929
|
+
settle();
|
|
7930
|
+
});
|
|
7931
|
+
});
|
|
7442
7932
|
}
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7933
|
+
var defaultEnv;
|
|
7934
|
+
var init_clipboard = __esm({
|
|
7935
|
+
"src/tui/clipboard.ts"() {
|
|
7936
|
+
"use strict";
|
|
7937
|
+
init_attachments();
|
|
7938
|
+
defaultEnv = {
|
|
7939
|
+
platform: process.platform,
|
|
7940
|
+
env: process.env,
|
|
7941
|
+
spawn: nodeSpawn,
|
|
7942
|
+
tmpdir: os4.tmpdir
|
|
7943
|
+
};
|
|
7449
7944
|
}
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7945
|
+
});
|
|
7946
|
+
|
|
7947
|
+
// src/tui/completion.ts
|
|
7948
|
+
function longestCommonPrefix(names) {
|
|
7949
|
+
if (names.length === 0) {
|
|
7950
|
+
return "";
|
|
7453
7951
|
}
|
|
7454
|
-
|
|
7455
|
-
|
|
7952
|
+
let prefix = names[0] ?? "";
|
|
7953
|
+
for (let i = 1; i < names.length; i++) {
|
|
7954
|
+
const n = names[i] ?? "";
|
|
7955
|
+
let j = 0;
|
|
7956
|
+
while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
|
|
7957
|
+
j += 1;
|
|
7958
|
+
}
|
|
7959
|
+
prefix = prefix.slice(0, j);
|
|
7960
|
+
if (prefix.length === 0) {
|
|
7961
|
+
break;
|
|
7962
|
+
}
|
|
7456
7963
|
}
|
|
7457
|
-
return
|
|
7964
|
+
return prefix;
|
|
7458
7965
|
}
|
|
7459
|
-
function
|
|
7460
|
-
|
|
7966
|
+
function computeTabCompletion(args) {
|
|
7967
|
+
const { matches, firstLine: firstLine3 } = args;
|
|
7968
|
+
if (matches.length === 0) {
|
|
7461
7969
|
return null;
|
|
7462
7970
|
}
|
|
7463
|
-
const
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7971
|
+
const space = firstLine3.indexOf(" ");
|
|
7972
|
+
const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
|
|
7973
|
+
const tail = space === -1 ? "" : firstLine3.slice(space);
|
|
7974
|
+
if (matches.length === 1) {
|
|
7975
|
+
const name = matches[0] ?? "";
|
|
7976
|
+
const suffix = tail.startsWith(" ") ? "" : " ";
|
|
7977
|
+
return name + suffix + tail;
|
|
7469
7978
|
}
|
|
7470
|
-
|
|
7979
|
+
const commonPrefix = longestCommonPrefix(matches);
|
|
7980
|
+
if (commonPrefix.length <= typedPrefix.length) {
|
|
7471
7981
|
return null;
|
|
7472
7982
|
}
|
|
7473
|
-
return
|
|
7474
|
-
}
|
|
7475
|
-
function readString(u, key) {
|
|
7476
|
-
const v = u[key];
|
|
7477
|
-
return typeof v === "string" ? v : void 0;
|
|
7983
|
+
return commonPrefix + tail;
|
|
7478
7984
|
}
|
|
7479
|
-
var
|
|
7480
|
-
|
|
7481
|
-
"src/tui/render-update.ts"() {
|
|
7985
|
+
var init_completion = __esm({
|
|
7986
|
+
"src/tui/completion.ts"() {
|
|
7482
7987
|
"use strict";
|
|
7483
|
-
STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
7484
7988
|
}
|
|
7485
7989
|
});
|
|
7486
7990
|
|
|
@@ -7693,7 +8197,7 @@ function formatBlock(text, prefix, bodyStyle, prefixStyle, sentBy, fillRow) {
|
|
|
7693
8197
|
}
|
|
7694
8198
|
return out;
|
|
7695
8199
|
}
|
|
7696
|
-
function
|
|
8200
|
+
function formatToolLine2(state) {
|
|
7697
8201
|
const initial = state.initialTitle;
|
|
7698
8202
|
const latest = state.latestTitle;
|
|
7699
8203
|
const initialLc = initial.toLowerCase();
|
|
@@ -7853,6 +8357,11 @@ async function runTuiApp(opts) {
|
|
|
7853
8357
|
while (nextOpts !== null) {
|
|
7854
8358
|
nextOpts = await runSession(term, config, nextOpts, exitHint);
|
|
7855
8359
|
}
|
|
8360
|
+
const pendingUpdate = await getPendingUpdate();
|
|
8361
|
+
if (pendingUpdate) {
|
|
8362
|
+
process.stderr.write(`\u2728 ${formatUpdateNoticeLine(pendingUpdate)}
|
|
8363
|
+
`);
|
|
8364
|
+
}
|
|
7856
8365
|
if (exitHint.sessionId) {
|
|
7857
8366
|
const short = stripHydraSessionPrefix(exitHint.sessionId);
|
|
7858
8367
|
process.stdout.write(`To resume: hydra-acp tui --resume ${short}
|
|
@@ -8096,6 +8605,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8096
8605
|
let initialModel;
|
|
8097
8606
|
let initialMode;
|
|
8098
8607
|
let initialCommands;
|
|
8608
|
+
let initialUsage;
|
|
8099
8609
|
let initialTurnStartedAt;
|
|
8100
8610
|
if (ctx.sessionId === "__new__") {
|
|
8101
8611
|
const hydraNewMeta = {};
|
|
@@ -8125,6 +8635,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8125
8635
|
}
|
|
8126
8636
|
initialModel = hydraMeta.currentModel;
|
|
8127
8637
|
initialMode = hydraMeta.currentMode;
|
|
8638
|
+
initialUsage = hydraMeta.currentUsage;
|
|
8128
8639
|
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
8129
8640
|
if (hydraMeta.availableCommands) {
|
|
8130
8641
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
@@ -8150,6 +8661,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8150
8661
|
}
|
|
8151
8662
|
initialModel = hydraMeta.currentModel;
|
|
8152
8663
|
initialMode = hydraMeta.currentMode;
|
|
8664
|
+
initialUsage = hydraMeta.currentUsage;
|
|
8153
8665
|
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
8154
8666
|
if (hydraMeta.availableCommands) {
|
|
8155
8667
|
initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
|
|
@@ -8356,18 +8868,25 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8356
8868
|
}
|
|
8357
8869
|
return true;
|
|
8358
8870
|
};
|
|
8359
|
-
const
|
|
8871
|
+
const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
|
|
8872
|
+
const usage = { ...initialUsage ?? {} };
|
|
8360
8873
|
screen.start();
|
|
8361
|
-
screen.
|
|
8362
|
-
agent:
|
|
8874
|
+
screen.setSessionbar({
|
|
8875
|
+
agent: sessionbarAgent,
|
|
8363
8876
|
cwd: resolvedCwd,
|
|
8364
8877
|
sessionId: resolvedSessionId,
|
|
8365
8878
|
title: resolvedTitle,
|
|
8366
|
-
model: initialModel
|
|
8879
|
+
model: initialModel,
|
|
8880
|
+
usage: { ...usage }
|
|
8367
8881
|
});
|
|
8368
8882
|
if (initialMode) {
|
|
8369
8883
|
screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
|
|
8370
8884
|
}
|
|
8885
|
+
void getPendingUpdate().then((info) => {
|
|
8886
|
+
if (info) {
|
|
8887
|
+
screen.notify(`\u2728 ${formatUpdateNoticeLine(info)}`, 3e4);
|
|
8888
|
+
}
|
|
8889
|
+
});
|
|
8371
8890
|
let finishSession = null;
|
|
8372
8891
|
const sessionDone = new Promise((resolve5) => {
|
|
8373
8892
|
finishSession = resolve5;
|
|
@@ -8929,7 +9448,6 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8929
9448
|
);
|
|
8930
9449
|
}
|
|
8931
9450
|
};
|
|
8932
|
-
const usage = {};
|
|
8933
9451
|
const toolStates = /* @__PURE__ */ new Map();
|
|
8934
9452
|
const toolCallOrder = [];
|
|
8935
9453
|
let toolsExpanded = false;
|
|
@@ -9012,7 +9530,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9012
9530
|
for (const id of visibleIds) {
|
|
9013
9531
|
const state = toolStates.get(id);
|
|
9014
9532
|
if (state) {
|
|
9015
|
-
lines.push(
|
|
9533
|
+
lines.push(formatToolLine2(state));
|
|
9016
9534
|
}
|
|
9017
9535
|
}
|
|
9018
9536
|
screen.upsertLines("tools", lines);
|
|
@@ -9058,11 +9576,11 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9058
9576
|
}
|
|
9059
9577
|
if (event.kind === "session-info") {
|
|
9060
9578
|
if (event.title !== void 0) {
|
|
9061
|
-
screen.
|
|
9579
|
+
screen.setSessionbar({ title: event.title });
|
|
9062
9580
|
}
|
|
9063
9581
|
if (event.agentId !== void 0 && event.agentId !== resolvedAgentId) {
|
|
9064
9582
|
resolvedAgentId = event.agentId;
|
|
9065
|
-
screen.
|
|
9583
|
+
screen.setSessionbar({ agent: event.agentId });
|
|
9066
9584
|
}
|
|
9067
9585
|
return;
|
|
9068
9586
|
}
|
|
@@ -9085,7 +9603,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9085
9603
|
changed = true;
|
|
9086
9604
|
}
|
|
9087
9605
|
if (changed) {
|
|
9088
|
-
screen.
|
|
9606
|
+
screen.setSessionbar({ usage: { ...usage } });
|
|
9089
9607
|
}
|
|
9090
9608
|
return;
|
|
9091
9609
|
}
|
|
@@ -9136,7 +9654,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
9136
9654
|
return;
|
|
9137
9655
|
}
|
|
9138
9656
|
if (event.kind === "model-changed") {
|
|
9139
|
-
screen.
|
|
9657
|
+
screen.setSessionbar({ model: event.model });
|
|
9140
9658
|
}
|
|
9141
9659
|
const formatted = formatEvent(event);
|
|
9142
9660
|
if (formatted.length > 0) {
|
|
@@ -9387,6 +9905,7 @@ var init_app = __esm({
|
|
|
9387
9905
|
init_session();
|
|
9388
9906
|
init_paths();
|
|
9389
9907
|
init_hydra_version();
|
|
9908
|
+
init_update_check();
|
|
9390
9909
|
init_history();
|
|
9391
9910
|
init_discovery();
|
|
9392
9911
|
init_picker();
|
|
@@ -9425,6 +9944,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
9425
9944
|
"foreground",
|
|
9426
9945
|
"help",
|
|
9427
9946
|
"info",
|
|
9947
|
+
"json",
|
|
9428
9948
|
"new",
|
|
9429
9949
|
"reattach",
|
|
9430
9950
|
"replace",
|
|
@@ -11700,6 +12220,7 @@ function constantTimeEqual(a, b) {
|
|
|
11700
12220
|
// src/daemon/routes/sessions.ts
|
|
11701
12221
|
init_config();
|
|
11702
12222
|
init_bundle();
|
|
12223
|
+
init_transcript();
|
|
11703
12224
|
init_types();
|
|
11704
12225
|
init_hydra_version();
|
|
11705
12226
|
import * as os3 from "os";
|
|
@@ -11782,6 +12303,24 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11782
12303
|
);
|
|
11783
12304
|
reply.code(200).send(bundle);
|
|
11784
12305
|
});
|
|
12306
|
+
app.get("/v1/sessions/:id/transcript", async (request, reply) => {
|
|
12307
|
+
const raw = request.params.id;
|
|
12308
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
12309
|
+
const exported = await manager.exportBundle(id);
|
|
12310
|
+
if (!exported) {
|
|
12311
|
+
reply.code(404).send({ error: "session not found" });
|
|
12312
|
+
return;
|
|
12313
|
+
}
|
|
12314
|
+
const bundle = encodeBundle({
|
|
12315
|
+
record: exported.record,
|
|
12316
|
+
history: exported.history,
|
|
12317
|
+
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
12318
|
+
hydraVersion: HYDRA_VERSION,
|
|
12319
|
+
machine: os3.hostname()
|
|
12320
|
+
});
|
|
12321
|
+
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
12322
|
+
reply.code(200).send(bundleToMarkdown(bundle));
|
|
12323
|
+
});
|
|
11785
12324
|
app.post("/v1/sessions/import", async (request, reply) => {
|
|
11786
12325
|
const body = request.body ?? {};
|
|
11787
12326
|
if (body.bundle === void 0) {
|
|
@@ -12096,11 +12635,18 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12096
12635
|
model: hydraMeta.model
|
|
12097
12636
|
});
|
|
12098
12637
|
const client = bindClientToSession(connection, session, state);
|
|
12099
|
-
await session.attach(client, "full");
|
|
12638
|
+
const { entries: replay } = await session.attach(client, "full");
|
|
12100
12639
|
state.attached.set(session.sessionId, {
|
|
12101
12640
|
sessionId: session.sessionId,
|
|
12102
12641
|
clientId: client.clientId
|
|
12103
12642
|
});
|
|
12643
|
+
setImmediate(() => {
|
|
12644
|
+
void (async () => {
|
|
12645
|
+
for (const note of replay) {
|
|
12646
|
+
await connection.notify(note.method, note.params).catch(() => void 0);
|
|
12647
|
+
}
|
|
12648
|
+
})();
|
|
12649
|
+
});
|
|
12104
12650
|
return {
|
|
12105
12651
|
sessionId: session.sessionId,
|
|
12106
12652
|
_meta: buildResponseMeta(session)
|
|
@@ -12314,6 +12860,9 @@ function buildResponseMeta(session) {
|
|
|
12314
12860
|
if (session.currentMode !== void 0) {
|
|
12315
12861
|
ours.currentMode = session.currentMode;
|
|
12316
12862
|
}
|
|
12863
|
+
if (session.currentUsage !== void 0) {
|
|
12864
|
+
ours.currentUsage = session.currentUsage;
|
|
12865
|
+
}
|
|
12317
12866
|
const commands = session.mergedAvailableCommands();
|
|
12318
12867
|
if (commands.length > 0) {
|
|
12319
12868
|
ours.availableCommands = commands;
|
|
@@ -13714,54 +14263,8 @@ function injectHydraMeta(msg, additions) {
|
|
|
13714
14263
|
};
|
|
13715
14264
|
}
|
|
13716
14265
|
|
|
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
14266
|
// src/cli.ts
|
|
14267
|
+
init_update_check();
|
|
13765
14268
|
var suppressUpdateNotice = false;
|
|
13766
14269
|
async function main() {
|
|
13767
14270
|
const argv = process.argv.slice(2);
|
|
@@ -13872,7 +14375,10 @@ async function main() {
|
|
|
13872
14375
|
case "sessions": {
|
|
13873
14376
|
const sub = positional[1];
|
|
13874
14377
|
if (sub === void 0 || sub === "list") {
|
|
13875
|
-
await runSessionsList({
|
|
14378
|
+
await runSessionsList({
|
|
14379
|
+
all: flags.all === true,
|
|
14380
|
+
json: flags.json === true
|
|
14381
|
+
});
|
|
13876
14382
|
return;
|
|
13877
14383
|
}
|
|
13878
14384
|
if (sub === "kill") {
|
|
@@ -13888,6 +14394,11 @@ async function main() {
|
|
|
13888
14394
|
await runSessionsExport(positional[2], out);
|
|
13889
14395
|
return;
|
|
13890
14396
|
}
|
|
14397
|
+
if (sub === "transcript") {
|
|
14398
|
+
const out = resolveOption(flags, "out");
|
|
14399
|
+
await runSessionsTranscript(positional[2], out);
|
|
14400
|
+
return;
|
|
14401
|
+
}
|
|
13891
14402
|
if (sub === "import") {
|
|
13892
14403
|
const cwd = resolveOption(flags, "cwd");
|
|
13893
14404
|
await runSessionsImport(positional[2], {
|
|
@@ -14031,11 +14542,14 @@ function printHelp() {
|
|
|
14031
14542
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
14032
14543
|
" hydra-acp daemon stop|restart|status",
|
|
14033
14544
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
14034
|
-
" hydra-acp sessions [list] [--all]
|
|
14545
|
+
" hydra-acp sessions [list] [--all] [--json]",
|
|
14546
|
+
" List sessions (live + 20 most-recent cold; --all for everything; --json emits the raw daemon response as JSON for scripts)",
|
|
14035
14547
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
14036
14548
|
" hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
|
|
14037
14549
|
" hydra-acp sessions export <id> [--out <file>|.]",
|
|
14038
14550
|
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
14551
|
+
" hydra-acp sessions transcript <id>|<file> [--out <file>|.]",
|
|
14552
|
+
" 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
14553
|
" hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
|
|
14040
14554
|
" 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
14555
|
" hydra-acp extensions list List configured extensions and live state",
|