@hydra-acp/cli 0.1.12 → 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 +675 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -749,6 +749,11 @@ var init_hydra_commands = __esm({
|
|
|
749
749
|
name: "hydra agent",
|
|
750
750
|
argsHint: "<agent>",
|
|
751
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)"
|
|
752
757
|
}
|
|
753
758
|
];
|
|
754
759
|
VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
@@ -1635,6 +1640,8 @@ var init_session = __esm({
|
|
|
1635
1640
|
return this.runTitleCommand(arg);
|
|
1636
1641
|
case "agent":
|
|
1637
1642
|
return this.runAgentCommand(arg);
|
|
1643
|
+
case "kill":
|
|
1644
|
+
return this.runKillCommand();
|
|
1638
1645
|
default: {
|
|
1639
1646
|
const err = new Error(
|
|
1640
1647
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -1746,6 +1753,17 @@ var init_session = __esm({
|
|
|
1746
1753
|
return { stopReason: "end_turn" };
|
|
1747
1754
|
});
|
|
1748
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
|
+
}
|
|
1749
1767
|
// Walk the persisted history and produce a labeled transcript suitable
|
|
1750
1768
|
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
1751
1769
|
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
@@ -2058,7 +2076,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2058
2076
|
}
|
|
2059
2077
|
async enqueuePrompt(task) {
|
|
2060
2078
|
return new Promise((resolve5, reject) => {
|
|
2061
|
-
const
|
|
2079
|
+
const run3 = async () => {
|
|
2062
2080
|
try {
|
|
2063
2081
|
const result = await task();
|
|
2064
2082
|
resolve5(result);
|
|
@@ -2066,7 +2084,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2066
2084
|
reject(err);
|
|
2067
2085
|
}
|
|
2068
2086
|
};
|
|
2069
|
-
this.promptQueue.push(
|
|
2087
|
+
this.promptQueue.push(run3);
|
|
2070
2088
|
void this.drainQueue();
|
|
2071
2089
|
});
|
|
2072
2090
|
}
|
|
@@ -3410,8 +3428,15 @@ async function pickSession(term, opts) {
|
|
|
3410
3428
|
let total = 1 + visible.length;
|
|
3411
3429
|
let selectedIdx = 0;
|
|
3412
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
|
+
}
|
|
3413
3437
|
let searchActive = false;
|
|
3414
3438
|
let searchTerm = "";
|
|
3439
|
+
let cwdOnly = false;
|
|
3415
3440
|
let mode = "normal";
|
|
3416
3441
|
let pendingAction = null;
|
|
3417
3442
|
let transientStatus = null;
|
|
@@ -3440,10 +3465,14 @@ async function pickSession(term, opts) {
|
|
|
3440
3465
|
computeLayout();
|
|
3441
3466
|
};
|
|
3442
3467
|
const applyFilter = () => {
|
|
3468
|
+
let base = allSessions;
|
|
3469
|
+
if (cwdOnly) {
|
|
3470
|
+
base = base.filter((s) => s.cwd === opts.cwd);
|
|
3471
|
+
}
|
|
3443
3472
|
if (searchActive && searchTerm.length > 0) {
|
|
3444
|
-
visible =
|
|
3473
|
+
visible = base.filter((s) => matchesSearch(s, searchTerm));
|
|
3445
3474
|
} else {
|
|
3446
|
-
visible =
|
|
3475
|
+
visible = base;
|
|
3447
3476
|
}
|
|
3448
3477
|
rebuildRows();
|
|
3449
3478
|
if (searchActive) {
|
|
@@ -3488,16 +3517,19 @@ async function pickSession(term, opts) {
|
|
|
3488
3517
|
const formatIndicator = () => {
|
|
3489
3518
|
const above = scrollOffset;
|
|
3490
3519
|
const below = Math.max(0, visible.length - scrollOffset - viewportSize);
|
|
3491
|
-
if (above === 0 && below === 0) {
|
|
3492
|
-
return "";
|
|
3493
|
-
}
|
|
3494
3520
|
const parts = [];
|
|
3521
|
+
if (cwdOnly) {
|
|
3522
|
+
parts.push("cwd-only");
|
|
3523
|
+
}
|
|
3495
3524
|
if (above > 0) {
|
|
3496
3525
|
parts.push(`\u2191 ${above} above`);
|
|
3497
3526
|
}
|
|
3498
3527
|
if (below > 0) {
|
|
3499
3528
|
parts.push(`\u2193 ${below} below`);
|
|
3500
3529
|
}
|
|
3530
|
+
if (parts.length === 0) {
|
|
3531
|
+
return "";
|
|
3532
|
+
}
|
|
3501
3533
|
return ` ${parts.join(" \xB7 ")}`;
|
|
3502
3534
|
};
|
|
3503
3535
|
const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
|
|
@@ -3721,6 +3753,38 @@ async function pickSession(term, opts) {
|
|
|
3721
3753
|
renderFromScratch();
|
|
3722
3754
|
return;
|
|
3723
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
|
+
}
|
|
3724
3788
|
if (name === "r" || name === "R") {
|
|
3725
3789
|
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
3726
3790
|
void refresh(currentId);
|
|
@@ -3859,6 +3923,90 @@ var init_picker = __esm({
|
|
|
3859
3923
|
}
|
|
3860
3924
|
});
|
|
3861
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
|
+
|
|
3862
4010
|
// src/tui/screen.ts
|
|
3863
4011
|
import stringWidth from "string-width";
|
|
3864
4012
|
import wrapAnsi from "wrap-ansi";
|
|
@@ -3867,7 +4015,8 @@ function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null
|
|
|
3867
4015
|
if (!line) {
|
|
3868
4016
|
return `${zone}|${width}|empty|${highlight2 ?? ""}|${active}`;
|
|
3869
4017
|
}
|
|
3870
|
-
|
|
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}`;
|
|
3871
4020
|
}
|
|
3872
4021
|
function computePromptVisualRows(buffer, room) {
|
|
3873
4022
|
const rows = [];
|
|
@@ -4115,6 +4264,14 @@ function* segmentForWidth(text) {
|
|
|
4115
4264
|
i = runEnd;
|
|
4116
4265
|
}
|
|
4117
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
|
+
}
|
|
4118
4275
|
function wrap(text, width, opts = {}) {
|
|
4119
4276
|
if (width <= 0) {
|
|
4120
4277
|
return [text];
|
|
@@ -4364,6 +4521,8 @@ function mapKeyName(name) {
|
|
|
4364
4521
|
return "ctrl-s";
|
|
4365
4522
|
case "CTRL_U":
|
|
4366
4523
|
return "ctrl-u";
|
|
4524
|
+
case "CTRL_V":
|
|
4525
|
+
return "ctrl-v";
|
|
4367
4526
|
case "CTRL_W":
|
|
4368
4527
|
return "ctrl-w";
|
|
4369
4528
|
case "CTRL_Y":
|
|
@@ -4374,13 +4533,14 @@ function mapKeyName(name) {
|
|
|
4374
4533
|
return null;
|
|
4375
4534
|
}
|
|
4376
4535
|
}
|
|
4377
|
-
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;
|
|
4378
4537
|
var init_screen = __esm({
|
|
4379
4538
|
"src/tui/screen.ts"() {
|
|
4380
4539
|
"use strict";
|
|
4381
4540
|
init_agent_display();
|
|
4382
4541
|
init_paths();
|
|
4383
4542
|
init_session();
|
|
4543
|
+
init_attachments();
|
|
4384
4544
|
HEADER_ROWS = 2;
|
|
4385
4545
|
BANNER_ROWS = 1;
|
|
4386
4546
|
SEPARATOR_ROWS = 1;
|
|
@@ -4388,6 +4548,7 @@ var init_screen = __esm({
|
|
|
4388
4548
|
MAX_QUEUED_ROWS = 5;
|
|
4389
4549
|
MAX_PERMISSION_ROWS = 12;
|
|
4390
4550
|
MAX_COMPLETION_ROWS = 6;
|
|
4551
|
+
MAX_CHIP_ROWS = 4;
|
|
4391
4552
|
CONFIRM_PROMPT_ROWS = 2;
|
|
4392
4553
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
4393
4554
|
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
@@ -4407,6 +4568,12 @@ var init_screen = __esm({
|
|
|
4407
4568
|
lastPromptRows = 0;
|
|
4408
4569
|
queuedTexts = [];
|
|
4409
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 = [];
|
|
4410
4577
|
repaintPaused = 0;
|
|
4411
4578
|
repaintPending = false;
|
|
4412
4579
|
lastRepaintAt = 0;
|
|
@@ -4469,7 +4636,7 @@ var init_screen = __esm({
|
|
|
4469
4636
|
banner = {
|
|
4470
4637
|
status: "ready",
|
|
4471
4638
|
planMode: false,
|
|
4472
|
-
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",
|
|
4473
4640
|
queued: 0
|
|
4474
4641
|
};
|
|
4475
4642
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -4600,7 +4767,12 @@ var init_screen = __esm({
|
|
|
4600
4767
|
this.pasteActive = false;
|
|
4601
4768
|
const pasted = Buffer.from(this.pasteBuffer, "binary").toString("utf-8").replace(/\r\n?/g, "\n");
|
|
4602
4769
|
this.pasteBuffer = "";
|
|
4603
|
-
|
|
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
|
+
}
|
|
4604
4776
|
continue;
|
|
4605
4777
|
}
|
|
4606
4778
|
const startIdx = text.indexOf(startMarker);
|
|
@@ -5351,7 +5523,7 @@ var init_screen = __esm({
|
|
|
5351
5523
|
}
|
|
5352
5524
|
scrollbackVisibleRows() {
|
|
5353
5525
|
const top = HEADER_ROWS + SEPARATOR_ROWS;
|
|
5354
|
-
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();
|
|
5355
5527
|
return Math.max(0, bottom - top + 1);
|
|
5356
5528
|
}
|
|
5357
5529
|
maxScrollOffset() {
|
|
@@ -5433,6 +5605,7 @@ var init_screen = __esm({
|
|
|
5433
5605
|
this.drawScrollback();
|
|
5434
5606
|
this.drawCompletionZone();
|
|
5435
5607
|
this.drawQueuedZone();
|
|
5608
|
+
this.drawAttachmentChipZone();
|
|
5436
5609
|
const promptRows = this.promptRows();
|
|
5437
5610
|
const separatorRow = h - promptRows - BANNER_ROWS;
|
|
5438
5611
|
this.drawSeparator(separatorRow);
|
|
@@ -5524,6 +5697,16 @@ var init_screen = __esm({
|
|
|
5524
5697
|
queuedRows() {
|
|
5525
5698
|
return Math.min(MAX_QUEUED_ROWS, this.queuedTexts.length);
|
|
5526
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
|
+
}
|
|
5527
5710
|
completionRows() {
|
|
5528
5711
|
if (this.permissionPrompt) {
|
|
5529
5712
|
return 0;
|
|
@@ -5539,7 +5722,8 @@ var init_screen = __esm({
|
|
|
5539
5722
|
const promptRows = this.promptRows();
|
|
5540
5723
|
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5541
5724
|
const queuedRows = this.queuedRows();
|
|
5542
|
-
const
|
|
5725
|
+
const chipRows = this.chipRows();
|
|
5726
|
+
const completionBottom = separatorRow - 1 - queuedRows - chipRows;
|
|
5543
5727
|
const completionTop = completionBottom - rows + 1;
|
|
5544
5728
|
let nameWidth = 0;
|
|
5545
5729
|
for (const item of this.completions.slice(0, rows)) {
|
|
@@ -5572,6 +5756,58 @@ var init_screen = __esm({
|
|
|
5572
5756
|
});
|
|
5573
5757
|
}
|
|
5574
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
|
+
}
|
|
5575
5811
|
drawQueuedZone() {
|
|
5576
5812
|
const rows = this.queuedRows();
|
|
5577
5813
|
if (rows === 0) {
|
|
@@ -5580,7 +5816,8 @@ var init_screen = __esm({
|
|
|
5580
5816
|
const w = this.term.width;
|
|
5581
5817
|
const promptRows = this.promptRows();
|
|
5582
5818
|
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5583
|
-
const
|
|
5819
|
+
const chipRows = this.chipRows();
|
|
5820
|
+
const queuedBottom = separatorRow - 1 - chipRows;
|
|
5584
5821
|
const queuedTop = queuedBottom - rows + 1;
|
|
5585
5822
|
const editingIndex = this.dispatcher.state().queueIndex;
|
|
5586
5823
|
for (let i = 0; i < rows; i++) {
|
|
@@ -5729,6 +5966,8 @@ var init_screen = __esm({
|
|
|
5729
5966
|
}
|
|
5730
5967
|
} else if (this.banner.status === "disconnected") {
|
|
5731
5968
|
this.term.brightRed(`${dot} ${this.banner.status}`);
|
|
5969
|
+
} else if (this.banner.status === "cold") {
|
|
5970
|
+
this.term.brightMagenta(`${dot} ${this.banner.status}`);
|
|
5732
5971
|
} else {
|
|
5733
5972
|
this.term.brightGreen(`${dot} ${this.banner.status}`);
|
|
5734
5973
|
}
|
|
@@ -5883,6 +6122,9 @@ var init_screen = __esm({
|
|
|
5883
6122
|
if (line.ansi) {
|
|
5884
6123
|
wrappedLine.ansi = true;
|
|
5885
6124
|
}
|
|
6125
|
+
if (i === 0 && line.iterm2Image) {
|
|
6126
|
+
wrappedLine.iterm2Image = line.iterm2Image;
|
|
6127
|
+
}
|
|
5886
6128
|
if (id !== void 0 && chunk.length > 0) {
|
|
5887
6129
|
const found = line.body.indexOf(chunk, scanPos);
|
|
5888
6130
|
const colOffset = found === -1 ? scanPos : found;
|
|
@@ -5928,6 +6170,12 @@ var init_screen = __esm({
|
|
|
5928
6170
|
if (line.ansi || line.body.includes("^")) {
|
|
5929
6171
|
this.term.styleReset();
|
|
5930
6172
|
}
|
|
6173
|
+
if (line.iterm2Image && this.isIterm2()) {
|
|
6174
|
+
this.writeIterm2Image(
|
|
6175
|
+
line.iterm2Image.data,
|
|
6176
|
+
line.iterm2Image.heightCells
|
|
6177
|
+
);
|
|
6178
|
+
}
|
|
5931
6179
|
}
|
|
5932
6180
|
};
|
|
5933
6181
|
NON_ASCII = /[^\x20-\x7e]/;
|
|
@@ -5972,6 +6220,17 @@ var init_input = __esm({
|
|
|
5972
6220
|
// here so ^Y can yank it back. Standard readline keeps a stack; we
|
|
5973
6221
|
// only keep one slot because that's what 99% of yank uses look like.
|
|
5974
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;
|
|
5975
6234
|
constructor(opts = {}) {
|
|
5976
6235
|
this.history = [...opts.history ?? []];
|
|
5977
6236
|
this.planMode = opts.planMode ?? false;
|
|
@@ -5984,9 +6243,22 @@ var init_input = __esm({
|
|
|
5984
6243
|
planMode: this.planMode,
|
|
5985
6244
|
historyIndex: this.historyIndex,
|
|
5986
6245
|
queueIndex: this.queueIndex,
|
|
6246
|
+
attachments: [...this.attachments],
|
|
5987
6247
|
historySearchQuery: this.historySearch?.query ?? null
|
|
5988
6248
|
};
|
|
5989
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
|
+
}
|
|
5990
6262
|
setTurnRunning(running) {
|
|
5991
6263
|
this.turnRunning = running;
|
|
5992
6264
|
}
|
|
@@ -6017,13 +6289,17 @@ var init_input = __esm({
|
|
|
6017
6289
|
}
|
|
6018
6290
|
// Public seed for the buffer (used for Escape pre-fill). Treated like a
|
|
6019
6291
|
// fresh draft: nav state and any saved draft are cleared, cursor lands
|
|
6020
|
-
// at the end so the user can edit immediately.
|
|
6021
|
-
|
|
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 = []) {
|
|
6022
6296
|
this.loadEntry(text);
|
|
6023
6297
|
this.historyIndex = -1;
|
|
6024
6298
|
this.queueIndex = -1;
|
|
6025
6299
|
this.savedDraft = null;
|
|
6300
|
+
this.savedAttachments = null;
|
|
6026
6301
|
this.historySearch = null;
|
|
6302
|
+
this.attachments = [...attachments];
|
|
6027
6303
|
}
|
|
6028
6304
|
feed(event) {
|
|
6029
6305
|
if (this.historySearch !== null) {
|
|
@@ -6069,6 +6345,9 @@ var init_input = __esm({
|
|
|
6069
6345
|
this.insertText(event.text);
|
|
6070
6346
|
return [];
|
|
6071
6347
|
}
|
|
6348
|
+
if (event.type === "attachment-paths") {
|
|
6349
|
+
return [];
|
|
6350
|
+
}
|
|
6072
6351
|
return this.handleKey(event.name);
|
|
6073
6352
|
}
|
|
6074
6353
|
handleKey(name) {
|
|
@@ -6145,6 +6424,8 @@ var init_input = __esm({
|
|
|
6145
6424
|
case "ctrl-u":
|
|
6146
6425
|
this.killLine();
|
|
6147
6426
|
return [];
|
|
6427
|
+
case "ctrl-v":
|
|
6428
|
+
return [{ type: "attachment-request", source: "clipboard" }];
|
|
6148
6429
|
case "ctrl-w":
|
|
6149
6430
|
this.killWord();
|
|
6150
6431
|
return [];
|
|
@@ -6177,7 +6458,9 @@ var init_input = __esm({
|
|
|
6177
6458
|
this.historyIndex = -1;
|
|
6178
6459
|
this.queueIndex = -1;
|
|
6179
6460
|
this.savedDraft = null;
|
|
6461
|
+
this.savedAttachments = null;
|
|
6180
6462
|
this.historySearch = null;
|
|
6463
|
+
this.attachments = [];
|
|
6181
6464
|
}
|
|
6182
6465
|
insertChar(ch) {
|
|
6183
6466
|
if (ch.length === 0) {
|
|
@@ -6329,6 +6612,8 @@ var init_input = __esm({
|
|
|
6329
6612
|
row: this.row,
|
|
6330
6613
|
col: this.col
|
|
6331
6614
|
};
|
|
6615
|
+
this.savedAttachments = [...this.attachments];
|
|
6616
|
+
this.attachments = [];
|
|
6332
6617
|
if (this.queue.length > 0) {
|
|
6333
6618
|
this.queueIndex = this.queue.length - 1;
|
|
6334
6619
|
this.loadEntry(this.queue[this.queueIndex] ?? "");
|
|
@@ -6401,6 +6686,8 @@ var init_input = __esm({
|
|
|
6401
6686
|
this.row = this.savedDraft.row;
|
|
6402
6687
|
this.col = this.savedDraft.col;
|
|
6403
6688
|
this.savedDraft = null;
|
|
6689
|
+
this.attachments = this.savedAttachments ?? [];
|
|
6690
|
+
this.savedAttachments = null;
|
|
6404
6691
|
} else {
|
|
6405
6692
|
this.clearBuffer();
|
|
6406
6693
|
}
|
|
@@ -6554,18 +6841,20 @@ var init_input = __esm({
|
|
|
6554
6841
|
const text = this.bufferText();
|
|
6555
6842
|
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
6556
6843
|
const index = this.queueIndex;
|
|
6844
|
+
const attachments2 = [...this.attachments];
|
|
6557
6845
|
this.clearBuffer();
|
|
6558
6846
|
if (text.trim().length === 0) {
|
|
6559
6847
|
return [{ type: "queue-remove", index }];
|
|
6560
6848
|
}
|
|
6561
|
-
return [{ type: "queue-edit", index, text }];
|
|
6849
|
+
return [{ type: "queue-edit", index, text, attachments: attachments2 }];
|
|
6562
6850
|
}
|
|
6563
|
-
if (text.trim().length === 0) {
|
|
6851
|
+
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
6564
6852
|
return [];
|
|
6565
6853
|
}
|
|
6566
6854
|
const planMode = this.planMode;
|
|
6855
|
+
const attachments = [...this.attachments];
|
|
6567
6856
|
this.clearBuffer();
|
|
6568
|
-
return [{ type: "send", text, planMode }];
|
|
6857
|
+
return [{ type: "send", text, planMode, attachments }];
|
|
6569
6858
|
}
|
|
6570
6859
|
// Home: jump to the very start of the prompt buffer. If we're already
|
|
6571
6860
|
// there, fall through to scrolling the scrollback to its top.
|
|
@@ -6590,13 +6879,15 @@ var init_input = __esm({
|
|
|
6590
6879
|
return [{ type: "scroll-to-bottom" }];
|
|
6591
6880
|
}
|
|
6592
6881
|
handleCtrlC() {
|
|
6593
|
-
if (!this.bufferIsEmpty()) {
|
|
6882
|
+
if (!this.bufferIsEmpty() || this.attachments.length > 0) {
|
|
6594
6883
|
this.buffer = [""];
|
|
6595
6884
|
this.row = 0;
|
|
6596
6885
|
this.col = 0;
|
|
6886
|
+
this.attachments = [];
|
|
6597
6887
|
if (this.queueIndex === -1) {
|
|
6598
6888
|
this.historyIndex = -1;
|
|
6599
6889
|
this.savedDraft = null;
|
|
6890
|
+
this.savedAttachments = null;
|
|
6600
6891
|
}
|
|
6601
6892
|
return [];
|
|
6602
6893
|
}
|
|
@@ -6614,6 +6905,232 @@ var init_input = __esm({
|
|
|
6614
6905
|
}
|
|
6615
6906
|
});
|
|
6616
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
|
+
|
|
6617
7134
|
// src/tui/completion.ts
|
|
6618
7135
|
function longestCommonPrefix(names) {
|
|
6619
7136
|
if (names.length === 0) {
|
|
@@ -6948,8 +7465,29 @@ import chalk from "chalk";
|
|
|
6948
7465
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
6949
7466
|
function formatEvent(event) {
|
|
6950
7467
|
switch (event.kind) {
|
|
6951
|
-
case "user-text":
|
|
6952
|
-
|
|
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
|
+
}
|
|
6953
7491
|
case "agent-text":
|
|
6954
7492
|
return formatBlock(event.text, " ", "agent");
|
|
6955
7493
|
case "agent-thought":
|
|
@@ -7279,6 +7817,8 @@ var init_format = __esm({
|
|
|
7279
7817
|
import { appendFileSync, statSync, renameSync } from "fs";
|
|
7280
7818
|
import { nanoid as nanoid3 } from "nanoid";
|
|
7281
7819
|
import termkit from "terminal-kit";
|
|
7820
|
+
import fs15 from "fs/promises";
|
|
7821
|
+
import path11 from "path";
|
|
7282
7822
|
async function runTuiApp(opts) {
|
|
7283
7823
|
const config = await ensureConfig();
|
|
7284
7824
|
logMaxBytes = config.tui.logMaxBytes;
|
|
@@ -7396,6 +7936,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7396
7936
|
appendRender(event);
|
|
7397
7937
|
maybeDismissPermissionByToolUpdate(update);
|
|
7398
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
|
+
});
|
|
7399
7948
|
const handlePermissionResolved = (update) => {
|
|
7400
7949
|
const u = update ?? {};
|
|
7401
7950
|
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
|
|
@@ -7499,6 +8048,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7499
8048
|
});
|
|
7500
8049
|
let upstreamSessionId;
|
|
7501
8050
|
let agentInfoName;
|
|
8051
|
+
let agentAcceptsImages = true;
|
|
7502
8052
|
try {
|
|
7503
8053
|
const initResult = await conn.request("initialize", {
|
|
7504
8054
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
@@ -7509,6 +8059,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7509
8059
|
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
7510
8060
|
});
|
|
7511
8061
|
agentInfoName = initResult?.agentInfo?.name;
|
|
8062
|
+
const imageCap = initResult?.agentCapabilities?.promptCapabilities?.image;
|
|
8063
|
+
if (imageCap === false) {
|
|
8064
|
+
agentAcceptsImages = false;
|
|
8065
|
+
}
|
|
7512
8066
|
} catch {
|
|
7513
8067
|
}
|
|
7514
8068
|
let resolvedSessionId = ctx.sessionId;
|
|
@@ -7607,6 +8161,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7607
8161
|
if (tryHandleCompletionKey(ev)) {
|
|
7608
8162
|
continue;
|
|
7609
8163
|
}
|
|
8164
|
+
if (ev.type === "attachment-paths") {
|
|
8165
|
+
void handleAttachmentPaths(ev.paths);
|
|
8166
|
+
continue;
|
|
8167
|
+
}
|
|
7610
8168
|
const effects = dispatcher.feed(ev);
|
|
7611
8169
|
for (const effect of effects) {
|
|
7612
8170
|
handleEffect(effect);
|
|
@@ -7616,6 +8174,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7616
8174
|
screen.setBannerSearchIndicator(
|
|
7617
8175
|
dispatcher.state().historySearchQuery
|
|
7618
8176
|
);
|
|
8177
|
+
screen.setAttachments(dispatcher.state().attachments);
|
|
7619
8178
|
screen.refreshPrompt();
|
|
7620
8179
|
}
|
|
7621
8180
|
});
|
|
@@ -7906,7 +8465,8 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7906
8465
|
const choice = await pickSession(term, {
|
|
7907
8466
|
cwd: resolvedCwd,
|
|
7908
8467
|
sessions,
|
|
7909
|
-
config
|
|
8468
|
+
config,
|
|
8469
|
+
currentSessionId: resolvedSessionId
|
|
7910
8470
|
});
|
|
7911
8471
|
if (choice.kind === "abort") {
|
|
7912
8472
|
screen.start();
|
|
@@ -7937,13 +8497,17 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7937
8497
|
const handleEffect = (effect) => {
|
|
7938
8498
|
switch (effect.type) {
|
|
7939
8499
|
case "send":
|
|
7940
|
-
enqueuePrompt(effect.text, effect.planMode);
|
|
8500
|
+
enqueuePrompt(effect.text, effect.planMode, effect.attachments);
|
|
7941
8501
|
return;
|
|
7942
8502
|
case "queue-edit": {
|
|
7943
8503
|
const realIdx = effect.index + queueHeadOffset();
|
|
7944
8504
|
const existing = promptQueue[realIdx];
|
|
7945
8505
|
if (existing) {
|
|
7946
|
-
promptQueue[realIdx] = {
|
|
8506
|
+
promptQueue[realIdx] = {
|
|
8507
|
+
text: effect.text,
|
|
8508
|
+
planMode: existing.planMode,
|
|
8509
|
+
attachments: effect.attachments
|
|
8510
|
+
};
|
|
7947
8511
|
refreshQueueDisplay();
|
|
7948
8512
|
}
|
|
7949
8513
|
return;
|
|
@@ -7962,7 +8526,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7962
8526
|
const waitingEmpty = promptQueue.length <= headOffset;
|
|
7963
8527
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
7964
8528
|
if (waitingEmpty && bufferEmpty) {
|
|
7965
|
-
pendingPrefill =
|
|
8529
|
+
pendingPrefill = {
|
|
8530
|
+
text: turnInFlight.text,
|
|
8531
|
+
attachments: turnInFlight.attachments
|
|
8532
|
+
};
|
|
7966
8533
|
}
|
|
7967
8534
|
}
|
|
7968
8535
|
if (turnInFlight) {
|
|
@@ -8001,17 +8568,81 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8001
8568
|
screen.enterScrollbackSearch();
|
|
8002
8569
|
screen.updateScrollbackSearchTerm(effect.query);
|
|
8003
8570
|
return;
|
|
8571
|
+
case "attachment-request":
|
|
8572
|
+
void handleClipboardAttachment();
|
|
8573
|
+
return;
|
|
8004
8574
|
}
|
|
8005
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;
|
|
8627
|
+
}
|
|
8628
|
+
const effects = dispatcher.feed({ type: "paste", text: result.text });
|
|
8629
|
+
for (const effect of effects) {
|
|
8630
|
+
handleEffect(effect);
|
|
8631
|
+
}
|
|
8632
|
+
screen.refreshPrompt();
|
|
8633
|
+
};
|
|
8006
8634
|
const promptQueue = [];
|
|
8007
8635
|
let workerActive = false;
|
|
8008
8636
|
const refreshQueueDisplay = () => {
|
|
8009
8637
|
const waiting = promptQueue.slice(workerActive ? 1 : 0);
|
|
8010
|
-
|
|
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);
|
|
8011
8642
|
screen.setBanner({ queued: waiting.length });
|
|
8012
8643
|
dispatcher.setQueue(waiting.map((p) => p.text));
|
|
8013
8644
|
};
|
|
8014
|
-
const enqueuePrompt = (text, planMode) => {
|
|
8645
|
+
const enqueuePrompt = (text, planMode, attachments) => {
|
|
8015
8646
|
screen.scrollToBottom();
|
|
8016
8647
|
if (handleBuiltinCommand(text)) {
|
|
8017
8648
|
return;
|
|
@@ -8019,7 +8650,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8019
8650
|
history = appendEntry(history, text);
|
|
8020
8651
|
dispatcher.setHistory(history);
|
|
8021
8652
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
8022
|
-
promptQueue.push({ text, planMode });
|
|
8653
|
+
promptQueue.push({ text, planMode, attachments });
|
|
8023
8654
|
refreshQueueDisplay();
|
|
8024
8655
|
tickWorker();
|
|
8025
8656
|
};
|
|
@@ -8186,31 +8817,38 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8186
8817
|
break;
|
|
8187
8818
|
}
|
|
8188
8819
|
refreshQueueDisplay();
|
|
8189
|
-
await processPrompt(next.text, next.planMode);
|
|
8820
|
+
await processPrompt(next.text, next.planMode, next.attachments);
|
|
8190
8821
|
promptQueue.shift();
|
|
8191
8822
|
}
|
|
8192
8823
|
} finally {
|
|
8193
8824
|
workerActive = false;
|
|
8194
8825
|
refreshQueueDisplay();
|
|
8195
8826
|
if (pendingPrefill !== null) {
|
|
8196
|
-
const text = pendingPrefill;
|
|
8827
|
+
const { text, attachments } = pendingPrefill;
|
|
8197
8828
|
pendingPrefill = null;
|
|
8198
8829
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
8199
8830
|
if (bufferEmpty) {
|
|
8200
|
-
dispatcher.setBuffer(text);
|
|
8831
|
+
dispatcher.setBuffer(text, attachments);
|
|
8201
8832
|
screen.refreshPrompt();
|
|
8202
8833
|
}
|
|
8203
8834
|
}
|
|
8204
8835
|
}
|
|
8205
8836
|
};
|
|
8206
|
-
const processPrompt = async (text, planMode) => {
|
|
8207
|
-
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
|
+
}
|
|
8208
8845
|
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
8209
8846
|
adjustPendingTurns(1);
|
|
8210
|
-
appendRender({ kind: "user-text", text });
|
|
8847
|
+
appendRender({ kind: "user-text", text, attachments });
|
|
8211
8848
|
let cancelled = false;
|
|
8212
8849
|
turnInFlight = {
|
|
8213
8850
|
text,
|
|
8851
|
+
attachments,
|
|
8214
8852
|
cancel: () => {
|
|
8215
8853
|
if (cancelled) {
|
|
8216
8854
|
return;
|
|
@@ -8707,6 +9345,8 @@ var init_app = __esm({
|
|
|
8707
9345
|
init_picker();
|
|
8708
9346
|
init_screen();
|
|
8709
9347
|
init_input();
|
|
9348
|
+
init_attachments();
|
|
9349
|
+
init_clipboard();
|
|
8710
9350
|
init_completion();
|
|
8711
9351
|
init_render_update();
|
|
8712
9352
|
init_format();
|
package/dist/index.d.ts
CHANGED
|
@@ -1777,6 +1777,7 @@ declare class Session {
|
|
|
1777
1777
|
private runTitleRegen;
|
|
1778
1778
|
private runInternalPrompt;
|
|
1779
1779
|
private runAgentCommand;
|
|
1780
|
+
private runKillCommand;
|
|
1780
1781
|
private buildSwitchTranscript;
|
|
1781
1782
|
seedFromImport(): Promise<void>;
|
|
1782
1783
|
private broadcastAgentSwitch;
|
package/dist/index.js
CHANGED
|
@@ -1448,6 +1448,11 @@ var HYDRA_COMMANDS = [
|
|
|
1448
1448
|
name: "hydra agent",
|
|
1449
1449
|
argsHint: "<agent>",
|
|
1450
1450
|
description: "Swap the agent backing this session, preserving context"
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
verb: "kill",
|
|
1454
|
+
name: "hydra kill",
|
|
1455
|
+
description: "Close this session (kills the agent; record is kept so it can be resumed later)"
|
|
1451
1456
|
}
|
|
1452
1457
|
];
|
|
1453
1458
|
var VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
@@ -2152,6 +2157,8 @@ var Session = class {
|
|
|
2152
2157
|
return this.runTitleCommand(arg);
|
|
2153
2158
|
case "agent":
|
|
2154
2159
|
return this.runAgentCommand(arg);
|
|
2160
|
+
case "kill":
|
|
2161
|
+
return this.runKillCommand();
|
|
2155
2162
|
default: {
|
|
2156
2163
|
const err = new Error(
|
|
2157
2164
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -2263,6 +2270,17 @@ var Session = class {
|
|
|
2263
2270
|
return { stopReason: "end_turn" };
|
|
2264
2271
|
});
|
|
2265
2272
|
}
|
|
2273
|
+
// Close this session in-place. Bypasses enqueuePrompt deliberately so a
|
|
2274
|
+
// mid-turn /hydra kill takes effect immediately — agent.kill() will tear
|
|
2275
|
+
// down any in-flight request as a side effect. The record is kept
|
|
2276
|
+
// (deleteRecord:false) so the session goes cold and can be resurrected.
|
|
2277
|
+
// Returns end_turn so the prompt() caller's response resolves normally,
|
|
2278
|
+
// but every attached client has already received hydra-acp/session_closed
|
|
2279
|
+
// by the time this returns.
|
|
2280
|
+
async runKillCommand() {
|
|
2281
|
+
await this.close({ deleteRecord: false });
|
|
2282
|
+
return { stopReason: "end_turn" };
|
|
2283
|
+
}
|
|
2266
2284
|
// Walk the persisted history and produce a labeled transcript suitable
|
|
2267
2285
|
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
2268
2286
|
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|