@hydra-acp/cli 0.1.11 → 0.1.13
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 +816 -45
- package/dist/index.d.ts +46 -0
- package/dist/index.js +113 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -264,7 +264,13 @@ var init_config = __esm({
|
|
|
264
264
|
// Width cap on the cwd column in the `sessions list` output and the
|
|
265
265
|
// TUI picker. Set higher if you keep deeply-nested working directories
|
|
266
266
|
// and want them visible; the elastic title column shrinks to make room.
|
|
267
|
-
cwdColumnMaxWidth: z.number().int().positive().default(24)
|
|
267
|
+
cwdColumnMaxWidth: z.number().int().positive().default(24),
|
|
268
|
+
// When true (default), emit OSC 9;4 progress-bar control codes so the
|
|
269
|
+
// host terminal can show an indeterminate busy indicator (taskbar pulse
|
|
270
|
+
// on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
|
|
271
|
+
// running. Set false if your terminal renders this obnoxiously or you
|
|
272
|
+
// just don't want it.
|
|
273
|
+
progressIndicator: z.boolean().default(true)
|
|
268
274
|
});
|
|
269
275
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
270
276
|
ExtensionBody = z.object({
|
|
@@ -302,7 +308,8 @@ var init_config = __esm({
|
|
|
302
308
|
maxScrollbackLines: 1e4,
|
|
303
309
|
mouse: true,
|
|
304
310
|
logMaxBytes: 5 * 1024 * 1024,
|
|
305
|
-
cwdColumnMaxWidth: 24
|
|
311
|
+
cwdColumnMaxWidth: 24,
|
|
312
|
+
progressIndicator: true
|
|
306
313
|
})
|
|
307
314
|
});
|
|
308
315
|
HydraConfigReadOnly = HydraConfig.extend({
|
|
@@ -483,6 +490,11 @@ var init_types = __esm({
|
|
|
483
490
|
// Last-known usage snapshot so list views can show per-session cost
|
|
484
491
|
// (and tokens, in callers that care) without resurrecting cold sessions.
|
|
485
492
|
currentUsage: SessionListUsage.optional(),
|
|
493
|
+
// Origin host (and origin upstream id) for imported sessions. Picker
|
|
494
|
+
// uses the host to fill in the UPSTREAM cell pre-first-attach;
|
|
495
|
+
// future "connect back to origin" callers would dial both.
|
|
496
|
+
importedFromMachine: z3.string().optional(),
|
|
497
|
+
importedFromUpstreamSessionId: z3.string().optional(),
|
|
486
498
|
updatedAt: z3.string(),
|
|
487
499
|
attachedClients: z3.number().int().nonnegative(),
|
|
488
500
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -737,6 +749,11 @@ var init_hydra_commands = __esm({
|
|
|
737
749
|
name: "hydra agent",
|
|
738
750
|
argsHint: "<agent>",
|
|
739
751
|
description: "Swap the agent backing this session, preserving context"
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
verb: "kill",
|
|
755
|
+
name: "hydra kill",
|
|
756
|
+
description: "Close this session (kills the agent; record is kept so it can be resumed later)"
|
|
740
757
|
}
|
|
741
758
|
];
|
|
742
759
|
VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
@@ -1006,6 +1023,7 @@ var init_session = __esm({
|
|
|
1006
1023
|
// and noisy state churn keep a quiet session alive forever.
|
|
1007
1024
|
lastRecordedAt;
|
|
1008
1025
|
spawnReplacementAgent;
|
|
1026
|
+
logger;
|
|
1009
1027
|
agentChangeHandlers = [];
|
|
1010
1028
|
// Last available_commands_update we observed from the agent. Stored
|
|
1011
1029
|
// so we can re-broadcast a merged (hydra ∪ agent) list whenever
|
|
@@ -1037,6 +1055,7 @@ var init_session = __esm({
|
|
|
1037
1055
|
}
|
|
1038
1056
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
1039
1057
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
1058
|
+
this.logger = init.logger;
|
|
1040
1059
|
if (init.firstPromptSeeded) {
|
|
1041
1060
|
this.firstPromptSeeded = true;
|
|
1042
1061
|
}
|
|
@@ -1366,6 +1385,9 @@ var init_session = __esm({
|
|
|
1366
1385
|
if (this.closed) {
|
|
1367
1386
|
return;
|
|
1368
1387
|
}
|
|
1388
|
+
this.logger?.info(
|
|
1389
|
+
`session ${this.sessionId} closing deleteRecord=${opts.deleteRecord ?? false} regenTitle=${opts.regenTitle ?? false}`
|
|
1390
|
+
);
|
|
1369
1391
|
this.cancelIdleTimer();
|
|
1370
1392
|
if (opts.regenTitle && this.firstPromptSeeded) {
|
|
1371
1393
|
const timeoutMs = opts.regenTitleTimeoutMs ?? 5e3;
|
|
@@ -1618,6 +1640,8 @@ var init_session = __esm({
|
|
|
1618
1640
|
return this.runTitleCommand(arg);
|
|
1619
1641
|
case "agent":
|
|
1620
1642
|
return this.runAgentCommand(arg);
|
|
1643
|
+
case "kill":
|
|
1644
|
+
return this.runKillCommand();
|
|
1621
1645
|
default: {
|
|
1622
1646
|
const err = new Error(
|
|
1623
1647
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -1729,6 +1753,17 @@ var init_session = __esm({
|
|
|
1729
1753
|
return { stopReason: "end_turn" };
|
|
1730
1754
|
});
|
|
1731
1755
|
}
|
|
1756
|
+
// Close this session in-place. Bypasses enqueuePrompt deliberately so a
|
|
1757
|
+
// mid-turn /hydra kill takes effect immediately — agent.kill() will tear
|
|
1758
|
+
// down any in-flight request as a side effect. The record is kept
|
|
1759
|
+
// (deleteRecord:false) so the session goes cold and can be resurrected.
|
|
1760
|
+
// Returns end_turn so the prompt() caller's response resolves normally,
|
|
1761
|
+
// but every attached client has already received hydra-acp/session_closed
|
|
1762
|
+
// by the time this returns.
|
|
1763
|
+
async runKillCommand() {
|
|
1764
|
+
await this.close({ deleteRecord: false });
|
|
1765
|
+
return { stopReason: "end_turn" };
|
|
1766
|
+
}
|
|
1732
1767
|
// Walk the persisted history and produce a labeled transcript suitable
|
|
1733
1768
|
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
1734
1769
|
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
@@ -1920,6 +1955,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1920
1955
|
return;
|
|
1921
1956
|
}
|
|
1922
1957
|
const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
|
|
1958
|
+
const idleSec = Math.round(idle / 1e3);
|
|
1959
|
+
this.logger?.info(
|
|
1960
|
+
`session ${this.sessionId} idle timeout fired after ${idleSec}s (window=${Math.round(this.idleTimeoutMs / 1e3)}s) \u2014 closing`
|
|
1961
|
+
);
|
|
1923
1962
|
void this.close(opts).catch(() => void 0);
|
|
1924
1963
|
}
|
|
1925
1964
|
cancelIdleTimer() {
|
|
@@ -2037,7 +2076,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2037
2076
|
}
|
|
2038
2077
|
async enqueuePrompt(task) {
|
|
2039
2078
|
return new Promise((resolve5, reject) => {
|
|
2040
|
-
const
|
|
2079
|
+
const run3 = async () => {
|
|
2041
2080
|
try {
|
|
2042
2081
|
const result = await task();
|
|
2043
2082
|
resolve5(result);
|
|
@@ -2045,7 +2084,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2045
2084
|
reject(err);
|
|
2046
2085
|
}
|
|
2047
2086
|
};
|
|
2048
|
-
this.promptQueue.push(
|
|
2087
|
+
this.promptQueue.push(run3);
|
|
2049
2088
|
void this.drainQueue();
|
|
2050
2089
|
});
|
|
2051
2090
|
}
|
|
@@ -2096,6 +2135,8 @@ function recordFromMemorySession(args) {
|
|
|
2096
2135
|
lineageId: args.lineageId,
|
|
2097
2136
|
upstreamSessionId: args.upstreamSessionId,
|
|
2098
2137
|
importedFromSessionId: args.importedFromSessionId,
|
|
2138
|
+
importedFromUpstreamSessionId: args.importedFromUpstreamSessionId,
|
|
2139
|
+
importedFromMachine: args.importedFromMachine,
|
|
2099
2140
|
agentId: args.agentId,
|
|
2100
2141
|
cwd: args.cwd,
|
|
2101
2142
|
title: args.title,
|
|
@@ -2143,6 +2184,16 @@ var init_session_store = __esm({
|
|
|
2143
2184
|
// origin's local id at export time, kept for debuggability and as a
|
|
2144
2185
|
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
2145
2186
|
importedFromSessionId: z4.string().optional(),
|
|
2187
|
+
// Origin's agent-side session id at export time. Carried as a
|
|
2188
|
+
// breadcrumb and as the handle a future "connect back to origin"
|
|
2189
|
+
// feature would dial. Absent when the origin record had no upstream
|
|
2190
|
+
// bound (re-export of an imported, not-yet-attached session).
|
|
2191
|
+
importedFromUpstreamSessionId: z4.string().optional(),
|
|
2192
|
+
// Hostname of the machine that exported the bundle we imported
|
|
2193
|
+
// (i.e. the most recent hop, not necessarily the true multi-hop
|
|
2194
|
+
// origin). Surfaced in the picker so imported rows don't look like
|
|
2195
|
+
// they materialized from nowhere.
|
|
2196
|
+
importedFromMachine: z4.string().optional(),
|
|
2146
2197
|
agentId: z4.string(),
|
|
2147
2198
|
cwd: z4.string(),
|
|
2148
2199
|
title: z4.string().optional(),
|
|
@@ -2358,6 +2409,7 @@ function encodeBundle(params) {
|
|
|
2358
2409
|
session: {
|
|
2359
2410
|
sessionId: params.record.sessionId,
|
|
2360
2411
|
lineageId: params.record.lineageId,
|
|
2412
|
+
...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
|
|
2361
2413
|
agentId: params.record.agentId,
|
|
2362
2414
|
cwd: params.record.cwd,
|
|
2363
2415
|
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
@@ -2395,6 +2447,12 @@ var init_bundle = __esm({
|
|
|
2395
2447
|
// Required on bundles — the export path backfills if the source
|
|
2396
2448
|
// record was written before lineageId existed.
|
|
2397
2449
|
lineageId: z5.string(),
|
|
2450
|
+
// The exporter's agent-side session id at export time. Carried so
|
|
2451
|
+
// importers can persist it as a breadcrumb (and, eventually, as the
|
|
2452
|
+
// handle a "connect back to origin" feature would need). Omitted on
|
|
2453
|
+
// bundles whose source record never bound to an agent (e.g. a
|
|
2454
|
+
// re-export of an imported, not-yet-attached session).
|
|
2455
|
+
upstreamSessionId: z5.string().optional(),
|
|
2398
2456
|
agentId: z5.string(),
|
|
2399
2457
|
cwd: z5.string(),
|
|
2400
2458
|
title: z5.string().optional(),
|
|
@@ -2608,7 +2666,7 @@ var init_agent_display = __esm({
|
|
|
2608
2666
|
function toRow(s, now = Date.now()) {
|
|
2609
2667
|
return {
|
|
2610
2668
|
session: stripHydraSessionPrefix(s.sessionId),
|
|
2611
|
-
upstream: s.upstreamSessionId
|
|
2669
|
+
upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
|
|
2612
2670
|
state: formatState(s.status, s.attachedClients),
|
|
2613
2671
|
agent: formatAgentCell(s.agentId, s.currentUsage),
|
|
2614
2672
|
age: formatRelativeAge(s.updatedAt, now),
|
|
@@ -2616,6 +2674,15 @@ function toRow(s, now = Date.now()) {
|
|
|
2616
2674
|
cwd: shortenHomePath(s.cwd)
|
|
2617
2675
|
};
|
|
2618
2676
|
}
|
|
2677
|
+
function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
|
|
2678
|
+
if (upstreamSessionId && upstreamSessionId.length > 0) {
|
|
2679
|
+
return upstreamSessionId;
|
|
2680
|
+
}
|
|
2681
|
+
if (importedFromMachine && importedFromMachine.length > 0) {
|
|
2682
|
+
return `\u2190 ${importedFromMachine}`;
|
|
2683
|
+
}
|
|
2684
|
+
return "-";
|
|
2685
|
+
}
|
|
2619
2686
|
function formatState(status, clients) {
|
|
2620
2687
|
if (status === "cold") {
|
|
2621
2688
|
return "COLD";
|
|
@@ -2955,11 +3022,11 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
2955
3022
|
function bundleToSummary(parsed) {
|
|
2956
3023
|
return {
|
|
2957
3024
|
sessionId: parsed.session.sessionId,
|
|
2958
|
-
upstreamSessionId: "-",
|
|
2959
3025
|
cwd: parsed.session.cwd,
|
|
2960
3026
|
agentId: parsed.session.agentId,
|
|
2961
3027
|
currentUsage: parsed.session.currentUsage,
|
|
2962
3028
|
title: parsed.session.title,
|
|
3029
|
+
importedFromMachine: parsed.exportedFrom.machine,
|
|
2963
3030
|
attachedClients: 0,
|
|
2964
3031
|
updatedAt: parsed.session.updatedAt,
|
|
2965
3032
|
status: "cold"
|
|
@@ -2980,10 +3047,13 @@ function printBundleInfo(raw, cwdColumnMaxWidth) {
|
|
|
2980
3047
|
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
2981
3048
|
process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
2982
3049
|
process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
|
|
3050
|
+
const originUpstream = parsed.session.upstreamSessionId ?? "-";
|
|
2983
3051
|
process.stdout.write(
|
|
2984
3052
|
`
|
|
2985
3053
|
lineage: ${parsed.session.lineageId}
|
|
2986
3054
|
exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
|
|
3055
|
+
origin session: ${parsed.session.sessionId}
|
|
3056
|
+
origin upstream: ${originUpstream}
|
|
2987
3057
|
history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
|
|
2988
3058
|
` : "\n")
|
|
2989
3059
|
);
|
|
@@ -3284,7 +3354,9 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
3284
3354
|
agentId: s.agentId,
|
|
3285
3355
|
currentModel: s.currentModel,
|
|
3286
3356
|
currentUsage: s.currentUsage,
|
|
3287
|
-
title: s.title
|
|
3357
|
+
title: s.title,
|
|
3358
|
+
importedFromMachine: s.importedFromMachine,
|
|
3359
|
+
importedFromUpstreamSessionId: s.importedFromUpstreamSessionId
|
|
3288
3360
|
}));
|
|
3289
3361
|
}
|
|
3290
3362
|
async function killSession(config, id, fetchImpl = fetch) {
|
|
@@ -3356,8 +3428,15 @@ async function pickSession(term, opts) {
|
|
|
3356
3428
|
let total = 1 + visible.length;
|
|
3357
3429
|
let selectedIdx = 0;
|
|
3358
3430
|
let scrollOffset = 0;
|
|
3431
|
+
if (opts.currentSessionId !== void 0) {
|
|
3432
|
+
const idx = visible.findIndex((s) => s.sessionId === opts.currentSessionId);
|
|
3433
|
+
if (idx >= 0) {
|
|
3434
|
+
selectedIdx = idx + 1;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3359
3437
|
let searchActive = false;
|
|
3360
3438
|
let searchTerm = "";
|
|
3439
|
+
let cwdOnly = false;
|
|
3361
3440
|
let mode = "normal";
|
|
3362
3441
|
let pendingAction = null;
|
|
3363
3442
|
let transientStatus = null;
|
|
@@ -3386,10 +3465,14 @@ async function pickSession(term, opts) {
|
|
|
3386
3465
|
computeLayout();
|
|
3387
3466
|
};
|
|
3388
3467
|
const applyFilter = () => {
|
|
3468
|
+
let base = allSessions;
|
|
3469
|
+
if (cwdOnly) {
|
|
3470
|
+
base = base.filter((s) => s.cwd === opts.cwd);
|
|
3471
|
+
}
|
|
3389
3472
|
if (searchActive && searchTerm.length > 0) {
|
|
3390
|
-
visible =
|
|
3473
|
+
visible = base.filter((s) => matchesSearch(s, searchTerm));
|
|
3391
3474
|
} else {
|
|
3392
|
-
visible =
|
|
3475
|
+
visible = base;
|
|
3393
3476
|
}
|
|
3394
3477
|
rebuildRows();
|
|
3395
3478
|
if (searchActive) {
|
|
@@ -3434,16 +3517,19 @@ async function pickSession(term, opts) {
|
|
|
3434
3517
|
const formatIndicator = () => {
|
|
3435
3518
|
const above = scrollOffset;
|
|
3436
3519
|
const below = Math.max(0, visible.length - scrollOffset - viewportSize);
|
|
3437
|
-
if (above === 0 && below === 0) {
|
|
3438
|
-
return "";
|
|
3439
|
-
}
|
|
3440
3520
|
const parts = [];
|
|
3521
|
+
if (cwdOnly) {
|
|
3522
|
+
parts.push("cwd-only");
|
|
3523
|
+
}
|
|
3441
3524
|
if (above > 0) {
|
|
3442
3525
|
parts.push(`\u2191 ${above} above`);
|
|
3443
3526
|
}
|
|
3444
3527
|
if (below > 0) {
|
|
3445
3528
|
parts.push(`\u2193 ${below} below`);
|
|
3446
3529
|
}
|
|
3530
|
+
if (parts.length === 0) {
|
|
3531
|
+
return "";
|
|
3532
|
+
}
|
|
3447
3533
|
return ` ${parts.join(" \xB7 ")}`;
|
|
3448
3534
|
};
|
|
3449
3535
|
const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
|
|
@@ -3667,6 +3753,38 @@ async function pickSession(term, opts) {
|
|
|
3667
3753
|
renderFromScratch();
|
|
3668
3754
|
return;
|
|
3669
3755
|
}
|
|
3756
|
+
if (name === "n" || name === "N") {
|
|
3757
|
+
move(1);
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
if (name === "p" || name === "P") {
|
|
3761
|
+
move(-1);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
if (name === "c" || name === "C") {
|
|
3765
|
+
cleanup();
|
|
3766
|
+
resolve5({ kind: "new" });
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
if (name === "q" || name === "Q") {
|
|
3770
|
+
cleanup();
|
|
3771
|
+
resolve5({ kind: "abort" });
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
if (name === "o" || name === "O") {
|
|
3775
|
+
const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
3776
|
+
cwdOnly = !cwdOnly;
|
|
3777
|
+
applyFilter();
|
|
3778
|
+
if (keepId !== void 0) {
|
|
3779
|
+
const idx = visible.findIndex((s) => s.sessionId === keepId);
|
|
3780
|
+
if (idx >= 0) {
|
|
3781
|
+
selectedIdx = idx + 1;
|
|
3782
|
+
adjustScroll();
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
renderFromScratch();
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3670
3788
|
if (name === "r" || name === "R") {
|
|
3671
3789
|
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
3672
3790
|
void refresh(currentId);
|
|
@@ -3805,6 +3923,90 @@ var init_picker = __esm({
|
|
|
3805
3923
|
}
|
|
3806
3924
|
});
|
|
3807
3925
|
|
|
3926
|
+
// src/tui/attachments.ts
|
|
3927
|
+
import path9 from "path";
|
|
3928
|
+
function mimeFromExtension(p) {
|
|
3929
|
+
return EXTENSION_TO_MIME[path9.extname(p).toLowerCase()] ?? null;
|
|
3930
|
+
}
|
|
3931
|
+
function isSupportedImagePath(p) {
|
|
3932
|
+
return mimeFromExtension(p) !== null;
|
|
3933
|
+
}
|
|
3934
|
+
function formatSize(bytes) {
|
|
3935
|
+
if (bytes >= 1024 * 1024) {
|
|
3936
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3937
|
+
}
|
|
3938
|
+
if (bytes >= 1024) {
|
|
3939
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
3940
|
+
}
|
|
3941
|
+
return `${bytes}B`;
|
|
3942
|
+
}
|
|
3943
|
+
function parseImageDropPaste(raw) {
|
|
3944
|
+
const text = raw.trim();
|
|
3945
|
+
if (text.length === 0) {
|
|
3946
|
+
return null;
|
|
3947
|
+
}
|
|
3948
|
+
const tokens = [];
|
|
3949
|
+
let i = 0;
|
|
3950
|
+
while (i < text.length) {
|
|
3951
|
+
while (i < text.length && /\s/.test(text[i] ?? "")) {
|
|
3952
|
+
i++;
|
|
3953
|
+
}
|
|
3954
|
+
if (i >= text.length) {
|
|
3955
|
+
break;
|
|
3956
|
+
}
|
|
3957
|
+
const ch = text[i];
|
|
3958
|
+
let token = "";
|
|
3959
|
+
if (ch === "'" || ch === '"') {
|
|
3960
|
+
const quote = ch;
|
|
3961
|
+
i++;
|
|
3962
|
+
while (i < text.length && text[i] !== quote) {
|
|
3963
|
+
token += text[i];
|
|
3964
|
+
i++;
|
|
3965
|
+
}
|
|
3966
|
+
if (i >= text.length) {
|
|
3967
|
+
return null;
|
|
3968
|
+
}
|
|
3969
|
+
i++;
|
|
3970
|
+
} else {
|
|
3971
|
+
while (i < text.length && !/\s/.test(text[i] ?? "")) {
|
|
3972
|
+
if (text[i] === "\\" && i + 1 < text.length) {
|
|
3973
|
+
token += text[i + 1];
|
|
3974
|
+
i += 2;
|
|
3975
|
+
} else {
|
|
3976
|
+
token += text[i];
|
|
3977
|
+
i++;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
let normalized = token;
|
|
3982
|
+
if (normalized.startsWith("file://")) {
|
|
3983
|
+
normalized = decodeURI(normalized.slice("file://".length));
|
|
3984
|
+
}
|
|
3985
|
+
if (!normalized.startsWith("/")) {
|
|
3986
|
+
return null;
|
|
3987
|
+
}
|
|
3988
|
+
if (!isSupportedImagePath(normalized)) {
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
tokens.push(normalized);
|
|
3992
|
+
}
|
|
3993
|
+
return tokens.length > 0 ? tokens : null;
|
|
3994
|
+
}
|
|
3995
|
+
var MAX_ATTACHMENT_BYTES, EXTENSION_TO_MIME;
|
|
3996
|
+
var init_attachments = __esm({
|
|
3997
|
+
"src/tui/attachments.ts"() {
|
|
3998
|
+
"use strict";
|
|
3999
|
+
MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
4000
|
+
EXTENSION_TO_MIME = {
|
|
4001
|
+
".png": "image/png",
|
|
4002
|
+
".jpg": "image/jpeg",
|
|
4003
|
+
".jpeg": "image/jpeg",
|
|
4004
|
+
".gif": "image/gif",
|
|
4005
|
+
".webp": "image/webp"
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
});
|
|
4009
|
+
|
|
3808
4010
|
// src/tui/screen.ts
|
|
3809
4011
|
import stringWidth from "string-width";
|
|
3810
4012
|
import wrapAnsi from "wrap-ansi";
|
|
@@ -3813,7 +4015,8 @@ function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null
|
|
|
3813
4015
|
if (!line) {
|
|
3814
4016
|
return `${zone}|${width}|empty|${highlight2 ?? ""}|${active}`;
|
|
3815
4017
|
}
|
|
3816
|
-
|
|
4018
|
+
const img = line.iterm2Image ? `i${line.iterm2Image.heightCells}:${line.iterm2Image.data.length}` : "";
|
|
4019
|
+
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}|${highlight2 ?? ""}|${active}|${img}`;
|
|
3817
4020
|
}
|
|
3818
4021
|
function computePromptVisualRows(buffer, room) {
|
|
3819
4022
|
const rows = [];
|
|
@@ -4061,6 +4264,14 @@ function* segmentForWidth(text) {
|
|
|
4061
4264
|
i = runEnd;
|
|
4062
4265
|
}
|
|
4063
4266
|
}
|
|
4267
|
+
function buildIterm2ImageEscape(base64, heightCells, insideTmux) {
|
|
4268
|
+
const inner = `\x1B]1337;File=inline=1;height=${heightCells};preserveAspectRatio=1:${base64}\x07`;
|
|
4269
|
+
if (!insideTmux) {
|
|
4270
|
+
return inner;
|
|
4271
|
+
}
|
|
4272
|
+
const doubled = inner.replace(/\x1b/g, "\x1B\x1B");
|
|
4273
|
+
return `\x1BPtmux;${doubled}\x1B\\`;
|
|
4274
|
+
}
|
|
4064
4275
|
function wrap(text, width, opts = {}) {
|
|
4065
4276
|
if (width <= 0) {
|
|
4066
4277
|
return [text];
|
|
@@ -4310,6 +4521,8 @@ function mapKeyName(name) {
|
|
|
4310
4521
|
return "ctrl-s";
|
|
4311
4522
|
case "CTRL_U":
|
|
4312
4523
|
return "ctrl-u";
|
|
4524
|
+
case "CTRL_V":
|
|
4525
|
+
return "ctrl-v";
|
|
4313
4526
|
case "CTRL_W":
|
|
4314
4527
|
return "ctrl-w";
|
|
4315
4528
|
case "CTRL_Y":
|
|
@@ -4320,13 +4533,14 @@ function mapKeyName(name) {
|
|
|
4320
4533
|
return null;
|
|
4321
4534
|
}
|
|
4322
4535
|
}
|
|
4323
|
-
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
4536
|
+
var HEADER_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;
|
|
4324
4537
|
var init_screen = __esm({
|
|
4325
4538
|
"src/tui/screen.ts"() {
|
|
4326
4539
|
"use strict";
|
|
4327
4540
|
init_agent_display();
|
|
4328
4541
|
init_paths();
|
|
4329
4542
|
init_session();
|
|
4543
|
+
init_attachments();
|
|
4330
4544
|
HEADER_ROWS = 2;
|
|
4331
4545
|
BANNER_ROWS = 1;
|
|
4332
4546
|
SEPARATOR_ROWS = 1;
|
|
@@ -4334,6 +4548,7 @@ var init_screen = __esm({
|
|
|
4334
4548
|
MAX_QUEUED_ROWS = 5;
|
|
4335
4549
|
MAX_PERMISSION_ROWS = 12;
|
|
4336
4550
|
MAX_COMPLETION_ROWS = 6;
|
|
4551
|
+
MAX_CHIP_ROWS = 4;
|
|
4337
4552
|
CONFIRM_PROMPT_ROWS = 2;
|
|
4338
4553
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
4339
4554
|
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
@@ -4353,6 +4568,12 @@ var init_screen = __esm({
|
|
|
4353
4568
|
lastPromptRows = 0;
|
|
4354
4569
|
queuedTexts = [];
|
|
4355
4570
|
lastQueueEditingIndex = -1;
|
|
4571
|
+
// Attachments on the current draft, pushed by the app whenever the
|
|
4572
|
+
// dispatcher mutates. The chip zone (drawAttachmentChipZone) renders
|
|
4573
|
+
// one row per attachment plus, in iTerm2-capable terminals, an inline
|
|
4574
|
+
// thumbnail. Capped at MAX_CHIP_ROWS in the visible zone — additional
|
|
4575
|
+
// chips collapse into an overflow row.
|
|
4576
|
+
attachments = [];
|
|
4356
4577
|
repaintPaused = 0;
|
|
4357
4578
|
repaintPending = false;
|
|
4358
4579
|
lastRepaintAt = 0;
|
|
@@ -4415,7 +4636,7 @@ var init_screen = __esm({
|
|
|
4415
4636
|
banner = {
|
|
4416
4637
|
status: "ready",
|
|
4417
4638
|
planMode: false,
|
|
4418
|
-
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
4639
|
+
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
4419
4640
|
queued: 0
|
|
4420
4641
|
};
|
|
4421
4642
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -4436,6 +4657,12 @@ var init_screen = __esm({
|
|
|
4436
4657
|
pasteBuffer = "";
|
|
4437
4658
|
rawStdinHandler;
|
|
4438
4659
|
mouseEnabled;
|
|
4660
|
+
progressIndicatorEnabled;
|
|
4661
|
+
// Last OSC 9;4 state we wrote (3 = indeterminate, 0 = remove). Used to
|
|
4662
|
+
// suppress redundant writes when setBanner runs but `status` didn't
|
|
4663
|
+
// actually change, and to re-emit on start() if a picker round-trip
|
|
4664
|
+
// cleared the host terminal's indicator.
|
|
4665
|
+
lastProgressState = 0;
|
|
4439
4666
|
constructor(opts) {
|
|
4440
4667
|
this.term = opts.term;
|
|
4441
4668
|
this.dispatcher = opts.dispatcher;
|
|
@@ -4443,6 +4670,7 @@ var init_screen = __esm({
|
|
|
4443
4670
|
this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
|
|
4444
4671
|
this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
|
|
4445
4672
|
this.mouseEnabled = opts.mouse ?? true;
|
|
4673
|
+
this.progressIndicatorEnabled = opts.progressIndicator ?? true;
|
|
4446
4674
|
this.resizeHandler = () => this.repaint();
|
|
4447
4675
|
this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
|
|
4448
4676
|
this.mouseHandler = (name) => this.handleMouse(name);
|
|
@@ -4471,6 +4699,8 @@ var init_screen = __esm({
|
|
|
4471
4699
|
}
|
|
4472
4700
|
this.term.on("resize", this.resizeHandler);
|
|
4473
4701
|
this.installBracketedPaste();
|
|
4702
|
+
this.lastProgressState = 0;
|
|
4703
|
+
this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
|
|
4474
4704
|
this.repaint();
|
|
4475
4705
|
}
|
|
4476
4706
|
stop() {
|
|
@@ -4491,6 +4721,7 @@ var init_screen = __esm({
|
|
|
4491
4721
|
this.term.grabInput(false);
|
|
4492
4722
|
this.term.hideCursor(false);
|
|
4493
4723
|
process.stdout.write("\x1B[?7h");
|
|
4724
|
+
this.writeProgressIndicator(0);
|
|
4494
4725
|
this.term.fullscreen(false);
|
|
4495
4726
|
this.term("\n");
|
|
4496
4727
|
}
|
|
@@ -4536,7 +4767,12 @@ var init_screen = __esm({
|
|
|
4536
4767
|
this.pasteActive = false;
|
|
4537
4768
|
const pasted = Buffer.from(this.pasteBuffer, "binary").toString("utf-8").replace(/\r\n?/g, "\n");
|
|
4538
4769
|
this.pasteBuffer = "";
|
|
4539
|
-
|
|
4770
|
+
const paths2 = parseImageDropPaste(pasted);
|
|
4771
|
+
if (paths2 !== null) {
|
|
4772
|
+
this.onKey([{ type: "attachment-paths", paths: paths2 }]);
|
|
4773
|
+
} else {
|
|
4774
|
+
this.onKey([{ type: "paste", text: pasted }]);
|
|
4775
|
+
}
|
|
4540
4776
|
continue;
|
|
4541
4777
|
}
|
|
4542
4778
|
const startIdx = text.indexOf(startMarker);
|
|
@@ -4779,9 +5015,26 @@ var init_screen = __esm({
|
|
|
4779
5015
|
}
|
|
4780
5016
|
setBanner(banner) {
|
|
4781
5017
|
this.banner = { ...this.banner, ...banner };
|
|
5018
|
+
this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
|
|
4782
5019
|
this.drawBanner();
|
|
4783
5020
|
this.placeCursor();
|
|
4784
5021
|
}
|
|
5022
|
+
// OSC 9;4 progress-bar control. State 3 = indeterminate (pulsing
|
|
5023
|
+
// taskbar / dock badge while a turn is running); state 0 = remove.
|
|
5024
|
+
// ConEmu-flavor sequence — supported by Windows Terminal, WezTerm,
|
|
5025
|
+
// Ghostty, Konsole, Black Box, Rio, and others; ignored harmlessly
|
|
5026
|
+
// by terminals that don't implement it. Disabled entirely when
|
|
5027
|
+
// tui.progressIndicator is false.
|
|
5028
|
+
writeProgressIndicator(state) {
|
|
5029
|
+
if (!this.progressIndicatorEnabled) {
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
5032
|
+
if (state === this.lastProgressState) {
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
this.lastProgressState = state;
|
|
5036
|
+
process.stdout.write(`\x1B]9;4;${state}\x1B\\`);
|
|
5037
|
+
}
|
|
4785
5038
|
// Transient right-side banner message. Cleared automatically after
|
|
4786
5039
|
// durationMs (default 4s). Each call resets the timer, so rapid
|
|
4787
5040
|
// successive notifications coalesce on the latest text. Active
|
|
@@ -5270,7 +5523,7 @@ var init_screen = __esm({
|
|
|
5270
5523
|
}
|
|
5271
5524
|
scrollbackVisibleRows() {
|
|
5272
5525
|
const top = HEADER_ROWS + SEPARATOR_ROWS;
|
|
5273
|
-
const bottom = this.term.height - this.promptRows() - BANNER_ROWS - SEPARATOR_ROWS - this.queuedRows() - this.completionRows();
|
|
5526
|
+
const bottom = this.term.height - this.promptRows() - BANNER_ROWS - SEPARATOR_ROWS - this.chipRows() - this.queuedRows() - this.completionRows();
|
|
5274
5527
|
return Math.max(0, bottom - top + 1);
|
|
5275
5528
|
}
|
|
5276
5529
|
maxScrollOffset() {
|
|
@@ -5352,6 +5605,7 @@ var init_screen = __esm({
|
|
|
5352
5605
|
this.drawScrollback();
|
|
5353
5606
|
this.drawCompletionZone();
|
|
5354
5607
|
this.drawQueuedZone();
|
|
5608
|
+
this.drawAttachmentChipZone();
|
|
5355
5609
|
const promptRows = this.promptRows();
|
|
5356
5610
|
const separatorRow = h - promptRows - BANNER_ROWS;
|
|
5357
5611
|
this.drawSeparator(separatorRow);
|
|
@@ -5443,6 +5697,16 @@ var init_screen = __esm({
|
|
|
5443
5697
|
queuedRows() {
|
|
5444
5698
|
return Math.min(MAX_QUEUED_ROWS, this.queuedTexts.length);
|
|
5445
5699
|
}
|
|
5700
|
+
chipRows() {
|
|
5701
|
+
return Math.min(MAX_CHIP_ROWS, this.attachments.length);
|
|
5702
|
+
}
|
|
5703
|
+
setAttachments(attachments) {
|
|
5704
|
+
if (this.attachments.length === attachments.length && this.attachments.every((a, i) => a === attachments[i])) {
|
|
5705
|
+
return;
|
|
5706
|
+
}
|
|
5707
|
+
this.attachments = [...attachments];
|
|
5708
|
+
this.repaint();
|
|
5709
|
+
}
|
|
5446
5710
|
completionRows() {
|
|
5447
5711
|
if (this.permissionPrompt) {
|
|
5448
5712
|
return 0;
|
|
@@ -5458,7 +5722,8 @@ var init_screen = __esm({
|
|
|
5458
5722
|
const promptRows = this.promptRows();
|
|
5459
5723
|
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5460
5724
|
const queuedRows = this.queuedRows();
|
|
5461
|
-
const
|
|
5725
|
+
const chipRows = this.chipRows();
|
|
5726
|
+
const completionBottom = separatorRow - 1 - queuedRows - chipRows;
|
|
5462
5727
|
const completionTop = completionBottom - rows + 1;
|
|
5463
5728
|
let nameWidth = 0;
|
|
5464
5729
|
for (const item of this.completions.slice(0, rows)) {
|
|
@@ -5491,6 +5756,58 @@ var init_screen = __esm({
|
|
|
5491
5756
|
});
|
|
5492
5757
|
}
|
|
5493
5758
|
}
|
|
5759
|
+
// Chip zone: one row per attached image, sitting between the queued
|
|
5760
|
+
// zone and the separator (closest to the user's draft). Each row
|
|
5761
|
+
// shows "📎 <name> · <size>" plus, in iTerm2-capable terminals, a
|
|
5762
|
+
// tiny inline thumbnail at the end. Overflow collapses into a
|
|
5763
|
+
// single "+ N more attached" row.
|
|
5764
|
+
drawAttachmentChipZone() {
|
|
5765
|
+
const rows = this.chipRows();
|
|
5766
|
+
if (rows === 0) {
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
const w = this.term.width;
|
|
5770
|
+
const promptRows = this.promptRows();
|
|
5771
|
+
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5772
|
+
const chipBottom = separatorRow - 1;
|
|
5773
|
+
const chipTop = chipBottom - rows + 1;
|
|
5774
|
+
const iterm = this.isIterm2();
|
|
5775
|
+
for (let i = 0; i < rows; i++) {
|
|
5776
|
+
const row = chipTop + i;
|
|
5777
|
+
const isLast = i === rows - 1 && this.attachments.length > MAX_CHIP_ROWS;
|
|
5778
|
+
const overflow = this.attachments.length - MAX_CHIP_ROWS;
|
|
5779
|
+
const att = this.attachments[i];
|
|
5780
|
+
const label = att ? `${att.name ?? "image"} \xB7 ${formatSize(att.sizeBytes)}` : "";
|
|
5781
|
+
const sig = isLast ? `chip|${w}|overflow|${overflow}` : att ? `chip|${w}|${iterm ? "i" : "t"}|${label}|${att.sizeBytes}` : `chip|${w}|empty`;
|
|
5782
|
+
this.paintRow(row, sig, () => {
|
|
5783
|
+
if (isLast) {
|
|
5784
|
+
this.term.dim(` \u{1F4CE} + ${overflow + 1} more attached`);
|
|
5785
|
+
return;
|
|
5786
|
+
}
|
|
5787
|
+
if (!att) {
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5790
|
+
this.term(" ").yellow(`\u{1F4CE} ${label}`);
|
|
5791
|
+
if (iterm) {
|
|
5792
|
+
this.term(" ");
|
|
5793
|
+
this.writeIterm2Image(att.data, 1);
|
|
5794
|
+
}
|
|
5795
|
+
});
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
isIterm2() {
|
|
5799
|
+
const env = process.env;
|
|
5800
|
+
return env.LC_TERMINAL === "iTerm2" || env.TERM_PROGRAM === "iTerm.app";
|
|
5801
|
+
}
|
|
5802
|
+
// Emits the iTerm2 OSC 1337 inline image escape at the current
|
|
5803
|
+
// cursor position. Wraps in DCS-passthrough when tmux is detected
|
|
5804
|
+
// (requires `set -g allow-passthrough on` in the user's tmux conf).
|
|
5805
|
+
// Caller is responsible for knowing iTerm2 is the active terminal.
|
|
5806
|
+
writeIterm2Image(base64, heightCells) {
|
|
5807
|
+
process.stdout.write(
|
|
5808
|
+
buildIterm2ImageEscape(base64, heightCells, Boolean(process.env.TMUX))
|
|
5809
|
+
);
|
|
5810
|
+
}
|
|
5494
5811
|
drawQueuedZone() {
|
|
5495
5812
|
const rows = this.queuedRows();
|
|
5496
5813
|
if (rows === 0) {
|
|
@@ -5499,7 +5816,8 @@ var init_screen = __esm({
|
|
|
5499
5816
|
const w = this.term.width;
|
|
5500
5817
|
const promptRows = this.promptRows();
|
|
5501
5818
|
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5502
|
-
const
|
|
5819
|
+
const chipRows = this.chipRows();
|
|
5820
|
+
const queuedBottom = separatorRow - 1 - chipRows;
|
|
5503
5821
|
const queuedTop = queuedBottom - rows + 1;
|
|
5504
5822
|
const editingIndex = this.dispatcher.state().queueIndex;
|
|
5505
5823
|
for (let i = 0; i < rows; i++) {
|
|
@@ -5648,6 +5966,8 @@ var init_screen = __esm({
|
|
|
5648
5966
|
}
|
|
5649
5967
|
} else if (this.banner.status === "disconnected") {
|
|
5650
5968
|
this.term.brightRed(`${dot} ${this.banner.status}`);
|
|
5969
|
+
} else if (this.banner.status === "cold") {
|
|
5970
|
+
this.term.brightMagenta(`${dot} ${this.banner.status}`);
|
|
5651
5971
|
} else {
|
|
5652
5972
|
this.term.brightGreen(`${dot} ${this.banner.status}`);
|
|
5653
5973
|
}
|
|
@@ -5802,6 +6122,9 @@ var init_screen = __esm({
|
|
|
5802
6122
|
if (line.ansi) {
|
|
5803
6123
|
wrappedLine.ansi = true;
|
|
5804
6124
|
}
|
|
6125
|
+
if (i === 0 && line.iterm2Image) {
|
|
6126
|
+
wrappedLine.iterm2Image = line.iterm2Image;
|
|
6127
|
+
}
|
|
5805
6128
|
if (id !== void 0 && chunk.length > 0) {
|
|
5806
6129
|
const found = line.body.indexOf(chunk, scanPos);
|
|
5807
6130
|
const colOffset = found === -1 ? scanPos : found;
|
|
@@ -5847,6 +6170,12 @@ var init_screen = __esm({
|
|
|
5847
6170
|
if (line.ansi || line.body.includes("^")) {
|
|
5848
6171
|
this.term.styleReset();
|
|
5849
6172
|
}
|
|
6173
|
+
if (line.iterm2Image && this.isIterm2()) {
|
|
6174
|
+
this.writeIterm2Image(
|
|
6175
|
+
line.iterm2Image.data,
|
|
6176
|
+
line.iterm2Image.heightCells
|
|
6177
|
+
);
|
|
6178
|
+
}
|
|
5850
6179
|
}
|
|
5851
6180
|
};
|
|
5852
6181
|
NON_ASCII = /[^\x20-\x7e]/;
|
|
@@ -5891,6 +6220,17 @@ var init_input = __esm({
|
|
|
5891
6220
|
// here so ^Y can yank it back. Standard readline keeps a stack; we
|
|
5892
6221
|
// only keep one slot because that's what 99% of yank uses look like.
|
|
5893
6222
|
killBuffer = "";
|
|
6223
|
+
// Images attached to the current draft. Cleared in the same paths
|
|
6224
|
+
// that clear the text buffer (clearBuffer, after send). Queue
|
|
6225
|
+
// navigation snapshots/restores them alongside savedDraft so up/down
|
|
6226
|
+
// through queued items doesn't drop chips.
|
|
6227
|
+
attachments = [];
|
|
6228
|
+
// Snapshot of `attachments` taken when the user starts walking
|
|
6229
|
+
// history/queue with chips already attached. Restored alongside the
|
|
6230
|
+
// text draft when the walk ends. Distinct from savedDraft because
|
|
6231
|
+
// queue slots (which may carry their own attachments — though we
|
|
6232
|
+
// don't surface that yet) shouldn't blend with the current draft's.
|
|
6233
|
+
savedAttachments = null;
|
|
5894
6234
|
constructor(opts = {}) {
|
|
5895
6235
|
this.history = [...opts.history ?? []];
|
|
5896
6236
|
this.planMode = opts.planMode ?? false;
|
|
@@ -5903,9 +6243,22 @@ var init_input = __esm({
|
|
|
5903
6243
|
planMode: this.planMode,
|
|
5904
6244
|
historyIndex: this.historyIndex,
|
|
5905
6245
|
queueIndex: this.queueIndex,
|
|
6246
|
+
attachments: [...this.attachments],
|
|
5906
6247
|
historySearchQuery: this.historySearch?.query ?? null
|
|
5907
6248
|
};
|
|
5908
6249
|
}
|
|
6250
|
+
// App calls this after asynchronously acquiring an image (drag-drop
|
|
6251
|
+
// file read, clipboard shellout). The dispatcher just records it;
|
|
6252
|
+
// chip rendering and capability gating live in the app/screen layer.
|
|
6253
|
+
addAttachment(attachment) {
|
|
6254
|
+
this.attachments.push(attachment);
|
|
6255
|
+
}
|
|
6256
|
+
removeAttachment(index) {
|
|
6257
|
+
if (index < 0 || index >= this.attachments.length) {
|
|
6258
|
+
return;
|
|
6259
|
+
}
|
|
6260
|
+
this.attachments.splice(index, 1);
|
|
6261
|
+
}
|
|
5909
6262
|
setTurnRunning(running) {
|
|
5910
6263
|
this.turnRunning = running;
|
|
5911
6264
|
}
|
|
@@ -5936,13 +6289,17 @@ var init_input = __esm({
|
|
|
5936
6289
|
}
|
|
5937
6290
|
// Public seed for the buffer (used for Escape pre-fill). Treated like a
|
|
5938
6291
|
// fresh draft: nav state and any saved draft are cleared, cursor lands
|
|
5939
|
-
// at the end so the user can edit immediately.
|
|
5940
|
-
|
|
6292
|
+
// at the end so the user can edit immediately. Attachments restore
|
|
6293
|
+
// alongside the text so a cancelled turn's chips land back in the
|
|
6294
|
+
// draft together with the typed prompt.
|
|
6295
|
+
setBuffer(text, attachments = []) {
|
|
5941
6296
|
this.loadEntry(text);
|
|
5942
6297
|
this.historyIndex = -1;
|
|
5943
6298
|
this.queueIndex = -1;
|
|
5944
6299
|
this.savedDraft = null;
|
|
6300
|
+
this.savedAttachments = null;
|
|
5945
6301
|
this.historySearch = null;
|
|
6302
|
+
this.attachments = [...attachments];
|
|
5946
6303
|
}
|
|
5947
6304
|
feed(event) {
|
|
5948
6305
|
if (this.historySearch !== null) {
|
|
@@ -5988,6 +6345,9 @@ var init_input = __esm({
|
|
|
5988
6345
|
this.insertText(event.text);
|
|
5989
6346
|
return [];
|
|
5990
6347
|
}
|
|
6348
|
+
if (event.type === "attachment-paths") {
|
|
6349
|
+
return [];
|
|
6350
|
+
}
|
|
5991
6351
|
return this.handleKey(event.name);
|
|
5992
6352
|
}
|
|
5993
6353
|
handleKey(name) {
|
|
@@ -6064,6 +6424,8 @@ var init_input = __esm({
|
|
|
6064
6424
|
case "ctrl-u":
|
|
6065
6425
|
this.killLine();
|
|
6066
6426
|
return [];
|
|
6427
|
+
case "ctrl-v":
|
|
6428
|
+
return [{ type: "attachment-request", source: "clipboard" }];
|
|
6067
6429
|
case "ctrl-w":
|
|
6068
6430
|
this.killWord();
|
|
6069
6431
|
return [];
|
|
@@ -6096,7 +6458,9 @@ var init_input = __esm({
|
|
|
6096
6458
|
this.historyIndex = -1;
|
|
6097
6459
|
this.queueIndex = -1;
|
|
6098
6460
|
this.savedDraft = null;
|
|
6461
|
+
this.savedAttachments = null;
|
|
6099
6462
|
this.historySearch = null;
|
|
6463
|
+
this.attachments = [];
|
|
6100
6464
|
}
|
|
6101
6465
|
insertChar(ch) {
|
|
6102
6466
|
if (ch.length === 0) {
|
|
@@ -6248,6 +6612,8 @@ var init_input = __esm({
|
|
|
6248
6612
|
row: this.row,
|
|
6249
6613
|
col: this.col
|
|
6250
6614
|
};
|
|
6615
|
+
this.savedAttachments = [...this.attachments];
|
|
6616
|
+
this.attachments = [];
|
|
6251
6617
|
if (this.queue.length > 0) {
|
|
6252
6618
|
this.queueIndex = this.queue.length - 1;
|
|
6253
6619
|
this.loadEntry(this.queue[this.queueIndex] ?? "");
|
|
@@ -6320,6 +6686,8 @@ var init_input = __esm({
|
|
|
6320
6686
|
this.row = this.savedDraft.row;
|
|
6321
6687
|
this.col = this.savedDraft.col;
|
|
6322
6688
|
this.savedDraft = null;
|
|
6689
|
+
this.attachments = this.savedAttachments ?? [];
|
|
6690
|
+
this.savedAttachments = null;
|
|
6323
6691
|
} else {
|
|
6324
6692
|
this.clearBuffer();
|
|
6325
6693
|
}
|
|
@@ -6473,18 +6841,20 @@ var init_input = __esm({
|
|
|
6473
6841
|
const text = this.bufferText();
|
|
6474
6842
|
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
6475
6843
|
const index = this.queueIndex;
|
|
6844
|
+
const attachments2 = [...this.attachments];
|
|
6476
6845
|
this.clearBuffer();
|
|
6477
6846
|
if (text.trim().length === 0) {
|
|
6478
6847
|
return [{ type: "queue-remove", index }];
|
|
6479
6848
|
}
|
|
6480
|
-
return [{ type: "queue-edit", index, text }];
|
|
6849
|
+
return [{ type: "queue-edit", index, text, attachments: attachments2 }];
|
|
6481
6850
|
}
|
|
6482
|
-
if (text.trim().length === 0) {
|
|
6851
|
+
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
6483
6852
|
return [];
|
|
6484
6853
|
}
|
|
6485
6854
|
const planMode = this.planMode;
|
|
6855
|
+
const attachments = [...this.attachments];
|
|
6486
6856
|
this.clearBuffer();
|
|
6487
|
-
return [{ type: "send", text, planMode }];
|
|
6857
|
+
return [{ type: "send", text, planMode, attachments }];
|
|
6488
6858
|
}
|
|
6489
6859
|
// Home: jump to the very start of the prompt buffer. If we're already
|
|
6490
6860
|
// there, fall through to scrolling the scrollback to its top.
|
|
@@ -6509,13 +6879,15 @@ var init_input = __esm({
|
|
|
6509
6879
|
return [{ type: "scroll-to-bottom" }];
|
|
6510
6880
|
}
|
|
6511
6881
|
handleCtrlC() {
|
|
6512
|
-
if (!this.bufferIsEmpty()) {
|
|
6882
|
+
if (!this.bufferIsEmpty() || this.attachments.length > 0) {
|
|
6513
6883
|
this.buffer = [""];
|
|
6514
6884
|
this.row = 0;
|
|
6515
6885
|
this.col = 0;
|
|
6886
|
+
this.attachments = [];
|
|
6516
6887
|
if (this.queueIndex === -1) {
|
|
6517
6888
|
this.historyIndex = -1;
|
|
6518
6889
|
this.savedDraft = null;
|
|
6890
|
+
this.savedAttachments = null;
|
|
6519
6891
|
}
|
|
6520
6892
|
return [];
|
|
6521
6893
|
}
|
|
@@ -6533,6 +6905,232 @@ var init_input = __esm({
|
|
|
6533
6905
|
}
|
|
6534
6906
|
});
|
|
6535
6907
|
|
|
6908
|
+
// src/tui/clipboard.ts
|
|
6909
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
6910
|
+
import fs14 from "fs/promises";
|
|
6911
|
+
import os4 from "os";
|
|
6912
|
+
import path10 from "path";
|
|
6913
|
+
async function readClipboard(envIn = {}) {
|
|
6914
|
+
const env = { ...defaultEnv, ...envIn };
|
|
6915
|
+
if (env.platform === "darwin") {
|
|
6916
|
+
return readMacOS(env);
|
|
6917
|
+
}
|
|
6918
|
+
if (env.platform === "linux") {
|
|
6919
|
+
return readLinux(env);
|
|
6920
|
+
}
|
|
6921
|
+
return {
|
|
6922
|
+
ok: false,
|
|
6923
|
+
reason: `clipboard paste is not supported on ${env.platform}`
|
|
6924
|
+
};
|
|
6925
|
+
}
|
|
6926
|
+
async function readMacOS(env) {
|
|
6927
|
+
const tmpPath = path10.join(
|
|
6928
|
+
env.tmpdir(),
|
|
6929
|
+
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
6930
|
+
);
|
|
6931
|
+
const script = [
|
|
6932
|
+
"set png_data to the clipboard as \xABclass PNGf\xBB",
|
|
6933
|
+
`set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
|
|
6934
|
+
"write png_data to out_file",
|
|
6935
|
+
"close access out_file"
|
|
6936
|
+
];
|
|
6937
|
+
const args = [];
|
|
6938
|
+
for (const line of script) {
|
|
6939
|
+
args.push("-e", line);
|
|
6940
|
+
}
|
|
6941
|
+
try {
|
|
6942
|
+
await run2(env.spawn, "osascript", args);
|
|
6943
|
+
const img = await readFileAsAttachment(tmpPath, true);
|
|
6944
|
+
if (img.ok) {
|
|
6945
|
+
return img;
|
|
6946
|
+
}
|
|
6947
|
+
if (img.reason.startsWith("clipboard image is")) {
|
|
6948
|
+
return img;
|
|
6949
|
+
}
|
|
6950
|
+
} catch {
|
|
6951
|
+
await fs14.unlink(tmpPath).catch(() => void 0);
|
|
6952
|
+
}
|
|
6953
|
+
try {
|
|
6954
|
+
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
6955
|
+
if (buf.length === 0) {
|
|
6956
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
6957
|
+
}
|
|
6958
|
+
return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
|
|
6959
|
+
} catch {
|
|
6960
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
async function readLinux(env) {
|
|
6964
|
+
const tool = await detectLinuxTool(env);
|
|
6965
|
+
if (!tool) {
|
|
6966
|
+
return {
|
|
6967
|
+
ok: false,
|
|
6968
|
+
reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
|
|
6969
|
+
};
|
|
6970
|
+
}
|
|
6971
|
+
try {
|
|
6972
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
|
|
6973
|
+
if (buf.length > 0) {
|
|
6974
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
6975
|
+
return {
|
|
6976
|
+
ok: false,
|
|
6977
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
return {
|
|
6981
|
+
ok: true,
|
|
6982
|
+
kind: "image",
|
|
6983
|
+
attachment: {
|
|
6984
|
+
mimeType: "image/png",
|
|
6985
|
+
data: buf.toString("base64"),
|
|
6986
|
+
sizeBytes: buf.length
|
|
6987
|
+
}
|
|
6988
|
+
};
|
|
6989
|
+
}
|
|
6990
|
+
} catch {
|
|
6991
|
+
}
|
|
6992
|
+
try {
|
|
6993
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
|
|
6994
|
+
if (buf.length === 0) {
|
|
6995
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
6996
|
+
}
|
|
6997
|
+
return {
|
|
6998
|
+
ok: true,
|
|
6999
|
+
kind: "text",
|
|
7000
|
+
text: normalizeText(buf.toString("utf-8"))
|
|
7001
|
+
};
|
|
7002
|
+
} catch {
|
|
7003
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
async function detectLinuxTool(env) {
|
|
7007
|
+
if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
|
|
7008
|
+
return {
|
|
7009
|
+
cmd: "wl-paste",
|
|
7010
|
+
imageArgs: ["-t", "image/png"],
|
|
7011
|
+
// -n: drop trailing newline wl-paste adds by default. We further
|
|
7012
|
+
// normalize line endings below, but this avoids a spurious
|
|
7013
|
+
// empty trailing row from a single-line clipboard text.
|
|
7014
|
+
textArgs: ["-n"]
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
7017
|
+
if (env.env.DISPLAY && await which(env, "xclip")) {
|
|
7018
|
+
return {
|
|
7019
|
+
cmd: "xclip",
|
|
7020
|
+
imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
7021
|
+
textArgs: ["-selection", "clipboard", "-o"]
|
|
7022
|
+
};
|
|
7023
|
+
}
|
|
7024
|
+
return null;
|
|
7025
|
+
}
|
|
7026
|
+
function normalizeText(text) {
|
|
7027
|
+
return text.replace(/\r\n?/g, "\n");
|
|
7028
|
+
}
|
|
7029
|
+
async function which(env, cmd) {
|
|
7030
|
+
try {
|
|
7031
|
+
await run2(env.spawn, "which", [cmd]);
|
|
7032
|
+
return true;
|
|
7033
|
+
} catch {
|
|
7034
|
+
return false;
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
async function readFileAsAttachment(p, unlinkAfter) {
|
|
7038
|
+
try {
|
|
7039
|
+
const buf = await fs14.readFile(p);
|
|
7040
|
+
if (unlinkAfter) {
|
|
7041
|
+
await fs14.unlink(p).catch(() => void 0);
|
|
7042
|
+
}
|
|
7043
|
+
if (buf.length === 0) {
|
|
7044
|
+
return { ok: false, reason: "no image on clipboard" };
|
|
7045
|
+
}
|
|
7046
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
7047
|
+
return {
|
|
7048
|
+
ok: false,
|
|
7049
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7050
|
+
};
|
|
7051
|
+
}
|
|
7052
|
+
const mimeType = mimeFromExtension(p) ?? "image/png";
|
|
7053
|
+
return {
|
|
7054
|
+
ok: true,
|
|
7055
|
+
kind: "image",
|
|
7056
|
+
attachment: {
|
|
7057
|
+
mimeType,
|
|
7058
|
+
data: buf.toString("base64"),
|
|
7059
|
+
sizeBytes: buf.length
|
|
7060
|
+
}
|
|
7061
|
+
};
|
|
7062
|
+
} catch {
|
|
7063
|
+
return { ok: false, reason: "failed to read clipboard image" };
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
function run2(spawn6, cmd, args) {
|
|
7067
|
+
return new Promise((resolve5, reject) => {
|
|
7068
|
+
const proc = spawn6(cmd, args);
|
|
7069
|
+
proc.stdout?.on("data", () => void 0);
|
|
7070
|
+
proc.stderr?.on("data", () => void 0);
|
|
7071
|
+
proc.on("error", reject);
|
|
7072
|
+
proc.on("close", (code) => {
|
|
7073
|
+
if (code === 0) {
|
|
7074
|
+
resolve5();
|
|
7075
|
+
} else {
|
|
7076
|
+
reject(new Error(`${cmd} exited ${code}`));
|
|
7077
|
+
}
|
|
7078
|
+
});
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
function runCapture(spawn6, cmd, args) {
|
|
7082
|
+
return new Promise((resolve5, reject) => {
|
|
7083
|
+
const proc = spawn6(cmd, args);
|
|
7084
|
+
const chunks = [];
|
|
7085
|
+
let stdoutEnded = proc.stdout === null;
|
|
7086
|
+
let closedCode = null;
|
|
7087
|
+
let settled = false;
|
|
7088
|
+
const settle = () => {
|
|
7089
|
+
if (settled || !stdoutEnded || closedCode === null) {
|
|
7090
|
+
return;
|
|
7091
|
+
}
|
|
7092
|
+
settled = true;
|
|
7093
|
+
if (closedCode === 0) {
|
|
7094
|
+
resolve5(Buffer.concat(chunks));
|
|
7095
|
+
} else {
|
|
7096
|
+
reject(new Error(`${cmd} exited ${closedCode}`));
|
|
7097
|
+
}
|
|
7098
|
+
};
|
|
7099
|
+
proc.stdout?.on("data", (chunk) => {
|
|
7100
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
7101
|
+
});
|
|
7102
|
+
proc.stdout?.on("end", () => {
|
|
7103
|
+
stdoutEnded = true;
|
|
7104
|
+
settle();
|
|
7105
|
+
});
|
|
7106
|
+
proc.stderr?.on("data", () => void 0);
|
|
7107
|
+
proc.on("error", (err) => {
|
|
7108
|
+
if (settled) {
|
|
7109
|
+
return;
|
|
7110
|
+
}
|
|
7111
|
+
settled = true;
|
|
7112
|
+
reject(err);
|
|
7113
|
+
});
|
|
7114
|
+
proc.on("close", (code) => {
|
|
7115
|
+
closedCode = code ?? 0;
|
|
7116
|
+
settle();
|
|
7117
|
+
});
|
|
7118
|
+
});
|
|
7119
|
+
}
|
|
7120
|
+
var defaultEnv;
|
|
7121
|
+
var init_clipboard = __esm({
|
|
7122
|
+
"src/tui/clipboard.ts"() {
|
|
7123
|
+
"use strict";
|
|
7124
|
+
init_attachments();
|
|
7125
|
+
defaultEnv = {
|
|
7126
|
+
platform: process.platform,
|
|
7127
|
+
env: process.env,
|
|
7128
|
+
spawn: nodeSpawn,
|
|
7129
|
+
tmpdir: os4.tmpdir
|
|
7130
|
+
};
|
|
7131
|
+
}
|
|
7132
|
+
});
|
|
7133
|
+
|
|
6536
7134
|
// src/tui/completion.ts
|
|
6537
7135
|
function longestCommonPrefix(names) {
|
|
6538
7136
|
if (names.length === 0) {
|
|
@@ -6867,8 +7465,29 @@ import chalk from "chalk";
|
|
|
6867
7465
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
6868
7466
|
function formatEvent(event) {
|
|
6869
7467
|
switch (event.kind) {
|
|
6870
|
-
case "user-text":
|
|
6871
|
-
|
|
7468
|
+
case "user-text": {
|
|
7469
|
+
const lines = formatBlock(
|
|
7470
|
+
event.text,
|
|
7471
|
+
"\u258E ",
|
|
7472
|
+
"user",
|
|
7473
|
+
void 0,
|
|
7474
|
+
event.sentBy,
|
|
7475
|
+
true
|
|
7476
|
+
);
|
|
7477
|
+
if (event.attachments && event.attachments.length > 0) {
|
|
7478
|
+
for (const a of event.attachments) {
|
|
7479
|
+
lines.push({
|
|
7480
|
+
prefix: "\u258E ",
|
|
7481
|
+
prefixStyle: "user",
|
|
7482
|
+
body: `\u{1F4CE} ${a.name ?? "image"}`,
|
|
7483
|
+
bodyStyle: "user",
|
|
7484
|
+
fillRow: true,
|
|
7485
|
+
iterm2Image: { data: a.data, heightCells: 5 }
|
|
7486
|
+
});
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
return lines;
|
|
7490
|
+
}
|
|
6872
7491
|
case "agent-text":
|
|
6873
7492
|
return formatBlock(event.text, " ", "agent");
|
|
6874
7493
|
case "agent-thought":
|
|
@@ -7198,6 +7817,8 @@ var init_format = __esm({
|
|
|
7198
7817
|
import { appendFileSync, statSync, renameSync } from "fs";
|
|
7199
7818
|
import { nanoid as nanoid3 } from "nanoid";
|
|
7200
7819
|
import termkit from "terminal-kit";
|
|
7820
|
+
import fs15 from "fs/promises";
|
|
7821
|
+
import path11 from "path";
|
|
7201
7822
|
async function runTuiApp(opts) {
|
|
7202
7823
|
const config = await ensureConfig();
|
|
7203
7824
|
logMaxBytes = config.tui.logMaxBytes;
|
|
@@ -7315,6 +7936,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7315
7936
|
appendRender(event);
|
|
7316
7937
|
maybeDismissPermissionByToolUpdate(update);
|
|
7317
7938
|
});
|
|
7939
|
+
conn.onNotification("hydra-acp/session_closed", () => {
|
|
7940
|
+
if (pendingTurns > 0) {
|
|
7941
|
+
adjustPendingTurns(-pendingTurns);
|
|
7942
|
+
}
|
|
7943
|
+
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
7944
|
+
if (screenReady) {
|
|
7945
|
+
screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
|
|
7946
|
+
}
|
|
7947
|
+
});
|
|
7318
7948
|
const handlePermissionResolved = (update) => {
|
|
7319
7949
|
const u = update ?? {};
|
|
7320
7950
|
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
|
|
@@ -7418,6 +8048,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7418
8048
|
});
|
|
7419
8049
|
let upstreamSessionId;
|
|
7420
8050
|
let agentInfoName;
|
|
8051
|
+
let agentAcceptsImages = true;
|
|
7421
8052
|
try {
|
|
7422
8053
|
const initResult = await conn.request("initialize", {
|
|
7423
8054
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
@@ -7428,6 +8059,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7428
8059
|
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
7429
8060
|
});
|
|
7430
8061
|
agentInfoName = initResult?.agentInfo?.name;
|
|
8062
|
+
const imageCap = initResult?.agentCapabilities?.promptCapabilities?.image;
|
|
8063
|
+
if (imageCap === false) {
|
|
8064
|
+
agentAcceptsImages = false;
|
|
8065
|
+
}
|
|
7431
8066
|
} catch {
|
|
7432
8067
|
}
|
|
7433
8068
|
let resolvedSessionId = ctx.sessionId;
|
|
@@ -7511,6 +8146,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7511
8146
|
repaintThrottleMs: config.tui.repaintThrottleMs,
|
|
7512
8147
|
maxScrollbackLines: config.tui.maxScrollbackLines,
|
|
7513
8148
|
mouse: config.tui.mouse,
|
|
8149
|
+
progressIndicator: config.tui.progressIndicator,
|
|
7514
8150
|
onKey: (events) => {
|
|
7515
8151
|
for (const ev of events) {
|
|
7516
8152
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
@@ -7525,6 +8161,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7525
8161
|
if (tryHandleCompletionKey(ev)) {
|
|
7526
8162
|
continue;
|
|
7527
8163
|
}
|
|
8164
|
+
if (ev.type === "attachment-paths") {
|
|
8165
|
+
void handleAttachmentPaths(ev.paths);
|
|
8166
|
+
continue;
|
|
8167
|
+
}
|
|
7528
8168
|
const effects = dispatcher.feed(ev);
|
|
7529
8169
|
for (const effect of effects) {
|
|
7530
8170
|
handleEffect(effect);
|
|
@@ -7534,6 +8174,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7534
8174
|
screen.setBannerSearchIndicator(
|
|
7535
8175
|
dispatcher.state().historySearchQuery
|
|
7536
8176
|
);
|
|
8177
|
+
screen.setAttachments(dispatcher.state().attachments);
|
|
7537
8178
|
screen.refreshPrompt();
|
|
7538
8179
|
}
|
|
7539
8180
|
});
|
|
@@ -7824,7 +8465,8 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7824
8465
|
const choice = await pickSession(term, {
|
|
7825
8466
|
cwd: resolvedCwd,
|
|
7826
8467
|
sessions,
|
|
7827
|
-
config
|
|
8468
|
+
config,
|
|
8469
|
+
currentSessionId: resolvedSessionId
|
|
7828
8470
|
});
|
|
7829
8471
|
if (choice.kind === "abort") {
|
|
7830
8472
|
screen.start();
|
|
@@ -7855,13 +8497,17 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7855
8497
|
const handleEffect = (effect) => {
|
|
7856
8498
|
switch (effect.type) {
|
|
7857
8499
|
case "send":
|
|
7858
|
-
enqueuePrompt(effect.text, effect.planMode);
|
|
8500
|
+
enqueuePrompt(effect.text, effect.planMode, effect.attachments);
|
|
7859
8501
|
return;
|
|
7860
8502
|
case "queue-edit": {
|
|
7861
8503
|
const realIdx = effect.index + queueHeadOffset();
|
|
7862
8504
|
const existing = promptQueue[realIdx];
|
|
7863
8505
|
if (existing) {
|
|
7864
|
-
promptQueue[realIdx] = {
|
|
8506
|
+
promptQueue[realIdx] = {
|
|
8507
|
+
text: effect.text,
|
|
8508
|
+
planMode: existing.planMode,
|
|
8509
|
+
attachments: effect.attachments
|
|
8510
|
+
};
|
|
7865
8511
|
refreshQueueDisplay();
|
|
7866
8512
|
}
|
|
7867
8513
|
return;
|
|
@@ -7880,7 +8526,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7880
8526
|
const waitingEmpty = promptQueue.length <= headOffset;
|
|
7881
8527
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
7882
8528
|
if (waitingEmpty && bufferEmpty) {
|
|
7883
|
-
pendingPrefill =
|
|
8529
|
+
pendingPrefill = {
|
|
8530
|
+
text: turnInFlight.text,
|
|
8531
|
+
attachments: turnInFlight.attachments
|
|
8532
|
+
};
|
|
7884
8533
|
}
|
|
7885
8534
|
}
|
|
7886
8535
|
if (turnInFlight) {
|
|
@@ -7919,17 +8568,81 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7919
8568
|
screen.enterScrollbackSearch();
|
|
7920
8569
|
screen.updateScrollbackSearchTerm(effect.query);
|
|
7921
8570
|
return;
|
|
8571
|
+
case "attachment-request":
|
|
8572
|
+
void handleClipboardAttachment();
|
|
8573
|
+
return;
|
|
8574
|
+
}
|
|
8575
|
+
};
|
|
8576
|
+
const handleAttachmentPaths = async (paths2) => {
|
|
8577
|
+
if (!agentAcceptsImages) {
|
|
8578
|
+
screen.notify("agent does not accept image attachments");
|
|
8579
|
+
return;
|
|
8580
|
+
}
|
|
8581
|
+
let added = 0;
|
|
8582
|
+
for (const p of paths2) {
|
|
8583
|
+
const mimeType = mimeFromExtension(p);
|
|
8584
|
+
if (!mimeType) {
|
|
8585
|
+
screen.notify(`unsupported image type: ${path11.basename(p)}`);
|
|
8586
|
+
continue;
|
|
8587
|
+
}
|
|
8588
|
+
try {
|
|
8589
|
+
const buf = await fs15.readFile(p);
|
|
8590
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
8591
|
+
screen.notify(
|
|
8592
|
+
`image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
|
|
8593
|
+
);
|
|
8594
|
+
continue;
|
|
8595
|
+
}
|
|
8596
|
+
dispatcher.addAttachment({
|
|
8597
|
+
mimeType,
|
|
8598
|
+
data: buf.toString("base64"),
|
|
8599
|
+
name: path11.basename(p),
|
|
8600
|
+
sizeBytes: buf.length
|
|
8601
|
+
});
|
|
8602
|
+
added++;
|
|
8603
|
+
} catch (err) {
|
|
8604
|
+
screen.notify(`cannot read ${path11.basename(p)}: ${err.message}`);
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
if (added > 0) {
|
|
8608
|
+
screen.setAttachments(dispatcher.state().attachments);
|
|
8609
|
+
screen.refreshPrompt();
|
|
8610
|
+
}
|
|
8611
|
+
};
|
|
8612
|
+
const handleClipboardAttachment = async () => {
|
|
8613
|
+
const result = await readClipboard();
|
|
8614
|
+
if (!result.ok) {
|
|
8615
|
+
screen.notify(result.reason);
|
|
8616
|
+
return;
|
|
8617
|
+
}
|
|
8618
|
+
if (result.kind === "image") {
|
|
8619
|
+
if (!agentAcceptsImages) {
|
|
8620
|
+
screen.notify("agent does not accept image attachments");
|
|
8621
|
+
return;
|
|
8622
|
+
}
|
|
8623
|
+
dispatcher.addAttachment(result.attachment);
|
|
8624
|
+
screen.setAttachments(dispatcher.state().attachments);
|
|
8625
|
+
screen.refreshPrompt();
|
|
8626
|
+
return;
|
|
7922
8627
|
}
|
|
8628
|
+
const effects = dispatcher.feed({ type: "paste", text: result.text });
|
|
8629
|
+
for (const effect of effects) {
|
|
8630
|
+
handleEffect(effect);
|
|
8631
|
+
}
|
|
8632
|
+
screen.refreshPrompt();
|
|
7923
8633
|
};
|
|
7924
8634
|
const promptQueue = [];
|
|
7925
8635
|
let workerActive = false;
|
|
7926
8636
|
const refreshQueueDisplay = () => {
|
|
7927
8637
|
const waiting = promptQueue.slice(workerActive ? 1 : 0);
|
|
7928
|
-
|
|
8638
|
+
const displayTexts = waiting.map(
|
|
8639
|
+
(p) => p.attachments.length > 0 ? `${p.text} \xB7 \u{1F4CE}\xD7${p.attachments.length}` : p.text
|
|
8640
|
+
);
|
|
8641
|
+
screen.setQueuedPrompts(displayTexts);
|
|
7929
8642
|
screen.setBanner({ queued: waiting.length });
|
|
7930
8643
|
dispatcher.setQueue(waiting.map((p) => p.text));
|
|
7931
8644
|
};
|
|
7932
|
-
const enqueuePrompt = (text, planMode) => {
|
|
8645
|
+
const enqueuePrompt = (text, planMode, attachments) => {
|
|
7933
8646
|
screen.scrollToBottom();
|
|
7934
8647
|
if (handleBuiltinCommand(text)) {
|
|
7935
8648
|
return;
|
|
@@ -7937,7 +8650,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7937
8650
|
history = appendEntry(history, text);
|
|
7938
8651
|
dispatcher.setHistory(history);
|
|
7939
8652
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
7940
|
-
promptQueue.push({ text, planMode });
|
|
8653
|
+
promptQueue.push({ text, planMode, attachments });
|
|
7941
8654
|
refreshQueueDisplay();
|
|
7942
8655
|
tickWorker();
|
|
7943
8656
|
};
|
|
@@ -8104,31 +8817,38 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8104
8817
|
break;
|
|
8105
8818
|
}
|
|
8106
8819
|
refreshQueueDisplay();
|
|
8107
|
-
await processPrompt(next.text, next.planMode);
|
|
8820
|
+
await processPrompt(next.text, next.planMode, next.attachments);
|
|
8108
8821
|
promptQueue.shift();
|
|
8109
8822
|
}
|
|
8110
8823
|
} finally {
|
|
8111
8824
|
workerActive = false;
|
|
8112
8825
|
refreshQueueDisplay();
|
|
8113
8826
|
if (pendingPrefill !== null) {
|
|
8114
|
-
const text = pendingPrefill;
|
|
8827
|
+
const { text, attachments } = pendingPrefill;
|
|
8115
8828
|
pendingPrefill = null;
|
|
8116
8829
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
8117
8830
|
if (bufferEmpty) {
|
|
8118
|
-
dispatcher.setBuffer(text);
|
|
8831
|
+
dispatcher.setBuffer(text, attachments);
|
|
8119
8832
|
screen.refreshPrompt();
|
|
8120
8833
|
}
|
|
8121
8834
|
}
|
|
8122
8835
|
}
|
|
8123
8836
|
};
|
|
8124
|
-
const processPrompt = async (text, planMode) => {
|
|
8125
|
-
const userBlocks = [
|
|
8837
|
+
const processPrompt = async (text, planMode, attachments) => {
|
|
8838
|
+
const userBlocks = [];
|
|
8839
|
+
if (text.length > 0) {
|
|
8840
|
+
userBlocks.push({ type: "text", text });
|
|
8841
|
+
}
|
|
8842
|
+
for (const a of attachments) {
|
|
8843
|
+
userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
|
|
8844
|
+
}
|
|
8126
8845
|
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
8127
8846
|
adjustPendingTurns(1);
|
|
8128
|
-
appendRender({ kind: "user-text", text });
|
|
8847
|
+
appendRender({ kind: "user-text", text, attachments });
|
|
8129
8848
|
let cancelled = false;
|
|
8130
8849
|
turnInFlight = {
|
|
8131
8850
|
text,
|
|
8851
|
+
attachments,
|
|
8132
8852
|
cancel: () => {
|
|
8133
8853
|
if (cancelled) {
|
|
8134
8854
|
return;
|
|
@@ -8625,6 +9345,8 @@ var init_app = __esm({
|
|
|
8625
9345
|
init_picker();
|
|
8626
9346
|
init_screen();
|
|
8627
9347
|
init_input();
|
|
9348
|
+
init_attachments();
|
|
9349
|
+
init_clipboard();
|
|
8628
9350
|
init_completion();
|
|
8629
9351
|
init_render_update();
|
|
8630
9352
|
init_format();
|
|
@@ -9408,12 +10130,14 @@ var AgentInstance = class _AgentInstance {
|
|
|
9408
10130
|
killed = false;
|
|
9409
10131
|
stderrTail = "";
|
|
9410
10132
|
stderrTailBytes;
|
|
10133
|
+
logger;
|
|
9411
10134
|
exitHandlers = [];
|
|
9412
10135
|
constructor(opts, child) {
|
|
9413
10136
|
this.agentId = opts.agentId;
|
|
9414
10137
|
this.cwd = opts.cwd;
|
|
9415
10138
|
this.child = child;
|
|
9416
10139
|
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
10140
|
+
this.logger = opts.logger;
|
|
9417
10141
|
if (!child.stdout || !child.stdin) {
|
|
9418
10142
|
throw new Error("agent subprocess missing stdio");
|
|
9419
10143
|
}
|
|
@@ -9422,7 +10146,15 @@ var AgentInstance = class _AgentInstance {
|
|
|
9422
10146
|
child.stderr?.setEncoding("utf8");
|
|
9423
10147
|
child.stderr?.on("data", (chunk) => {
|
|
9424
10148
|
this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
|
|
9425
|
-
|
|
10149
|
+
if (this.logger) {
|
|
10150
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
10151
|
+
if (line.length > 0) {
|
|
10152
|
+
this.logger.info(`[${opts.agentId}] ${line}`);
|
|
10153
|
+
}
|
|
10154
|
+
}
|
|
10155
|
+
} else {
|
|
10156
|
+
process.stderr.write(`[${opts.agentId}] ${chunk}`);
|
|
10157
|
+
}
|
|
9426
10158
|
});
|
|
9427
10159
|
child.on("error", (err) => {
|
|
9428
10160
|
const msg = this.formatFailure(err.message);
|
|
@@ -9430,9 +10162,16 @@ var AgentInstance = class _AgentInstance {
|
|
|
9430
10162
|
});
|
|
9431
10163
|
child.on("exit", (code, signal) => {
|
|
9432
10164
|
this.exited = true;
|
|
9433
|
-
if (
|
|
10165
|
+
if (this.killed) {
|
|
10166
|
+
this.logger?.info(
|
|
10167
|
+
`agent ${opts.agentId} pid=${child.pid} exited after kill code=${code} signal=${signal}`
|
|
10168
|
+
);
|
|
10169
|
+
} else {
|
|
9434
10170
|
const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
|
|
9435
10171
|
this.connection.fail(new Error(this.formatFailure(reason)));
|
|
10172
|
+
this.logger?.warn(
|
|
10173
|
+
`agent ${opts.agentId} pid=${child.pid} exited unexpectedly code=${code} signal=${signal}`
|
|
10174
|
+
);
|
|
9436
10175
|
}
|
|
9437
10176
|
for (const handler of this.exitHandlers) {
|
|
9438
10177
|
handler(code, signal);
|
|
@@ -9453,7 +10192,15 @@ stderr: ${tail}` : reason;
|
|
|
9453
10192
|
const child = spawn3(opts.plan.command, opts.plan.args, {
|
|
9454
10193
|
cwd: opts.cwd,
|
|
9455
10194
|
env,
|
|
9456
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10195
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
10196
|
+
// setsid the agent into its own session/process group. The daemon
|
|
10197
|
+
// already runs in its own setsid'd session, but macOS terminals
|
|
10198
|
+
// (iTerm2, Terminal.app) sometimes still reach inherited child
|
|
10199
|
+
// processes when the user closes a window — putting the agent
|
|
10200
|
+
// one more session-boundary away keeps it alive across terminal
|
|
10201
|
+
// restarts. The daemon still owns the pipes, so this.kill()
|
|
10202
|
+
// continues to terminate it cleanly on idle/close.
|
|
10203
|
+
detached: true
|
|
9457
10204
|
});
|
|
9458
10205
|
return new _AgentInstance(opts, child);
|
|
9459
10206
|
}
|
|
@@ -9468,6 +10215,9 @@ stderr: ${tail}` : reason;
|
|
|
9468
10215
|
return;
|
|
9469
10216
|
}
|
|
9470
10217
|
this.killed = true;
|
|
10218
|
+
this.logger?.info(
|
|
10219
|
+
`agent ${this.agentId} pid=${this.child.pid} kill requested signal=${signal}`
|
|
10220
|
+
);
|
|
9471
10221
|
await this.connection.close().catch(() => void 0);
|
|
9472
10222
|
this.child.kill(signal);
|
|
9473
10223
|
}
|
|
@@ -9653,6 +10403,7 @@ var SessionManager = class {
|
|
|
9653
10403
|
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
9654
10404
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
9655
10405
|
this.defaultModels = options.defaultModels ?? {};
|
|
10406
|
+
this.logger = options.logger;
|
|
9656
10407
|
}
|
|
9657
10408
|
registry;
|
|
9658
10409
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -9667,6 +10418,7 @@ var SessionManager = class {
|
|
|
9667
10418
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
9668
10419
|
// back-to-back) don't lose writes via interleaved reads.
|
|
9669
10420
|
metaWriteQueues = /* @__PURE__ */ new Map();
|
|
10421
|
+
logger;
|
|
9670
10422
|
async create(params) {
|
|
9671
10423
|
const fresh = await this.bootstrapAgent({
|
|
9672
10424
|
agentId: params.agentId,
|
|
@@ -9684,6 +10436,7 @@ var SessionManager = class {
|
|
|
9684
10436
|
title: params.title,
|
|
9685
10437
|
agentArgs: params.agentArgs,
|
|
9686
10438
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
10439
|
+
logger: this.logger,
|
|
9687
10440
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
9688
10441
|
historyStore: this.histories,
|
|
9689
10442
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
@@ -9777,6 +10530,7 @@ var SessionManager = class {
|
|
|
9777
10530
|
title: params.title,
|
|
9778
10531
|
agentArgs: params.agentArgs,
|
|
9779
10532
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
10533
|
+
logger: this.logger,
|
|
9780
10534
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
9781
10535
|
historyStore: this.histories,
|
|
9782
10536
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
@@ -9823,6 +10577,7 @@ var SessionManager = class {
|
|
|
9823
10577
|
title: params.title,
|
|
9824
10578
|
agentArgs: params.agentArgs,
|
|
9825
10579
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
10580
|
+
logger: this.logger,
|
|
9826
10581
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
9827
10582
|
historyStore: this.histories,
|
|
9828
10583
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
@@ -10087,6 +10842,8 @@ var SessionManager = class {
|
|
|
10087
10842
|
agentId: r.agentId,
|
|
10088
10843
|
currentModel: r.currentModel,
|
|
10089
10844
|
currentUsage: r.currentUsage,
|
|
10845
|
+
importedFromMachine: r.importedFromMachine,
|
|
10846
|
+
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
10090
10847
|
updatedAt: used,
|
|
10091
10848
|
attachedClients: 0,
|
|
10092
10849
|
status: "cold"
|
|
@@ -10194,6 +10951,8 @@ var SessionManager = class {
|
|
|
10194
10951
|
lineageId: args.bundle.session.lineageId,
|
|
10195
10952
|
upstreamSessionId: "",
|
|
10196
10953
|
importedFromSessionId: args.bundle.session.sessionId,
|
|
10954
|
+
importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
|
|
10955
|
+
importedFromMachine: args.bundle.exportedFrom.machine,
|
|
10197
10956
|
agentId: args.bundle.session.agentId,
|
|
10198
10957
|
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
10199
10958
|
title: args.bundle.session.title,
|
|
@@ -10316,6 +11075,8 @@ function mergeForPersistence(session, existing) {
|
|
|
10316
11075
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
10317
11076
|
upstreamSessionId: session.upstreamSessionId,
|
|
10318
11077
|
importedFromSessionId: existing?.importedFromSessionId,
|
|
11078
|
+
importedFromUpstreamSessionId: existing?.importedFromUpstreamSessionId,
|
|
11079
|
+
importedFromMachine: existing?.importedFromMachine,
|
|
10319
11080
|
agentId: session.agentId,
|
|
10320
11081
|
cwd: session.cwd,
|
|
10321
11082
|
title: session.title,
|
|
@@ -11595,11 +12356,20 @@ async function startDaemon(config) {
|
|
|
11595
12356
|
await auth(request, reply);
|
|
11596
12357
|
});
|
|
11597
12358
|
const registry = new Registry(config);
|
|
11598
|
-
const
|
|
12359
|
+
const agentLogger = {
|
|
12360
|
+
info: (msg) => app.log.info(msg),
|
|
12361
|
+
warn: (msg) => app.log.warn(msg)
|
|
12362
|
+
};
|
|
12363
|
+
const spawner = (opts) => AgentInstance.spawn({
|
|
12364
|
+
...opts,
|
|
12365
|
+
stderrTailBytes: config.daemon.agentStderrTailBytes,
|
|
12366
|
+
logger: agentLogger
|
|
12367
|
+
});
|
|
11599
12368
|
const manager = new SessionManager(registry, spawner, void 0, {
|
|
11600
12369
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
11601
12370
|
defaultModels: config.defaultModels,
|
|
11602
|
-
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
|
|
12371
|
+
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
12372
|
+
logger: agentLogger
|
|
11603
12373
|
});
|
|
11604
12374
|
const extensions = new ExtensionManager(extensionList(config));
|
|
11605
12375
|
registerHealthRoutes(app, HYDRA_VERSION);
|
|
@@ -11836,6 +12606,7 @@ async function runDaemonStart(flags = {}) {
|
|
|
11836
12606
|
};
|
|
11837
12607
|
process.on("SIGINT", () => void shutdown());
|
|
11838
12608
|
process.on("SIGTERM", () => void shutdown());
|
|
12609
|
+
process.on("SIGHUP", () => void 0);
|
|
11839
12610
|
return;
|
|
11840
12611
|
}
|
|
11841
12612
|
spawnDaemonDetached();
|