@hydra-acp/cli 0.1.12 → 0.1.14
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 +732 -45
- 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);
|
|
@@ -3823,7 +3887,7 @@ function readTermWidth(term) {
|
|
|
3823
3887
|
return term.width ?? 80;
|
|
3824
3888
|
}
|
|
3825
3889
|
function formatNewSessionLabel(cwd, maxWidth) {
|
|
3826
|
-
const prefix = "
|
|
3890
|
+
const prefix = "New session in ";
|
|
3827
3891
|
const budget = Math.max(1, maxWidth - prefix.length);
|
|
3828
3892
|
return prefix + truncateMiddle(shortenHomePath(cwd), budget);
|
|
3829
3893
|
}
|
|
@@ -3859,6 +3923,115 @@ 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 parseDataUriImage(uri) {
|
|
3935
|
+
const match = uri.match(/^data:(image\/[a-z0-9.+\-]+);base64,([A-Za-z0-9+/=]+)$/);
|
|
3936
|
+
if (!match) {
|
|
3937
|
+
return null;
|
|
3938
|
+
}
|
|
3939
|
+
const mimeType = match[1].toLowerCase();
|
|
3940
|
+
if (!SUPPORTED_MIMES.has(mimeType)) {
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3943
|
+
const data = match[2];
|
|
3944
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
3945
|
+
const sizeBytes = Math.floor(data.length * 3 / 4) - padding;
|
|
3946
|
+
return { mimeType, data, sizeBytes };
|
|
3947
|
+
}
|
|
3948
|
+
function isSupportedDataUriImage(uri) {
|
|
3949
|
+
return parseDataUriImage(uri) !== null;
|
|
3950
|
+
}
|
|
3951
|
+
function formatSize(bytes) {
|
|
3952
|
+
if (bytes >= 1024 * 1024) {
|
|
3953
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3954
|
+
}
|
|
3955
|
+
if (bytes >= 1024) {
|
|
3956
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
3957
|
+
}
|
|
3958
|
+
return `${bytes}B`;
|
|
3959
|
+
}
|
|
3960
|
+
function parseImageDropPaste(raw) {
|
|
3961
|
+
const text = raw.trim();
|
|
3962
|
+
if (text.length === 0) {
|
|
3963
|
+
return null;
|
|
3964
|
+
}
|
|
3965
|
+
const tokens = [];
|
|
3966
|
+
let i = 0;
|
|
3967
|
+
while (i < text.length) {
|
|
3968
|
+
while (i < text.length && /\s/.test(text[i] ?? "")) {
|
|
3969
|
+
i++;
|
|
3970
|
+
}
|
|
3971
|
+
if (i >= text.length) {
|
|
3972
|
+
break;
|
|
3973
|
+
}
|
|
3974
|
+
const ch = text[i];
|
|
3975
|
+
let token = "";
|
|
3976
|
+
if (ch === "'" || ch === '"') {
|
|
3977
|
+
const quote = ch;
|
|
3978
|
+
i++;
|
|
3979
|
+
while (i < text.length && text[i] !== quote) {
|
|
3980
|
+
token += text[i];
|
|
3981
|
+
i++;
|
|
3982
|
+
}
|
|
3983
|
+
if (i >= text.length) {
|
|
3984
|
+
return null;
|
|
3985
|
+
}
|
|
3986
|
+
i++;
|
|
3987
|
+
} else {
|
|
3988
|
+
while (i < text.length && !/\s/.test(text[i] ?? "")) {
|
|
3989
|
+
if (text[i] === "\\" && i + 1 < text.length) {
|
|
3990
|
+
token += text[i + 1];
|
|
3991
|
+
i += 2;
|
|
3992
|
+
} else {
|
|
3993
|
+
token += text[i];
|
|
3994
|
+
i++;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
if (token.startsWith("data:")) {
|
|
3999
|
+
if (!isSupportedDataUriImage(token)) {
|
|
4000
|
+
return null;
|
|
4001
|
+
}
|
|
4002
|
+
tokens.push(token);
|
|
4003
|
+
continue;
|
|
4004
|
+
}
|
|
4005
|
+
let normalized = token;
|
|
4006
|
+
if (normalized.startsWith("file://")) {
|
|
4007
|
+
normalized = decodeURI(normalized.slice("file://".length));
|
|
4008
|
+
}
|
|
4009
|
+
if (!normalized.startsWith("/")) {
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
4012
|
+
if (!isSupportedImagePath(normalized)) {
|
|
4013
|
+
return null;
|
|
4014
|
+
}
|
|
4015
|
+
tokens.push(normalized);
|
|
4016
|
+
}
|
|
4017
|
+
return tokens.length > 0 ? tokens : null;
|
|
4018
|
+
}
|
|
4019
|
+
var MAX_ATTACHMENT_BYTES, EXTENSION_TO_MIME, SUPPORTED_MIMES;
|
|
4020
|
+
var init_attachments = __esm({
|
|
4021
|
+
"src/tui/attachments.ts"() {
|
|
4022
|
+
"use strict";
|
|
4023
|
+
MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
4024
|
+
EXTENSION_TO_MIME = {
|
|
4025
|
+
".png": "image/png",
|
|
4026
|
+
".jpg": "image/jpeg",
|
|
4027
|
+
".jpeg": "image/jpeg",
|
|
4028
|
+
".gif": "image/gif",
|
|
4029
|
+
".webp": "image/webp"
|
|
4030
|
+
};
|
|
4031
|
+
SUPPORTED_MIMES = new Set(Object.values(EXTENSION_TO_MIME));
|
|
4032
|
+
}
|
|
4033
|
+
});
|
|
4034
|
+
|
|
3862
4035
|
// src/tui/screen.ts
|
|
3863
4036
|
import stringWidth from "string-width";
|
|
3864
4037
|
import wrapAnsi from "wrap-ansi";
|
|
@@ -3867,7 +4040,8 @@ function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null
|
|
|
3867
4040
|
if (!line) {
|
|
3868
4041
|
return `${zone}|${width}|empty|${highlight2 ?? ""}|${active}`;
|
|
3869
4042
|
}
|
|
3870
|
-
|
|
4043
|
+
const img = line.iterm2Image ? `i${line.iterm2Image.heightCells}:${line.iterm2Image.data.length}` : "";
|
|
4044
|
+
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}|${highlight2 ?? ""}|${active}|${img}`;
|
|
3871
4045
|
}
|
|
3872
4046
|
function computePromptVisualRows(buffer, room) {
|
|
3873
4047
|
const rows = [];
|
|
@@ -4115,6 +4289,14 @@ function* segmentForWidth(text) {
|
|
|
4115
4289
|
i = runEnd;
|
|
4116
4290
|
}
|
|
4117
4291
|
}
|
|
4292
|
+
function buildIterm2ImageEscape(base64, heightCells, insideTmux) {
|
|
4293
|
+
const inner = `\x1B]1337;File=inline=1;height=${heightCells};preserveAspectRatio=1:${base64}\x07`;
|
|
4294
|
+
if (!insideTmux) {
|
|
4295
|
+
return inner;
|
|
4296
|
+
}
|
|
4297
|
+
const doubled = inner.replace(/\x1b/g, "\x1B\x1B");
|
|
4298
|
+
return `\x1BPtmux;${doubled}\x1B\\`;
|
|
4299
|
+
}
|
|
4118
4300
|
function wrap(text, width, opts = {}) {
|
|
4119
4301
|
if (width <= 0) {
|
|
4120
4302
|
return [text];
|
|
@@ -4364,6 +4546,8 @@ function mapKeyName(name) {
|
|
|
4364
4546
|
return "ctrl-s";
|
|
4365
4547
|
case "CTRL_U":
|
|
4366
4548
|
return "ctrl-u";
|
|
4549
|
+
case "CTRL_V":
|
|
4550
|
+
return "ctrl-v";
|
|
4367
4551
|
case "CTRL_W":
|
|
4368
4552
|
return "ctrl-w";
|
|
4369
4553
|
case "CTRL_Y":
|
|
@@ -4374,13 +4558,14 @@ function mapKeyName(name) {
|
|
|
4374
4558
|
return null;
|
|
4375
4559
|
}
|
|
4376
4560
|
}
|
|
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;
|
|
4561
|
+
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
4562
|
var init_screen = __esm({
|
|
4379
4563
|
"src/tui/screen.ts"() {
|
|
4380
4564
|
"use strict";
|
|
4381
4565
|
init_agent_display();
|
|
4382
4566
|
init_paths();
|
|
4383
4567
|
init_session();
|
|
4568
|
+
init_attachments();
|
|
4384
4569
|
HEADER_ROWS = 2;
|
|
4385
4570
|
BANNER_ROWS = 1;
|
|
4386
4571
|
SEPARATOR_ROWS = 1;
|
|
@@ -4388,6 +4573,7 @@ var init_screen = __esm({
|
|
|
4388
4573
|
MAX_QUEUED_ROWS = 5;
|
|
4389
4574
|
MAX_PERMISSION_ROWS = 12;
|
|
4390
4575
|
MAX_COMPLETION_ROWS = 6;
|
|
4576
|
+
MAX_CHIP_ROWS = 4;
|
|
4391
4577
|
CONFIRM_PROMPT_ROWS = 2;
|
|
4392
4578
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
4393
4579
|
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
@@ -4407,6 +4593,12 @@ var init_screen = __esm({
|
|
|
4407
4593
|
lastPromptRows = 0;
|
|
4408
4594
|
queuedTexts = [];
|
|
4409
4595
|
lastQueueEditingIndex = -1;
|
|
4596
|
+
// Attachments on the current draft, pushed by the app whenever the
|
|
4597
|
+
// dispatcher mutates. The chip zone (drawAttachmentChipZone) renders
|
|
4598
|
+
// one row per attachment plus, in iTerm2-capable terminals, an inline
|
|
4599
|
+
// thumbnail. Capped at MAX_CHIP_ROWS in the visible zone — additional
|
|
4600
|
+
// chips collapse into an overflow row.
|
|
4601
|
+
attachments = [];
|
|
4410
4602
|
repaintPaused = 0;
|
|
4411
4603
|
repaintPending = false;
|
|
4412
4604
|
lastRepaintAt = 0;
|
|
@@ -4469,7 +4661,7 @@ var init_screen = __esm({
|
|
|
4469
4661
|
banner = {
|
|
4470
4662
|
status: "ready",
|
|
4471
4663
|
planMode: false,
|
|
4472
|
-
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
4664
|
+
hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
|
|
4473
4665
|
queued: 0
|
|
4474
4666
|
};
|
|
4475
4667
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
@@ -4600,7 +4792,12 @@ var init_screen = __esm({
|
|
|
4600
4792
|
this.pasteActive = false;
|
|
4601
4793
|
const pasted = Buffer.from(this.pasteBuffer, "binary").toString("utf-8").replace(/\r\n?/g, "\n");
|
|
4602
4794
|
this.pasteBuffer = "";
|
|
4603
|
-
|
|
4795
|
+
const paths2 = parseImageDropPaste(pasted);
|
|
4796
|
+
if (paths2 !== null) {
|
|
4797
|
+
this.onKey([{ type: "attachment-paths", paths: paths2 }]);
|
|
4798
|
+
} else {
|
|
4799
|
+
this.onKey([{ type: "paste", text: pasted }]);
|
|
4800
|
+
}
|
|
4604
4801
|
continue;
|
|
4605
4802
|
}
|
|
4606
4803
|
const startIdx = text.indexOf(startMarker);
|
|
@@ -5351,7 +5548,7 @@ var init_screen = __esm({
|
|
|
5351
5548
|
}
|
|
5352
5549
|
scrollbackVisibleRows() {
|
|
5353
5550
|
const top = HEADER_ROWS + SEPARATOR_ROWS;
|
|
5354
|
-
const bottom = this.term.height - this.promptRows() - BANNER_ROWS - SEPARATOR_ROWS - this.queuedRows() - this.completionRows();
|
|
5551
|
+
const bottom = this.term.height - this.promptRows() - BANNER_ROWS - SEPARATOR_ROWS - this.chipRows() - this.queuedRows() - this.completionRows();
|
|
5355
5552
|
return Math.max(0, bottom - top + 1);
|
|
5356
5553
|
}
|
|
5357
5554
|
maxScrollOffset() {
|
|
@@ -5433,6 +5630,7 @@ var init_screen = __esm({
|
|
|
5433
5630
|
this.drawScrollback();
|
|
5434
5631
|
this.drawCompletionZone();
|
|
5435
5632
|
this.drawQueuedZone();
|
|
5633
|
+
this.drawAttachmentChipZone();
|
|
5436
5634
|
const promptRows = this.promptRows();
|
|
5437
5635
|
const separatorRow = h - promptRows - BANNER_ROWS;
|
|
5438
5636
|
this.drawSeparator(separatorRow);
|
|
@@ -5524,6 +5722,16 @@ var init_screen = __esm({
|
|
|
5524
5722
|
queuedRows() {
|
|
5525
5723
|
return Math.min(MAX_QUEUED_ROWS, this.queuedTexts.length);
|
|
5526
5724
|
}
|
|
5725
|
+
chipRows() {
|
|
5726
|
+
return Math.min(MAX_CHIP_ROWS, this.attachments.length);
|
|
5727
|
+
}
|
|
5728
|
+
setAttachments(attachments) {
|
|
5729
|
+
if (this.attachments.length === attachments.length && this.attachments.every((a, i) => a === attachments[i])) {
|
|
5730
|
+
return;
|
|
5731
|
+
}
|
|
5732
|
+
this.attachments = [...attachments];
|
|
5733
|
+
this.repaint();
|
|
5734
|
+
}
|
|
5527
5735
|
completionRows() {
|
|
5528
5736
|
if (this.permissionPrompt) {
|
|
5529
5737
|
return 0;
|
|
@@ -5539,7 +5747,8 @@ var init_screen = __esm({
|
|
|
5539
5747
|
const promptRows = this.promptRows();
|
|
5540
5748
|
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5541
5749
|
const queuedRows = this.queuedRows();
|
|
5542
|
-
const
|
|
5750
|
+
const chipRows = this.chipRows();
|
|
5751
|
+
const completionBottom = separatorRow - 1 - queuedRows - chipRows;
|
|
5543
5752
|
const completionTop = completionBottom - rows + 1;
|
|
5544
5753
|
let nameWidth = 0;
|
|
5545
5754
|
for (const item of this.completions.slice(0, rows)) {
|
|
@@ -5572,6 +5781,58 @@ var init_screen = __esm({
|
|
|
5572
5781
|
});
|
|
5573
5782
|
}
|
|
5574
5783
|
}
|
|
5784
|
+
// Chip zone: one row per attached image, sitting between the queued
|
|
5785
|
+
// zone and the separator (closest to the user's draft). Each row
|
|
5786
|
+
// shows "📎 <name> · <size>" plus, in iTerm2-capable terminals, a
|
|
5787
|
+
// tiny inline thumbnail at the end. Overflow collapses into a
|
|
5788
|
+
// single "+ N more attached" row.
|
|
5789
|
+
drawAttachmentChipZone() {
|
|
5790
|
+
const rows = this.chipRows();
|
|
5791
|
+
if (rows === 0) {
|
|
5792
|
+
return;
|
|
5793
|
+
}
|
|
5794
|
+
const w = this.term.width;
|
|
5795
|
+
const promptRows = this.promptRows();
|
|
5796
|
+
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5797
|
+
const chipBottom = separatorRow - 1;
|
|
5798
|
+
const chipTop = chipBottom - rows + 1;
|
|
5799
|
+
const iterm = this.isIterm2();
|
|
5800
|
+
for (let i = 0; i < rows; i++) {
|
|
5801
|
+
const row = chipTop + i;
|
|
5802
|
+
const isLast = i === rows - 1 && this.attachments.length > MAX_CHIP_ROWS;
|
|
5803
|
+
const overflow = this.attachments.length - MAX_CHIP_ROWS;
|
|
5804
|
+
const att = this.attachments[i];
|
|
5805
|
+
const label = att ? `${att.name ?? "image"} \xB7 ${formatSize(att.sizeBytes)}` : "";
|
|
5806
|
+
const sig = isLast ? `chip|${w}|overflow|${overflow}` : att ? `chip|${w}|${iterm ? "i" : "t"}|${label}|${att.sizeBytes}` : `chip|${w}|empty`;
|
|
5807
|
+
this.paintRow(row, sig, () => {
|
|
5808
|
+
if (isLast) {
|
|
5809
|
+
this.term.dim(` \u{1F4CE} + ${overflow + 1} more attached`);
|
|
5810
|
+
return;
|
|
5811
|
+
}
|
|
5812
|
+
if (!att) {
|
|
5813
|
+
return;
|
|
5814
|
+
}
|
|
5815
|
+
this.term(" ").yellow(`\u{1F4CE} ${label}`);
|
|
5816
|
+
if (iterm) {
|
|
5817
|
+
this.term(" ");
|
|
5818
|
+
this.writeIterm2Image(att.data, 1);
|
|
5819
|
+
}
|
|
5820
|
+
});
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
isIterm2() {
|
|
5824
|
+
const env = process.env;
|
|
5825
|
+
return env.LC_TERMINAL === "iTerm2" || env.TERM_PROGRAM === "iTerm.app";
|
|
5826
|
+
}
|
|
5827
|
+
// Emits the iTerm2 OSC 1337 inline image escape at the current
|
|
5828
|
+
// cursor position. Wraps in DCS-passthrough when tmux is detected
|
|
5829
|
+
// (requires `set -g allow-passthrough on` in the user's tmux conf).
|
|
5830
|
+
// Caller is responsible for knowing iTerm2 is the active terminal.
|
|
5831
|
+
writeIterm2Image(base64, heightCells) {
|
|
5832
|
+
process.stdout.write(
|
|
5833
|
+
buildIterm2ImageEscape(base64, heightCells, Boolean(process.env.TMUX))
|
|
5834
|
+
);
|
|
5835
|
+
}
|
|
5575
5836
|
drawQueuedZone() {
|
|
5576
5837
|
const rows = this.queuedRows();
|
|
5577
5838
|
if (rows === 0) {
|
|
@@ -5580,7 +5841,8 @@ var init_screen = __esm({
|
|
|
5580
5841
|
const w = this.term.width;
|
|
5581
5842
|
const promptRows = this.promptRows();
|
|
5582
5843
|
const separatorRow = this.term.height - promptRows - BANNER_ROWS;
|
|
5583
|
-
const
|
|
5844
|
+
const chipRows = this.chipRows();
|
|
5845
|
+
const queuedBottom = separatorRow - 1 - chipRows;
|
|
5584
5846
|
const queuedTop = queuedBottom - rows + 1;
|
|
5585
5847
|
const editingIndex = this.dispatcher.state().queueIndex;
|
|
5586
5848
|
for (let i = 0; i < rows; i++) {
|
|
@@ -5729,6 +5991,8 @@ var init_screen = __esm({
|
|
|
5729
5991
|
}
|
|
5730
5992
|
} else if (this.banner.status === "disconnected") {
|
|
5731
5993
|
this.term.brightRed(`${dot} ${this.banner.status}`);
|
|
5994
|
+
} else if (this.banner.status === "cold") {
|
|
5995
|
+
this.term.brightMagenta(`${dot} ${this.banner.status}`);
|
|
5732
5996
|
} else {
|
|
5733
5997
|
this.term.brightGreen(`${dot} ${this.banner.status}`);
|
|
5734
5998
|
}
|
|
@@ -5883,6 +6147,9 @@ var init_screen = __esm({
|
|
|
5883
6147
|
if (line.ansi) {
|
|
5884
6148
|
wrappedLine.ansi = true;
|
|
5885
6149
|
}
|
|
6150
|
+
if (i === 0 && line.iterm2Image) {
|
|
6151
|
+
wrappedLine.iterm2Image = line.iterm2Image;
|
|
6152
|
+
}
|
|
5886
6153
|
if (id !== void 0 && chunk.length > 0) {
|
|
5887
6154
|
const found = line.body.indexOf(chunk, scanPos);
|
|
5888
6155
|
const colOffset = found === -1 ? scanPos : found;
|
|
@@ -5928,6 +6195,12 @@ var init_screen = __esm({
|
|
|
5928
6195
|
if (line.ansi || line.body.includes("^")) {
|
|
5929
6196
|
this.term.styleReset();
|
|
5930
6197
|
}
|
|
6198
|
+
if (line.iterm2Image && this.isIterm2()) {
|
|
6199
|
+
this.writeIterm2Image(
|
|
6200
|
+
line.iterm2Image.data,
|
|
6201
|
+
line.iterm2Image.heightCells
|
|
6202
|
+
);
|
|
6203
|
+
}
|
|
5931
6204
|
}
|
|
5932
6205
|
};
|
|
5933
6206
|
NON_ASCII = /[^\x20-\x7e]/;
|
|
@@ -5972,6 +6245,17 @@ var init_input = __esm({
|
|
|
5972
6245
|
// here so ^Y can yank it back. Standard readline keeps a stack; we
|
|
5973
6246
|
// only keep one slot because that's what 99% of yank uses look like.
|
|
5974
6247
|
killBuffer = "";
|
|
6248
|
+
// Images attached to the current draft. Cleared in the same paths
|
|
6249
|
+
// that clear the text buffer (clearBuffer, after send). Queue
|
|
6250
|
+
// navigation snapshots/restores them alongside savedDraft so up/down
|
|
6251
|
+
// through queued items doesn't drop chips.
|
|
6252
|
+
attachments = [];
|
|
6253
|
+
// Snapshot of `attachments` taken when the user starts walking
|
|
6254
|
+
// history/queue with chips already attached. Restored alongside the
|
|
6255
|
+
// text draft when the walk ends. Distinct from savedDraft because
|
|
6256
|
+
// queue slots (which may carry their own attachments — though we
|
|
6257
|
+
// don't surface that yet) shouldn't blend with the current draft's.
|
|
6258
|
+
savedAttachments = null;
|
|
5975
6259
|
constructor(opts = {}) {
|
|
5976
6260
|
this.history = [...opts.history ?? []];
|
|
5977
6261
|
this.planMode = opts.planMode ?? false;
|
|
@@ -5984,9 +6268,22 @@ var init_input = __esm({
|
|
|
5984
6268
|
planMode: this.planMode,
|
|
5985
6269
|
historyIndex: this.historyIndex,
|
|
5986
6270
|
queueIndex: this.queueIndex,
|
|
6271
|
+
attachments: [...this.attachments],
|
|
5987
6272
|
historySearchQuery: this.historySearch?.query ?? null
|
|
5988
6273
|
};
|
|
5989
6274
|
}
|
|
6275
|
+
// App calls this after asynchronously acquiring an image (drag-drop
|
|
6276
|
+
// file read, clipboard shellout). The dispatcher just records it;
|
|
6277
|
+
// chip rendering and capability gating live in the app/screen layer.
|
|
6278
|
+
addAttachment(attachment) {
|
|
6279
|
+
this.attachments.push(attachment);
|
|
6280
|
+
}
|
|
6281
|
+
removeAttachment(index) {
|
|
6282
|
+
if (index < 0 || index >= this.attachments.length) {
|
|
6283
|
+
return;
|
|
6284
|
+
}
|
|
6285
|
+
this.attachments.splice(index, 1);
|
|
6286
|
+
}
|
|
5990
6287
|
setTurnRunning(running) {
|
|
5991
6288
|
this.turnRunning = running;
|
|
5992
6289
|
}
|
|
@@ -6017,13 +6314,17 @@ var init_input = __esm({
|
|
|
6017
6314
|
}
|
|
6018
6315
|
// Public seed for the buffer (used for Escape pre-fill). Treated like a
|
|
6019
6316
|
// fresh draft: nav state and any saved draft are cleared, cursor lands
|
|
6020
|
-
// at the end so the user can edit immediately.
|
|
6021
|
-
|
|
6317
|
+
// at the end so the user can edit immediately. Attachments restore
|
|
6318
|
+
// alongside the text so a cancelled turn's chips land back in the
|
|
6319
|
+
// draft together with the typed prompt.
|
|
6320
|
+
setBuffer(text, attachments = []) {
|
|
6022
6321
|
this.loadEntry(text);
|
|
6023
6322
|
this.historyIndex = -1;
|
|
6024
6323
|
this.queueIndex = -1;
|
|
6025
6324
|
this.savedDraft = null;
|
|
6325
|
+
this.savedAttachments = null;
|
|
6026
6326
|
this.historySearch = null;
|
|
6327
|
+
this.attachments = [...attachments];
|
|
6027
6328
|
}
|
|
6028
6329
|
feed(event) {
|
|
6029
6330
|
if (this.historySearch !== null) {
|
|
@@ -6069,6 +6370,9 @@ var init_input = __esm({
|
|
|
6069
6370
|
this.insertText(event.text);
|
|
6070
6371
|
return [];
|
|
6071
6372
|
}
|
|
6373
|
+
if (event.type === "attachment-paths") {
|
|
6374
|
+
return [];
|
|
6375
|
+
}
|
|
6072
6376
|
return this.handleKey(event.name);
|
|
6073
6377
|
}
|
|
6074
6378
|
handleKey(name) {
|
|
@@ -6145,6 +6449,8 @@ var init_input = __esm({
|
|
|
6145
6449
|
case "ctrl-u":
|
|
6146
6450
|
this.killLine();
|
|
6147
6451
|
return [];
|
|
6452
|
+
case "ctrl-v":
|
|
6453
|
+
return [{ type: "attachment-request", source: "clipboard" }];
|
|
6148
6454
|
case "ctrl-w":
|
|
6149
6455
|
this.killWord();
|
|
6150
6456
|
return [];
|
|
@@ -6177,7 +6483,9 @@ var init_input = __esm({
|
|
|
6177
6483
|
this.historyIndex = -1;
|
|
6178
6484
|
this.queueIndex = -1;
|
|
6179
6485
|
this.savedDraft = null;
|
|
6486
|
+
this.savedAttachments = null;
|
|
6180
6487
|
this.historySearch = null;
|
|
6488
|
+
this.attachments = [];
|
|
6181
6489
|
}
|
|
6182
6490
|
insertChar(ch) {
|
|
6183
6491
|
if (ch.length === 0) {
|
|
@@ -6329,6 +6637,8 @@ var init_input = __esm({
|
|
|
6329
6637
|
row: this.row,
|
|
6330
6638
|
col: this.col
|
|
6331
6639
|
};
|
|
6640
|
+
this.savedAttachments = [...this.attachments];
|
|
6641
|
+
this.attachments = [];
|
|
6332
6642
|
if (this.queue.length > 0) {
|
|
6333
6643
|
this.queueIndex = this.queue.length - 1;
|
|
6334
6644
|
this.loadEntry(this.queue[this.queueIndex] ?? "");
|
|
@@ -6401,6 +6711,8 @@ var init_input = __esm({
|
|
|
6401
6711
|
this.row = this.savedDraft.row;
|
|
6402
6712
|
this.col = this.savedDraft.col;
|
|
6403
6713
|
this.savedDraft = null;
|
|
6714
|
+
this.attachments = this.savedAttachments ?? [];
|
|
6715
|
+
this.savedAttachments = null;
|
|
6404
6716
|
} else {
|
|
6405
6717
|
this.clearBuffer();
|
|
6406
6718
|
}
|
|
@@ -6554,18 +6866,20 @@ var init_input = __esm({
|
|
|
6554
6866
|
const text = this.bufferText();
|
|
6555
6867
|
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
6556
6868
|
const index = this.queueIndex;
|
|
6869
|
+
const attachments2 = [...this.attachments];
|
|
6557
6870
|
this.clearBuffer();
|
|
6558
6871
|
if (text.trim().length === 0) {
|
|
6559
6872
|
return [{ type: "queue-remove", index }];
|
|
6560
6873
|
}
|
|
6561
|
-
return [{ type: "queue-edit", index, text }];
|
|
6874
|
+
return [{ type: "queue-edit", index, text, attachments: attachments2 }];
|
|
6562
6875
|
}
|
|
6563
|
-
if (text.trim().length === 0) {
|
|
6876
|
+
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
6564
6877
|
return [];
|
|
6565
6878
|
}
|
|
6566
6879
|
const planMode = this.planMode;
|
|
6880
|
+
const attachments = [...this.attachments];
|
|
6567
6881
|
this.clearBuffer();
|
|
6568
|
-
return [{ type: "send", text, planMode }];
|
|
6882
|
+
return [{ type: "send", text, planMode, attachments }];
|
|
6569
6883
|
}
|
|
6570
6884
|
// Home: jump to the very start of the prompt buffer. If we're already
|
|
6571
6885
|
// there, fall through to scrolling the scrollback to its top.
|
|
@@ -6590,19 +6904,20 @@ var init_input = __esm({
|
|
|
6590
6904
|
return [{ type: "scroll-to-bottom" }];
|
|
6591
6905
|
}
|
|
6592
6906
|
handleCtrlC() {
|
|
6593
|
-
if (!this.bufferIsEmpty()) {
|
|
6594
|
-
this.buffer = [""];
|
|
6595
|
-
this.row = 0;
|
|
6596
|
-
this.col = 0;
|
|
6597
|
-
if (this.queueIndex === -1) {
|
|
6598
|
-
this.historyIndex = -1;
|
|
6599
|
-
this.savedDraft = null;
|
|
6600
|
-
}
|
|
6601
|
-
return [];
|
|
6602
|
-
}
|
|
6603
6907
|
if (this.queueIndex >= 0) {
|
|
6908
|
+
const index = this.queueIndex;
|
|
6604
6909
|
this.queueIndex = -1;
|
|
6605
6910
|
this.restoreDraft();
|
|
6911
|
+
return [{ type: "queue-remove", index }];
|
|
6912
|
+
}
|
|
6913
|
+
if (!this.bufferIsEmpty() || this.attachments.length > 0) {
|
|
6914
|
+
this.buffer = [""];
|
|
6915
|
+
this.row = 0;
|
|
6916
|
+
this.col = 0;
|
|
6917
|
+
this.attachments = [];
|
|
6918
|
+
this.historyIndex = -1;
|
|
6919
|
+
this.savedDraft = null;
|
|
6920
|
+
this.savedAttachments = null;
|
|
6606
6921
|
return [];
|
|
6607
6922
|
}
|
|
6608
6923
|
if (this.turnRunning) {
|
|
@@ -6614,6 +6929,232 @@ var init_input = __esm({
|
|
|
6614
6929
|
}
|
|
6615
6930
|
});
|
|
6616
6931
|
|
|
6932
|
+
// src/tui/clipboard.ts
|
|
6933
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
6934
|
+
import fs14 from "fs/promises";
|
|
6935
|
+
import os4 from "os";
|
|
6936
|
+
import path10 from "path";
|
|
6937
|
+
async function readClipboard(envIn = {}) {
|
|
6938
|
+
const env = { ...defaultEnv, ...envIn };
|
|
6939
|
+
if (env.platform === "darwin") {
|
|
6940
|
+
return readMacOS(env);
|
|
6941
|
+
}
|
|
6942
|
+
if (env.platform === "linux") {
|
|
6943
|
+
return readLinux(env);
|
|
6944
|
+
}
|
|
6945
|
+
return {
|
|
6946
|
+
ok: false,
|
|
6947
|
+
reason: `clipboard paste is not supported on ${env.platform}`
|
|
6948
|
+
};
|
|
6949
|
+
}
|
|
6950
|
+
async function readMacOS(env) {
|
|
6951
|
+
const tmpPath = path10.join(
|
|
6952
|
+
env.tmpdir(),
|
|
6953
|
+
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
6954
|
+
);
|
|
6955
|
+
const script = [
|
|
6956
|
+
"set png_data to the clipboard as \xABclass PNGf\xBB",
|
|
6957
|
+
`set out_file to (open for access (POSIX file "${tmpPath}") with write permission)`,
|
|
6958
|
+
"write png_data to out_file",
|
|
6959
|
+
"close access out_file"
|
|
6960
|
+
];
|
|
6961
|
+
const args = [];
|
|
6962
|
+
for (const line of script) {
|
|
6963
|
+
args.push("-e", line);
|
|
6964
|
+
}
|
|
6965
|
+
try {
|
|
6966
|
+
await run2(env.spawn, "osascript", args);
|
|
6967
|
+
const img = await readFileAsAttachment(tmpPath, true);
|
|
6968
|
+
if (img.ok) {
|
|
6969
|
+
return img;
|
|
6970
|
+
}
|
|
6971
|
+
if (img.reason.startsWith("clipboard image is")) {
|
|
6972
|
+
return img;
|
|
6973
|
+
}
|
|
6974
|
+
} catch {
|
|
6975
|
+
await fs14.unlink(tmpPath).catch(() => void 0);
|
|
6976
|
+
}
|
|
6977
|
+
try {
|
|
6978
|
+
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
6979
|
+
if (buf.length === 0) {
|
|
6980
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
6981
|
+
}
|
|
6982
|
+
return { ok: true, kind: "text", text: normalizeText(buf.toString("utf-8")) };
|
|
6983
|
+
} catch {
|
|
6984
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
async function readLinux(env) {
|
|
6988
|
+
const tool = await detectLinuxTool(env);
|
|
6989
|
+
if (!tool) {
|
|
6990
|
+
return {
|
|
6991
|
+
ok: false,
|
|
6992
|
+
reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
|
|
6993
|
+
};
|
|
6994
|
+
}
|
|
6995
|
+
try {
|
|
6996
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.imageArgs);
|
|
6997
|
+
if (buf.length > 0) {
|
|
6998
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
6999
|
+
return {
|
|
7000
|
+
ok: false,
|
|
7001
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7002
|
+
};
|
|
7003
|
+
}
|
|
7004
|
+
return {
|
|
7005
|
+
ok: true,
|
|
7006
|
+
kind: "image",
|
|
7007
|
+
attachment: {
|
|
7008
|
+
mimeType: "image/png",
|
|
7009
|
+
data: buf.toString("base64"),
|
|
7010
|
+
sizeBytes: buf.length
|
|
7011
|
+
}
|
|
7012
|
+
};
|
|
7013
|
+
}
|
|
7014
|
+
} catch {
|
|
7015
|
+
}
|
|
7016
|
+
try {
|
|
7017
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
|
|
7018
|
+
if (buf.length === 0) {
|
|
7019
|
+
return { ok: false, reason: "clipboard is empty" };
|
|
7020
|
+
}
|
|
7021
|
+
return {
|
|
7022
|
+
ok: true,
|
|
7023
|
+
kind: "text",
|
|
7024
|
+
text: normalizeText(buf.toString("utf-8"))
|
|
7025
|
+
};
|
|
7026
|
+
} catch {
|
|
7027
|
+
return { ok: false, reason: "clipboard read failed" };
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
async function detectLinuxTool(env) {
|
|
7031
|
+
if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
|
|
7032
|
+
return {
|
|
7033
|
+
cmd: "wl-paste",
|
|
7034
|
+
imageArgs: ["-t", "image/png"],
|
|
7035
|
+
// -n: drop trailing newline wl-paste adds by default. We further
|
|
7036
|
+
// normalize line endings below, but this avoids a spurious
|
|
7037
|
+
// empty trailing row from a single-line clipboard text.
|
|
7038
|
+
textArgs: ["-n"]
|
|
7039
|
+
};
|
|
7040
|
+
}
|
|
7041
|
+
if (env.env.DISPLAY && await which(env, "xclip")) {
|
|
7042
|
+
return {
|
|
7043
|
+
cmd: "xclip",
|
|
7044
|
+
imageArgs: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
7045
|
+
textArgs: ["-selection", "clipboard", "-o"]
|
|
7046
|
+
};
|
|
7047
|
+
}
|
|
7048
|
+
return null;
|
|
7049
|
+
}
|
|
7050
|
+
function normalizeText(text) {
|
|
7051
|
+
return text.replace(/\r\n?/g, "\n");
|
|
7052
|
+
}
|
|
7053
|
+
async function which(env, cmd) {
|
|
7054
|
+
try {
|
|
7055
|
+
await run2(env.spawn, "which", [cmd]);
|
|
7056
|
+
return true;
|
|
7057
|
+
} catch {
|
|
7058
|
+
return false;
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
7061
|
+
async function readFileAsAttachment(p, unlinkAfter) {
|
|
7062
|
+
try {
|
|
7063
|
+
const buf = await fs14.readFile(p);
|
|
7064
|
+
if (unlinkAfter) {
|
|
7065
|
+
await fs14.unlink(p).catch(() => void 0);
|
|
7066
|
+
}
|
|
7067
|
+
if (buf.length === 0) {
|
|
7068
|
+
return { ok: false, reason: "no image on clipboard" };
|
|
7069
|
+
}
|
|
7070
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
7071
|
+
return {
|
|
7072
|
+
ok: false,
|
|
7073
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
7074
|
+
};
|
|
7075
|
+
}
|
|
7076
|
+
const mimeType = mimeFromExtension(p) ?? "image/png";
|
|
7077
|
+
return {
|
|
7078
|
+
ok: true,
|
|
7079
|
+
kind: "image",
|
|
7080
|
+
attachment: {
|
|
7081
|
+
mimeType,
|
|
7082
|
+
data: buf.toString("base64"),
|
|
7083
|
+
sizeBytes: buf.length
|
|
7084
|
+
}
|
|
7085
|
+
};
|
|
7086
|
+
} catch {
|
|
7087
|
+
return { ok: false, reason: "failed to read clipboard image" };
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
function run2(spawn6, cmd, args) {
|
|
7091
|
+
return new Promise((resolve5, reject) => {
|
|
7092
|
+
const proc = spawn6(cmd, args);
|
|
7093
|
+
proc.stdout?.on("data", () => void 0);
|
|
7094
|
+
proc.stderr?.on("data", () => void 0);
|
|
7095
|
+
proc.on("error", reject);
|
|
7096
|
+
proc.on("close", (code) => {
|
|
7097
|
+
if (code === 0) {
|
|
7098
|
+
resolve5();
|
|
7099
|
+
} else {
|
|
7100
|
+
reject(new Error(`${cmd} exited ${code}`));
|
|
7101
|
+
}
|
|
7102
|
+
});
|
|
7103
|
+
});
|
|
7104
|
+
}
|
|
7105
|
+
function runCapture(spawn6, cmd, args) {
|
|
7106
|
+
return new Promise((resolve5, reject) => {
|
|
7107
|
+
const proc = spawn6(cmd, args);
|
|
7108
|
+
const chunks = [];
|
|
7109
|
+
let stdoutEnded = proc.stdout === null;
|
|
7110
|
+
let closedCode = null;
|
|
7111
|
+
let settled = false;
|
|
7112
|
+
const settle = () => {
|
|
7113
|
+
if (settled || !stdoutEnded || closedCode === null) {
|
|
7114
|
+
return;
|
|
7115
|
+
}
|
|
7116
|
+
settled = true;
|
|
7117
|
+
if (closedCode === 0) {
|
|
7118
|
+
resolve5(Buffer.concat(chunks));
|
|
7119
|
+
} else {
|
|
7120
|
+
reject(new Error(`${cmd} exited ${closedCode}`));
|
|
7121
|
+
}
|
|
7122
|
+
};
|
|
7123
|
+
proc.stdout?.on("data", (chunk) => {
|
|
7124
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
7125
|
+
});
|
|
7126
|
+
proc.stdout?.on("end", () => {
|
|
7127
|
+
stdoutEnded = true;
|
|
7128
|
+
settle();
|
|
7129
|
+
});
|
|
7130
|
+
proc.stderr?.on("data", () => void 0);
|
|
7131
|
+
proc.on("error", (err) => {
|
|
7132
|
+
if (settled) {
|
|
7133
|
+
return;
|
|
7134
|
+
}
|
|
7135
|
+
settled = true;
|
|
7136
|
+
reject(err);
|
|
7137
|
+
});
|
|
7138
|
+
proc.on("close", (code) => {
|
|
7139
|
+
closedCode = code ?? 0;
|
|
7140
|
+
settle();
|
|
7141
|
+
});
|
|
7142
|
+
});
|
|
7143
|
+
}
|
|
7144
|
+
var defaultEnv;
|
|
7145
|
+
var init_clipboard = __esm({
|
|
7146
|
+
"src/tui/clipboard.ts"() {
|
|
7147
|
+
"use strict";
|
|
7148
|
+
init_attachments();
|
|
7149
|
+
defaultEnv = {
|
|
7150
|
+
platform: process.platform,
|
|
7151
|
+
env: process.env,
|
|
7152
|
+
spawn: nodeSpawn,
|
|
7153
|
+
tmpdir: os4.tmpdir
|
|
7154
|
+
};
|
|
7155
|
+
}
|
|
7156
|
+
});
|
|
7157
|
+
|
|
6617
7158
|
// src/tui/completion.ts
|
|
6618
7159
|
function longestCommonPrefix(names) {
|
|
6619
7160
|
if (names.length === 0) {
|
|
@@ -6948,8 +7489,29 @@ import chalk from "chalk";
|
|
|
6948
7489
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
6949
7490
|
function formatEvent(event) {
|
|
6950
7491
|
switch (event.kind) {
|
|
6951
|
-
case "user-text":
|
|
6952
|
-
|
|
7492
|
+
case "user-text": {
|
|
7493
|
+
const lines = formatBlock(
|
|
7494
|
+
event.text,
|
|
7495
|
+
"\u258E ",
|
|
7496
|
+
"user",
|
|
7497
|
+
void 0,
|
|
7498
|
+
event.sentBy,
|
|
7499
|
+
true
|
|
7500
|
+
);
|
|
7501
|
+
if (event.attachments && event.attachments.length > 0) {
|
|
7502
|
+
for (const a of event.attachments) {
|
|
7503
|
+
lines.push({
|
|
7504
|
+
prefix: "\u258E ",
|
|
7505
|
+
prefixStyle: "user",
|
|
7506
|
+
body: `\u{1F4CE} ${a.name ?? "image"}`,
|
|
7507
|
+
bodyStyle: "user",
|
|
7508
|
+
fillRow: true,
|
|
7509
|
+
iterm2Image: { data: a.data, heightCells: 5 }
|
|
7510
|
+
});
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
return lines;
|
|
7514
|
+
}
|
|
6953
7515
|
case "agent-text":
|
|
6954
7516
|
return formatBlock(event.text, " ", "agent");
|
|
6955
7517
|
case "agent-thought":
|
|
@@ -7279,6 +7841,8 @@ var init_format = __esm({
|
|
|
7279
7841
|
import { appendFileSync, statSync, renameSync } from "fs";
|
|
7280
7842
|
import { nanoid as nanoid3 } from "nanoid";
|
|
7281
7843
|
import termkit from "terminal-kit";
|
|
7844
|
+
import fs15 from "fs/promises";
|
|
7845
|
+
import path11 from "path";
|
|
7282
7846
|
async function runTuiApp(opts) {
|
|
7283
7847
|
const config = await ensureConfig();
|
|
7284
7848
|
logMaxBytes = config.tui.logMaxBytes;
|
|
@@ -7396,6 +7960,15 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7396
7960
|
appendRender(event);
|
|
7397
7961
|
maybeDismissPermissionByToolUpdate(update);
|
|
7398
7962
|
});
|
|
7963
|
+
conn.onNotification("hydra-acp/session_closed", () => {
|
|
7964
|
+
if (pendingTurns > 0) {
|
|
7965
|
+
adjustPendingTurns(-pendingTurns);
|
|
7966
|
+
}
|
|
7967
|
+
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
7968
|
+
if (screenReady) {
|
|
7969
|
+
screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
|
|
7970
|
+
}
|
|
7971
|
+
});
|
|
7399
7972
|
const handlePermissionResolved = (update) => {
|
|
7400
7973
|
const u = update ?? {};
|
|
7401
7974
|
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
|
|
@@ -7499,6 +8072,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7499
8072
|
});
|
|
7500
8073
|
let upstreamSessionId;
|
|
7501
8074
|
let agentInfoName;
|
|
8075
|
+
let agentAcceptsImages = true;
|
|
7502
8076
|
try {
|
|
7503
8077
|
const initResult = await conn.request("initialize", {
|
|
7504
8078
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
@@ -7509,6 +8083,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7509
8083
|
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
7510
8084
|
});
|
|
7511
8085
|
agentInfoName = initResult?.agentInfo?.name;
|
|
8086
|
+
const imageCap = initResult?.agentCapabilities?.promptCapabilities?.image;
|
|
8087
|
+
if (imageCap === false) {
|
|
8088
|
+
agentAcceptsImages = false;
|
|
8089
|
+
}
|
|
7512
8090
|
} catch {
|
|
7513
8091
|
}
|
|
7514
8092
|
let resolvedSessionId = ctx.sessionId;
|
|
@@ -7607,6 +8185,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7607
8185
|
if (tryHandleCompletionKey(ev)) {
|
|
7608
8186
|
continue;
|
|
7609
8187
|
}
|
|
8188
|
+
if (ev.type === "attachment-paths") {
|
|
8189
|
+
void handleAttachmentPaths(ev.paths);
|
|
8190
|
+
continue;
|
|
8191
|
+
}
|
|
7610
8192
|
const effects = dispatcher.feed(ev);
|
|
7611
8193
|
for (const effect of effects) {
|
|
7612
8194
|
handleEffect(effect);
|
|
@@ -7616,6 +8198,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7616
8198
|
screen.setBannerSearchIndicator(
|
|
7617
8199
|
dispatcher.state().historySearchQuery
|
|
7618
8200
|
);
|
|
8201
|
+
screen.setAttachments(dispatcher.state().attachments);
|
|
7619
8202
|
screen.refreshPrompt();
|
|
7620
8203
|
}
|
|
7621
8204
|
});
|
|
@@ -7906,7 +8489,8 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7906
8489
|
const choice = await pickSession(term, {
|
|
7907
8490
|
cwd: resolvedCwd,
|
|
7908
8491
|
sessions,
|
|
7909
|
-
config
|
|
8492
|
+
config,
|
|
8493
|
+
currentSessionId: resolvedSessionId
|
|
7910
8494
|
});
|
|
7911
8495
|
if (choice.kind === "abort") {
|
|
7912
8496
|
screen.start();
|
|
@@ -7937,13 +8521,17 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7937
8521
|
const handleEffect = (effect) => {
|
|
7938
8522
|
switch (effect.type) {
|
|
7939
8523
|
case "send":
|
|
7940
|
-
enqueuePrompt(effect.text, effect.planMode);
|
|
8524
|
+
enqueuePrompt(effect.text, effect.planMode, effect.attachments);
|
|
7941
8525
|
return;
|
|
7942
8526
|
case "queue-edit": {
|
|
7943
8527
|
const realIdx = effect.index + queueHeadOffset();
|
|
7944
8528
|
const existing = promptQueue[realIdx];
|
|
7945
8529
|
if (existing) {
|
|
7946
|
-
promptQueue[realIdx] = {
|
|
8530
|
+
promptQueue[realIdx] = {
|
|
8531
|
+
text: effect.text,
|
|
8532
|
+
planMode: existing.planMode,
|
|
8533
|
+
attachments: effect.attachments
|
|
8534
|
+
};
|
|
7947
8535
|
refreshQueueDisplay();
|
|
7948
8536
|
}
|
|
7949
8537
|
return;
|
|
@@ -7962,7 +8550,10 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
7962
8550
|
const waitingEmpty = promptQueue.length <= headOffset;
|
|
7963
8551
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
7964
8552
|
if (waitingEmpty && bufferEmpty) {
|
|
7965
|
-
pendingPrefill =
|
|
8553
|
+
pendingPrefill = {
|
|
8554
|
+
text: turnInFlight.text,
|
|
8555
|
+
attachments: turnInFlight.attachments
|
|
8556
|
+
};
|
|
7966
8557
|
}
|
|
7967
8558
|
}
|
|
7968
8559
|
if (turnInFlight) {
|
|
@@ -8001,17 +8592,104 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8001
8592
|
screen.enterScrollbackSearch();
|
|
8002
8593
|
screen.updateScrollbackSearchTerm(effect.query);
|
|
8003
8594
|
return;
|
|
8595
|
+
case "attachment-request":
|
|
8596
|
+
void handleClipboardAttachment();
|
|
8597
|
+
return;
|
|
8004
8598
|
}
|
|
8005
8599
|
};
|
|
8600
|
+
const handleAttachmentPaths = async (tokens) => {
|
|
8601
|
+
if (!agentAcceptsImages) {
|
|
8602
|
+
screen.notify("agent does not accept image attachments");
|
|
8603
|
+
return;
|
|
8604
|
+
}
|
|
8605
|
+
let added = 0;
|
|
8606
|
+
for (const token of tokens) {
|
|
8607
|
+
if (token.startsWith("data:")) {
|
|
8608
|
+
const parsed = parseDataUriImage(token);
|
|
8609
|
+
if (!parsed) {
|
|
8610
|
+
screen.notify("unsupported data: URI");
|
|
8611
|
+
continue;
|
|
8612
|
+
}
|
|
8613
|
+
if (parsed.sizeBytes > MAX_ATTACHMENT_BYTES) {
|
|
8614
|
+
screen.notify(
|
|
8615
|
+
`image too large (${formatSize(parsed.sizeBytes)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
|
|
8616
|
+
);
|
|
8617
|
+
continue;
|
|
8618
|
+
}
|
|
8619
|
+
dispatcher.addAttachment({
|
|
8620
|
+
mimeType: parsed.mimeType,
|
|
8621
|
+
data: parsed.data,
|
|
8622
|
+
name: "pasted image",
|
|
8623
|
+
sizeBytes: parsed.sizeBytes
|
|
8624
|
+
});
|
|
8625
|
+
added++;
|
|
8626
|
+
continue;
|
|
8627
|
+
}
|
|
8628
|
+
const mimeType = mimeFromExtension(token);
|
|
8629
|
+
if (!mimeType) {
|
|
8630
|
+
screen.notify(`unsupported image type: ${path11.basename(token)}`);
|
|
8631
|
+
continue;
|
|
8632
|
+
}
|
|
8633
|
+
try {
|
|
8634
|
+
const buf = await fs15.readFile(token);
|
|
8635
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
8636
|
+
screen.notify(
|
|
8637
|
+
`image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
|
|
8638
|
+
);
|
|
8639
|
+
continue;
|
|
8640
|
+
}
|
|
8641
|
+
dispatcher.addAttachment({
|
|
8642
|
+
mimeType,
|
|
8643
|
+
data: buf.toString("base64"),
|
|
8644
|
+
name: path11.basename(token),
|
|
8645
|
+
sizeBytes: buf.length
|
|
8646
|
+
});
|
|
8647
|
+
added++;
|
|
8648
|
+
} catch (err) {
|
|
8649
|
+
screen.notify(
|
|
8650
|
+
`cannot read ${path11.basename(token)}: ${err.message}`
|
|
8651
|
+
);
|
|
8652
|
+
}
|
|
8653
|
+
}
|
|
8654
|
+
if (added > 0) {
|
|
8655
|
+
screen.setAttachments(dispatcher.state().attachments);
|
|
8656
|
+
screen.refreshPrompt();
|
|
8657
|
+
}
|
|
8658
|
+
};
|
|
8659
|
+
const handleClipboardAttachment = async () => {
|
|
8660
|
+
const result = await readClipboard();
|
|
8661
|
+
if (!result.ok) {
|
|
8662
|
+
screen.notify(result.reason);
|
|
8663
|
+
return;
|
|
8664
|
+
}
|
|
8665
|
+
if (result.kind === "image") {
|
|
8666
|
+
if (!agentAcceptsImages) {
|
|
8667
|
+
screen.notify("agent does not accept image attachments");
|
|
8668
|
+
return;
|
|
8669
|
+
}
|
|
8670
|
+
dispatcher.addAttachment(result.attachment);
|
|
8671
|
+
screen.setAttachments(dispatcher.state().attachments);
|
|
8672
|
+
screen.refreshPrompt();
|
|
8673
|
+
return;
|
|
8674
|
+
}
|
|
8675
|
+
const effects = dispatcher.feed({ type: "paste", text: result.text });
|
|
8676
|
+
for (const effect of effects) {
|
|
8677
|
+
handleEffect(effect);
|
|
8678
|
+
}
|
|
8679
|
+
screen.refreshPrompt();
|
|
8680
|
+
};
|
|
8006
8681
|
const promptQueue = [];
|
|
8007
8682
|
let workerActive = false;
|
|
8008
8683
|
const refreshQueueDisplay = () => {
|
|
8009
8684
|
const waiting = promptQueue.slice(workerActive ? 1 : 0);
|
|
8010
|
-
|
|
8685
|
+
const displayTexts = waiting.map(
|
|
8686
|
+
(p) => p.attachments.length > 0 ? `${p.text} \xB7 \u{1F4CE}\xD7${p.attachments.length}` : p.text
|
|
8687
|
+
);
|
|
8688
|
+
screen.setQueuedPrompts(displayTexts);
|
|
8011
8689
|
screen.setBanner({ queued: waiting.length });
|
|
8012
8690
|
dispatcher.setQueue(waiting.map((p) => p.text));
|
|
8013
8691
|
};
|
|
8014
|
-
const enqueuePrompt = (text, planMode) => {
|
|
8692
|
+
const enqueuePrompt = (text, planMode, attachments) => {
|
|
8015
8693
|
screen.scrollToBottom();
|
|
8016
8694
|
if (handleBuiltinCommand(text)) {
|
|
8017
8695
|
return;
|
|
@@ -8019,7 +8697,7 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8019
8697
|
history = appendEntry(history, text);
|
|
8020
8698
|
dispatcher.setHistory(history);
|
|
8021
8699
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
8022
|
-
promptQueue.push({ text, planMode });
|
|
8700
|
+
promptQueue.push({ text, planMode, attachments });
|
|
8023
8701
|
refreshQueueDisplay();
|
|
8024
8702
|
tickWorker();
|
|
8025
8703
|
};
|
|
@@ -8186,31 +8864,38 @@ async function runSession(term, config, opts, exitHint) {
|
|
|
8186
8864
|
break;
|
|
8187
8865
|
}
|
|
8188
8866
|
refreshQueueDisplay();
|
|
8189
|
-
await processPrompt(next.text, next.planMode);
|
|
8867
|
+
await processPrompt(next.text, next.planMode, next.attachments);
|
|
8190
8868
|
promptQueue.shift();
|
|
8191
8869
|
}
|
|
8192
8870
|
} finally {
|
|
8193
8871
|
workerActive = false;
|
|
8194
8872
|
refreshQueueDisplay();
|
|
8195
8873
|
if (pendingPrefill !== null) {
|
|
8196
|
-
const text = pendingPrefill;
|
|
8874
|
+
const { text, attachments } = pendingPrefill;
|
|
8197
8875
|
pendingPrefill = null;
|
|
8198
8876
|
const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
|
|
8199
8877
|
if (bufferEmpty) {
|
|
8200
|
-
dispatcher.setBuffer(text);
|
|
8878
|
+
dispatcher.setBuffer(text, attachments);
|
|
8201
8879
|
screen.refreshPrompt();
|
|
8202
8880
|
}
|
|
8203
8881
|
}
|
|
8204
8882
|
}
|
|
8205
8883
|
};
|
|
8206
|
-
const processPrompt = async (text, planMode) => {
|
|
8207
|
-
const userBlocks = [
|
|
8884
|
+
const processPrompt = async (text, planMode, attachments) => {
|
|
8885
|
+
const userBlocks = [];
|
|
8886
|
+
if (text.length > 0) {
|
|
8887
|
+
userBlocks.push({ type: "text", text });
|
|
8888
|
+
}
|
|
8889
|
+
for (const a of attachments) {
|
|
8890
|
+
userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
|
|
8891
|
+
}
|
|
8208
8892
|
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
8209
8893
|
adjustPendingTurns(1);
|
|
8210
|
-
appendRender({ kind: "user-text", text });
|
|
8894
|
+
appendRender({ kind: "user-text", text, attachments });
|
|
8211
8895
|
let cancelled = false;
|
|
8212
8896
|
turnInFlight = {
|
|
8213
8897
|
text,
|
|
8898
|
+
attachments,
|
|
8214
8899
|
cancel: () => {
|
|
8215
8900
|
if (cancelled) {
|
|
8216
8901
|
return;
|
|
@@ -8707,6 +9392,8 @@ var init_app = __esm({
|
|
|
8707
9392
|
init_picker();
|
|
8708
9393
|
init_screen();
|
|
8709
9394
|
init_input();
|
|
9395
|
+
init_attachments();
|
|
9396
|
+
init_clipboard();
|
|
8710
9397
|
init_completion();
|
|
8711
9398
|
init_render_update();
|
|
8712
9399
|
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
|