@hydra-acp/cli 0.1.30 → 0.1.32
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/README.md +17 -1
- package/dist/cli.js +797 -181
- package/dist/index.d.ts +5 -0
- package/dist/index.js +133 -20
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -271,12 +271,12 @@ var init_config = __esm({
|
|
|
271
271
|
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
272
272
|
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
273
273
|
maxScrollbackLines: z.number().int().positive().default(1e4),
|
|
274
|
-
// When true
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// false
|
|
278
|
-
//
|
|
279
|
-
mouse: z.boolean().default(
|
|
274
|
+
// When true, the TUI captures mouse events so the wheel can drive
|
|
275
|
+
// scrollback. The cost: terminals route clicks to the app, so text
|
|
276
|
+
// selection requires shift+drag to bypass mouse reporting. Default
|
|
277
|
+
// false — wheel scrollback stops working, but plain click-drag
|
|
278
|
+
// selects text via the terminal emulator. Set true to opt back in.
|
|
279
|
+
mouse: z.boolean().default(false),
|
|
280
280
|
// Size at which the TUI's session/update debug log (tui.log) rotates
|
|
281
281
|
// to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
|
|
282
282
|
logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
|
|
@@ -291,13 +291,13 @@ var init_config = __esm({
|
|
|
291
291
|
// just don't want it.
|
|
292
292
|
progressIndicator: z.boolean().default(true),
|
|
293
293
|
// What the unmodified Enter key does in the prompt composer.
|
|
294
|
-
// "
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// "
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
defaultEnterAction: z.enum(["enqueue", "amend"]).default("
|
|
294
|
+
// "amend" (default) — Enter amends the in-flight turn; Shift+Enter
|
|
295
|
+
// enqueues. With no turn in flight either key just enqueues,
|
|
296
|
+
// since there's nothing to amend.
|
|
297
|
+
// "enqueue" — flips the two: Enter enqueues the prompt (sends
|
|
298
|
+
// immediately when idle, queues behind an in-flight turn);
|
|
299
|
+
// Shift+Enter amends the in-flight turn.
|
|
300
|
+
defaultEnterAction: z.enum(["enqueue", "amend"]).default("amend")
|
|
301
301
|
});
|
|
302
302
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
303
303
|
ExtensionBody = z.object({
|
|
@@ -338,11 +338,11 @@ var init_config = __esm({
|
|
|
338
338
|
tui: TuiConfig.default({
|
|
339
339
|
repaintThrottleMs: 1e3,
|
|
340
340
|
maxScrollbackLines: 1e4,
|
|
341
|
-
mouse:
|
|
341
|
+
mouse: false,
|
|
342
342
|
logMaxBytes: 5 * 1024 * 1024,
|
|
343
343
|
cwdColumnMaxWidth: 24,
|
|
344
344
|
progressIndicator: true,
|
|
345
|
-
defaultEnterAction: "
|
|
345
|
+
defaultEnterAction: "amend"
|
|
346
346
|
})
|
|
347
347
|
});
|
|
348
348
|
}
|
|
@@ -584,6 +584,12 @@ var init_types = __esm({
|
|
|
584
584
|
name: z3.string(),
|
|
585
585
|
version: z3.string().optional()
|
|
586
586
|
}).optional(),
|
|
587
|
+
// When true, the connection observes the session but cannot mutate
|
|
588
|
+
// it: state-changing methods (session/prompt, session/cancel,
|
|
589
|
+
// session/set_model, etc.) are rejected with -32011, and attaching
|
|
590
|
+
// to a cold session does not resurrect or spawn an agent — just
|
|
591
|
+
// streams history from disk. Used by the TUI's view-only mode.
|
|
592
|
+
readonly: z3.boolean().optional(),
|
|
587
593
|
_meta: z3.record(z3.unknown()).optional()
|
|
588
594
|
});
|
|
589
595
|
HYDRA_META_KEY = "hydra-acp";
|
|
@@ -813,9 +819,9 @@ var init_connection = __esm({
|
|
|
813
819
|
}
|
|
814
820
|
const id = nanoid();
|
|
815
821
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
816
|
-
const response = new Promise((
|
|
822
|
+
const response = new Promise((resolve6, reject) => {
|
|
817
823
|
this.pending.set(id, {
|
|
818
|
-
resolve: (result) =>
|
|
824
|
+
resolve: (result) => resolve6(result),
|
|
819
825
|
reject
|
|
820
826
|
});
|
|
821
827
|
this.stream.send(message).catch((err) => {
|
|
@@ -3068,7 +3074,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3068
3074
|
}
|
|
3069
3075
|
const clientParams = this.rewriteForClient(params);
|
|
3070
3076
|
const toolCallId = extractToolCallId(clientParams);
|
|
3071
|
-
return new Promise((
|
|
3077
|
+
return new Promise((resolve6, reject) => {
|
|
3072
3078
|
let settled = false;
|
|
3073
3079
|
const outbound = [];
|
|
3074
3080
|
const entry = { addClient: sendTo };
|
|
@@ -3107,7 +3113,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3107
3113
|
update
|
|
3108
3114
|
}).catch(() => void 0);
|
|
3109
3115
|
}
|
|
3110
|
-
|
|
3116
|
+
resolve6(result);
|
|
3111
3117
|
});
|
|
3112
3118
|
}).catch((err) => {
|
|
3113
3119
|
settle(() => reject(err));
|
|
@@ -3123,14 +3129,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3123
3129
|
// in flight, but doesn't emit prompt_queue_* broadcasts — clients
|
|
3124
3130
|
// shouldn't see hydra's housekeeping in their chip list.
|
|
3125
3131
|
async enqueuePrompt(task) {
|
|
3126
|
-
return new Promise((
|
|
3132
|
+
return new Promise((resolve6, reject) => {
|
|
3127
3133
|
const entry = {
|
|
3128
3134
|
kind: "internal",
|
|
3129
3135
|
messageId: generateMessageId(),
|
|
3130
3136
|
enqueuedAt: Date.now(),
|
|
3131
3137
|
cancelled: false,
|
|
3132
3138
|
task,
|
|
3133
|
-
resolve:
|
|
3139
|
+
resolve: resolve6,
|
|
3134
3140
|
reject
|
|
3135
3141
|
};
|
|
3136
3142
|
this.promptQueue.push(entry);
|
|
@@ -3149,7 +3155,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3149
3155
|
if (client.clientInfo?.name) originator.name = client.clientInfo.name;
|
|
3150
3156
|
if (client.clientInfo?.version)
|
|
3151
3157
|
originator.version = client.clientInfo.version;
|
|
3152
|
-
return new Promise((
|
|
3158
|
+
return new Promise((resolve6, reject) => {
|
|
3153
3159
|
const entry = {
|
|
3154
3160
|
kind: "user",
|
|
3155
3161
|
messageId,
|
|
@@ -3158,7 +3164,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3158
3164
|
prompt: promptArray,
|
|
3159
3165
|
enqueuedAt: Date.now(),
|
|
3160
3166
|
cancelled: false,
|
|
3161
|
-
resolve:
|
|
3167
|
+
resolve: resolve6,
|
|
3162
3168
|
reject
|
|
3163
3169
|
};
|
|
3164
3170
|
this.promptQueue.push(entry);
|
|
@@ -4346,13 +4352,13 @@ function wsToMessageStream(ws) {
|
|
|
4346
4352
|
throw new Error("ws is closed");
|
|
4347
4353
|
}
|
|
4348
4354
|
const text = JSON.stringify(message);
|
|
4349
|
-
await new Promise((
|
|
4355
|
+
await new Promise((resolve6, reject) => {
|
|
4350
4356
|
ws.send(text, (err) => {
|
|
4351
4357
|
if (err) {
|
|
4352
4358
|
reject(err);
|
|
4353
4359
|
return;
|
|
4354
4360
|
}
|
|
4355
|
-
|
|
4361
|
+
resolve6();
|
|
4356
4362
|
});
|
|
4357
4363
|
});
|
|
4358
4364
|
},
|
|
@@ -4840,8 +4846,8 @@ async function runSessionsTranscript(idOrFile, outPath) {
|
|
|
4840
4846
|
}
|
|
4841
4847
|
async function readBundleFileIfExists(arg) {
|
|
4842
4848
|
try {
|
|
4843
|
-
const
|
|
4844
|
-
if (!
|
|
4849
|
+
const stat5 = await fs17.stat(arg);
|
|
4850
|
+
if (!stat5.isFile()) {
|
|
4845
4851
|
return null;
|
|
4846
4852
|
}
|
|
4847
4853
|
} catch {
|
|
@@ -4876,8 +4882,8 @@ async function runSessionsImport(file, opts = {}) {
|
|
|
4876
4882
|
if (opts.cwd !== void 0) {
|
|
4877
4883
|
const resolved = path11.resolve(opts.cwd);
|
|
4878
4884
|
try {
|
|
4879
|
-
const
|
|
4880
|
-
if (!
|
|
4885
|
+
const stat5 = await fs17.stat(resolved);
|
|
4886
|
+
if (!stat5.isDirectory()) {
|
|
4881
4887
|
process.stderr.write(`--cwd ${resolved} is not a directory
|
|
4882
4888
|
`);
|
|
4883
4889
|
process.exit(1);
|
|
@@ -5023,11 +5029,11 @@ function isResponse(msg) {
|
|
|
5023
5029
|
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
5024
5030
|
}
|
|
5025
5031
|
async function openWs(url, subprotocols) {
|
|
5026
|
-
return new Promise((
|
|
5032
|
+
return new Promise((resolve6, reject) => {
|
|
5027
5033
|
const ws = new WebSocket(url, subprotocols);
|
|
5028
5034
|
const onOpen = () => {
|
|
5029
5035
|
ws.off("error", onError);
|
|
5030
|
-
|
|
5036
|
+
resolve6(wsToMessageStream(ws));
|
|
5031
5037
|
};
|
|
5032
5038
|
const onError = (err) => {
|
|
5033
5039
|
ws.off("open", onOpen);
|
|
@@ -5098,8 +5104,8 @@ var init_resilient_ws = __esm({
|
|
|
5098
5104
|
throw new Error("resilient ws stream not connected");
|
|
5099
5105
|
}
|
|
5100
5106
|
const id = message.id;
|
|
5101
|
-
const promise = new Promise((
|
|
5102
|
-
this.pendingRequests.set(id, { resolve:
|
|
5107
|
+
const promise = new Promise((resolve6, reject) => {
|
|
5108
|
+
this.pendingRequests.set(id, { resolve: resolve6, reject });
|
|
5103
5109
|
});
|
|
5104
5110
|
try {
|
|
5105
5111
|
await this.current.send(message);
|
|
@@ -5127,8 +5133,8 @@ var init_resilient_ws = __esm({
|
|
|
5127
5133
|
this.bindStream(stream);
|
|
5128
5134
|
const wasFirst = this.firstConnect;
|
|
5129
5135
|
this.firstConnect = false;
|
|
5130
|
-
this.connectGate = new Promise((
|
|
5131
|
-
this.releaseConnectGate =
|
|
5136
|
+
this.connectGate = new Promise((resolve6) => {
|
|
5137
|
+
this.releaseConnectGate = resolve6;
|
|
5132
5138
|
});
|
|
5133
5139
|
try {
|
|
5134
5140
|
if (this.opts.onConnect) {
|
|
@@ -5413,6 +5419,15 @@ var init_discovery = __esm({
|
|
|
5413
5419
|
|
|
5414
5420
|
// src/tui/picker.ts
|
|
5415
5421
|
async function pickSession(term, opts) {
|
|
5422
|
+
process.stdout.write("\x1B[<u");
|
|
5423
|
+
process.stdout.write("\x1B[?2004l");
|
|
5424
|
+
process.stdout.write("\x1B[>4;0m");
|
|
5425
|
+
process.stdout.write("\x1B[>5;0m");
|
|
5426
|
+
process.stdout.write("\x1B[?1000l");
|
|
5427
|
+
process.stdout.write("\x1B[?1002l");
|
|
5428
|
+
process.stdout.write("\x1B[?1006l");
|
|
5429
|
+
process.stdout.write("\x1B[?1l");
|
|
5430
|
+
process.stdout.write("\x1B>");
|
|
5416
5431
|
if (opts.sessions.length === 0) {
|
|
5417
5432
|
return { kind: "new" };
|
|
5418
5433
|
}
|
|
@@ -5652,7 +5667,7 @@ async function pickSession(term, opts) {
|
|
|
5652
5667
|
};
|
|
5653
5668
|
renderFromScratch();
|
|
5654
5669
|
term.hideCursor();
|
|
5655
|
-
return await new Promise((
|
|
5670
|
+
return await new Promise((resolve6) => {
|
|
5656
5671
|
let resolved = false;
|
|
5657
5672
|
const onResize = () => {
|
|
5658
5673
|
if (resolved) {
|
|
@@ -5792,7 +5807,7 @@ async function pickSession(term, opts) {
|
|
|
5792
5807
|
if (mode === "help") {
|
|
5793
5808
|
if (name === "CTRL_C") {
|
|
5794
5809
|
cleanup();
|
|
5795
|
-
|
|
5810
|
+
resolve6({ kind: "abort" });
|
|
5796
5811
|
return;
|
|
5797
5812
|
}
|
|
5798
5813
|
mode = "normal";
|
|
@@ -5906,12 +5921,12 @@ async function pickSession(term, opts) {
|
|
|
5906
5921
|
}
|
|
5907
5922
|
if (name === "c" || name === "C") {
|
|
5908
5923
|
cleanup();
|
|
5909
|
-
|
|
5924
|
+
resolve6({ kind: "new" });
|
|
5910
5925
|
return;
|
|
5911
5926
|
}
|
|
5912
5927
|
if (name === "q" || name === "Q") {
|
|
5913
5928
|
cleanup();
|
|
5914
|
-
|
|
5929
|
+
resolve6({ kind: "abort" });
|
|
5915
5930
|
return;
|
|
5916
5931
|
}
|
|
5917
5932
|
if (name === "o" || name === "O") {
|
|
@@ -5947,6 +5962,23 @@ async function pickSession(term, opts) {
|
|
|
5947
5962
|
void refresh(currentId);
|
|
5948
5963
|
return;
|
|
5949
5964
|
}
|
|
5965
|
+
if ((name === "v" || name === "V") && selectedIdx > 0) {
|
|
5966
|
+
const session = visible[selectedIdx - 1];
|
|
5967
|
+
if (!session) {
|
|
5968
|
+
return;
|
|
5969
|
+
}
|
|
5970
|
+
cleanup();
|
|
5971
|
+
const result = {
|
|
5972
|
+
kind: "attach",
|
|
5973
|
+
sessionId: session.sessionId,
|
|
5974
|
+
readonly: true
|
|
5975
|
+
};
|
|
5976
|
+
if (session.agentId !== void 0) {
|
|
5977
|
+
result.agentId = session.agentId;
|
|
5978
|
+
}
|
|
5979
|
+
resolve6(result);
|
|
5980
|
+
return;
|
|
5981
|
+
}
|
|
5950
5982
|
if ((name === "k" || name === "K") && selectedIdx > 0) {
|
|
5951
5983
|
const session = visible[selectedIdx - 1];
|
|
5952
5984
|
if (!session) {
|
|
@@ -6030,12 +6062,12 @@ async function pickSession(term, opts) {
|
|
|
6030
6062
|
case "KP_ENTER": {
|
|
6031
6063
|
cleanup();
|
|
6032
6064
|
if (selectedIdx === 0) {
|
|
6033
|
-
|
|
6065
|
+
resolve6({ kind: "new" });
|
|
6034
6066
|
return;
|
|
6035
6067
|
}
|
|
6036
6068
|
const session = visible[selectedIdx - 1];
|
|
6037
6069
|
if (!session) {
|
|
6038
|
-
|
|
6070
|
+
resolve6({ kind: "abort" });
|
|
6039
6071
|
return;
|
|
6040
6072
|
}
|
|
6041
6073
|
const result = {
|
|
@@ -6045,14 +6077,14 @@ async function pickSession(term, opts) {
|
|
|
6045
6077
|
if (session.agentId !== void 0) {
|
|
6046
6078
|
result.agentId = session.agentId;
|
|
6047
6079
|
}
|
|
6048
|
-
|
|
6080
|
+
resolve6(result);
|
|
6049
6081
|
return;
|
|
6050
6082
|
}
|
|
6051
6083
|
case "ESCAPE":
|
|
6052
6084
|
case "CTRL_C":
|
|
6053
6085
|
case "CTRL_D":
|
|
6054
6086
|
cleanup();
|
|
6055
|
-
|
|
6087
|
+
resolve6({ kind: "abort" });
|
|
6056
6088
|
return;
|
|
6057
6089
|
}
|
|
6058
6090
|
};
|
|
@@ -6134,6 +6166,7 @@ var init_picker = __esm({
|
|
|
6134
6166
|
["PgUp / PgDn", "page up / page down"],
|
|
6135
6167
|
["Home / End", "first / last"],
|
|
6136
6168
|
["Enter", "open selected session (or create new)"],
|
|
6169
|
+
["v", "view-only (open transcript without spawning the agent)"],
|
|
6137
6170
|
null,
|
|
6138
6171
|
["/", "search sessions"],
|
|
6139
6172
|
["o", "toggle cwd-only filter"],
|
|
@@ -6152,10 +6185,184 @@ var init_picker = __esm({
|
|
|
6152
6185
|
}
|
|
6153
6186
|
});
|
|
6154
6187
|
|
|
6188
|
+
// src/core/cwd.ts
|
|
6189
|
+
import * as fs18 from "fs/promises";
|
|
6190
|
+
import * as path12 from "path";
|
|
6191
|
+
async function validateLocalCwd(input) {
|
|
6192
|
+
const trimmed = input.trim();
|
|
6193
|
+
if (trimmed.length === 0) {
|
|
6194
|
+
return { ok: false, reason: "path is empty" };
|
|
6195
|
+
}
|
|
6196
|
+
const resolved = path12.resolve(expandHome(trimmed));
|
|
6197
|
+
let stat5;
|
|
6198
|
+
try {
|
|
6199
|
+
stat5 = await fs18.stat(resolved);
|
|
6200
|
+
} catch {
|
|
6201
|
+
return { ok: false, reason: `${resolved} does not exist` };
|
|
6202
|
+
}
|
|
6203
|
+
if (!stat5.isDirectory()) {
|
|
6204
|
+
return { ok: false, reason: `${resolved} is not a directory` };
|
|
6205
|
+
}
|
|
6206
|
+
return { ok: true, path: resolved };
|
|
6207
|
+
}
|
|
6208
|
+
var init_cwd = __esm({
|
|
6209
|
+
"src/core/cwd.ts"() {
|
|
6210
|
+
"use strict";
|
|
6211
|
+
init_config();
|
|
6212
|
+
}
|
|
6213
|
+
});
|
|
6214
|
+
|
|
6215
|
+
// src/tui/import-cwd-prompt.ts
|
|
6216
|
+
import * as os4 from "os";
|
|
6217
|
+
async function promptForImportCwd(term, session, opts = {}) {
|
|
6218
|
+
const defaultCwd = opts.defaultCwd ?? os4.homedir();
|
|
6219
|
+
process.stdout.write("\x1B[<u");
|
|
6220
|
+
process.stdout.write("\x1B[?2004l");
|
|
6221
|
+
process.stdout.write("\x1B[>4;0m");
|
|
6222
|
+
process.stdout.write("\x1B[>5;0m");
|
|
6223
|
+
process.stdout.write("\x1B[?1000l");
|
|
6224
|
+
process.stdout.write("\x1B[?1002l");
|
|
6225
|
+
process.stdout.write("\x1B[?1006l");
|
|
6226
|
+
process.stdout.write("\x1B[?1l");
|
|
6227
|
+
process.stdout.write("\x1B>");
|
|
6228
|
+
const shortId2 = stripHydraSessionPrefix(session.sessionId);
|
|
6229
|
+
const fromMachine = session.importedFromMachine ?? "another machine";
|
|
6230
|
+
const originalCwd = session.cwd;
|
|
6231
|
+
let buffer = defaultCwd;
|
|
6232
|
+
let errorLine = null;
|
|
6233
|
+
let busy = false;
|
|
6234
|
+
const render = () => {
|
|
6235
|
+
term("\n");
|
|
6236
|
+
term.bold.cyan("Imported session: ");
|
|
6237
|
+
term(`${shortId2}
|
|
6238
|
+
`);
|
|
6239
|
+
term.dim(` from machine: `);
|
|
6240
|
+
term(`${fromMachine}
|
|
6241
|
+
`);
|
|
6242
|
+
term.dim(` original cwd: `);
|
|
6243
|
+
term(`${shortenHomePath(originalCwd)}
|
|
6244
|
+
`);
|
|
6245
|
+
term("\n");
|
|
6246
|
+
term(
|
|
6247
|
+
"This session has never been launched on this machine. Pick a local\n"
|
|
6248
|
+
);
|
|
6249
|
+
term("cwd for the agent (Enter to accept, Esc to cancel):\n\n");
|
|
6250
|
+
paintInput();
|
|
6251
|
+
if (errorLine) {
|
|
6252
|
+
term("\n");
|
|
6253
|
+
term.red(` ${errorLine}
|
|
6254
|
+
`);
|
|
6255
|
+
}
|
|
6256
|
+
};
|
|
6257
|
+
const paintInput = () => {
|
|
6258
|
+
term.bold("cwd: ");
|
|
6259
|
+
term(buffer);
|
|
6260
|
+
if (!busy) {
|
|
6261
|
+
term.bgWhite(" ");
|
|
6262
|
+
}
|
|
6263
|
+
};
|
|
6264
|
+
const repaintInput = () => {
|
|
6265
|
+
term.column(1);
|
|
6266
|
+
term.eraseLine();
|
|
6267
|
+
paintInput();
|
|
6268
|
+
if (errorLine !== null) {
|
|
6269
|
+
term("\n");
|
|
6270
|
+
term.eraseLine();
|
|
6271
|
+
term.red(` ${errorLine}`);
|
|
6272
|
+
term.up(1);
|
|
6273
|
+
term.column(1);
|
|
6274
|
+
}
|
|
6275
|
+
};
|
|
6276
|
+
render();
|
|
6277
|
+
return await new Promise((resolve6) => {
|
|
6278
|
+
let resolved = false;
|
|
6279
|
+
const cleanup = () => {
|
|
6280
|
+
if (resolved) {
|
|
6281
|
+
return;
|
|
6282
|
+
}
|
|
6283
|
+
resolved = true;
|
|
6284
|
+
term.off("key", onKey);
|
|
6285
|
+
term.grabInput(false);
|
|
6286
|
+
term.hideCursor(false);
|
|
6287
|
+
term("\n\n");
|
|
6288
|
+
};
|
|
6289
|
+
const finish = (value) => {
|
|
6290
|
+
cleanup();
|
|
6291
|
+
resolve6(value);
|
|
6292
|
+
};
|
|
6293
|
+
const onKey = (name, _matches, data) => {
|
|
6294
|
+
if (busy) {
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
if (name === "ENTER" || name === "KP_ENTER") {
|
|
6298
|
+
const candidate = buffer;
|
|
6299
|
+
busy = true;
|
|
6300
|
+
errorLine = null;
|
|
6301
|
+
repaintInput();
|
|
6302
|
+
void validateLocalCwd(candidate).then((result) => {
|
|
6303
|
+
busy = false;
|
|
6304
|
+
if (result.ok) {
|
|
6305
|
+
finish(result.path);
|
|
6306
|
+
return;
|
|
6307
|
+
}
|
|
6308
|
+
errorLine = result.reason;
|
|
6309
|
+
repaintInput();
|
|
6310
|
+
});
|
|
6311
|
+
return;
|
|
6312
|
+
}
|
|
6313
|
+
if (name === "ESCAPE" || name === "CTRL_C" || name === "CTRL_D") {
|
|
6314
|
+
finish(null);
|
|
6315
|
+
return;
|
|
6316
|
+
}
|
|
6317
|
+
if (name === "BACKSPACE") {
|
|
6318
|
+
if (buffer.length > 0) {
|
|
6319
|
+
buffer = buffer.slice(0, -1);
|
|
6320
|
+
errorLine = null;
|
|
6321
|
+
repaintInput();
|
|
6322
|
+
}
|
|
6323
|
+
return;
|
|
6324
|
+
}
|
|
6325
|
+
if (name === "CTRL_U") {
|
|
6326
|
+
buffer = "";
|
|
6327
|
+
errorLine = null;
|
|
6328
|
+
repaintInput();
|
|
6329
|
+
return;
|
|
6330
|
+
}
|
|
6331
|
+
if (name === "CTRL_W") {
|
|
6332
|
+
const trimmedRight = buffer.replace(/[/\s]+$/, "");
|
|
6333
|
+
const lastSep = Math.max(
|
|
6334
|
+
trimmedRight.lastIndexOf("/"),
|
|
6335
|
+
trimmedRight.lastIndexOf(" ")
|
|
6336
|
+
);
|
|
6337
|
+
buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
|
|
6338
|
+
errorLine = null;
|
|
6339
|
+
repaintInput();
|
|
6340
|
+
return;
|
|
6341
|
+
}
|
|
6342
|
+
if (data?.isCharacter) {
|
|
6343
|
+
buffer += name;
|
|
6344
|
+
errorLine = null;
|
|
6345
|
+
repaintInput();
|
|
6346
|
+
return;
|
|
6347
|
+
}
|
|
6348
|
+
};
|
|
6349
|
+
term.grabInput({});
|
|
6350
|
+
term.on("key", onKey);
|
|
6351
|
+
});
|
|
6352
|
+
}
|
|
6353
|
+
var init_import_cwd_prompt = __esm({
|
|
6354
|
+
"src/tui/import-cwd-prompt.ts"() {
|
|
6355
|
+
"use strict";
|
|
6356
|
+
init_paths();
|
|
6357
|
+
init_session();
|
|
6358
|
+
init_cwd();
|
|
6359
|
+
}
|
|
6360
|
+
});
|
|
6361
|
+
|
|
6155
6362
|
// src/tui/attachments.ts
|
|
6156
|
-
import
|
|
6363
|
+
import path13 from "path";
|
|
6157
6364
|
function mimeFromExtension(p) {
|
|
6158
|
-
return EXTENSION_TO_MIME[
|
|
6365
|
+
return EXTENSION_TO_MIME[path13.extname(p).toLowerCase()] ?? null;
|
|
6159
6366
|
}
|
|
6160
6367
|
function isSupportedImagePath(p) {
|
|
6161
6368
|
return mimeFromExtension(p) !== null;
|
|
@@ -6799,6 +7006,8 @@ function mapKeyName(name) {
|
|
|
6799
7006
|
return "ctrl-v";
|
|
6800
7007
|
case "CTRL_W":
|
|
6801
7008
|
return "ctrl-w";
|
|
7009
|
+
case "CTRL_X":
|
|
7010
|
+
return "ctrl-x";
|
|
6802
7011
|
case "CTRL_Y":
|
|
6803
7012
|
return "ctrl-y";
|
|
6804
7013
|
case "ESCAPE":
|
|
@@ -6807,6 +7016,110 @@ function mapKeyName(name) {
|
|
|
6807
7016
|
return null;
|
|
6808
7017
|
}
|
|
6809
7018
|
}
|
|
7019
|
+
function emergencyTerminalReset() {
|
|
7020
|
+
const seq = [
|
|
7021
|
+
"\x1B[?1000l",
|
|
7022
|
+
// mouse button reporting off
|
|
7023
|
+
"\x1B[?1002l",
|
|
7024
|
+
// mouse drag reporting off
|
|
7025
|
+
"\x1B[?1003l",
|
|
7026
|
+
// mouse any-motion reporting off
|
|
7027
|
+
"\x1B[?1006l",
|
|
7028
|
+
// SGR mouse mode off
|
|
7029
|
+
"\x1B[?1015l",
|
|
7030
|
+
// urxvt mouse mode off
|
|
7031
|
+
"\x1B[?2004l",
|
|
7032
|
+
// bracketed paste off
|
|
7033
|
+
"\x1B[>4;0m",
|
|
7034
|
+
// xterm modifyOtherKeys off
|
|
7035
|
+
"\x1B[>5;0m",
|
|
7036
|
+
// xterm formatOtherKeys off
|
|
7037
|
+
"\x1B[<u",
|
|
7038
|
+
// pop kitty keyboard stack
|
|
7039
|
+
"\x1B[?1l",
|
|
7040
|
+
// DECCKM off: arrows send CSI A/B/C/D not SS3 O A/B/C/D
|
|
7041
|
+
"\x1B>",
|
|
7042
|
+
// DECPAM off: numeric keypad mode
|
|
7043
|
+
"\x1B[?7h",
|
|
7044
|
+
// auto-wrap on
|
|
7045
|
+
"\x1B[?25h",
|
|
7046
|
+
// show cursor
|
|
7047
|
+
"\x1B]9;4;0\x07",
|
|
7048
|
+
// clear OSC 9;4 progress indicator
|
|
7049
|
+
"\x1B[?1049l"
|
|
7050
|
+
// leave alternate screen
|
|
7051
|
+
].join("");
|
|
7052
|
+
try {
|
|
7053
|
+
process.stdout.write(seq);
|
|
7054
|
+
} catch {
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7057
|
+
function mapCsiUToKeyName(code, mod) {
|
|
7058
|
+
const CTRL_LETTERS = {
|
|
7059
|
+
97: "ctrl-a",
|
|
7060
|
+
98: "ctrl-b",
|
|
7061
|
+
99: "ctrl-c",
|
|
7062
|
+
100: "ctrl-d",
|
|
7063
|
+
101: "ctrl-e",
|
|
7064
|
+
102: "ctrl-f",
|
|
7065
|
+
103: "ctrl-g",
|
|
7066
|
+
107: "ctrl-k",
|
|
7067
|
+
108: "ctrl-l",
|
|
7068
|
+
110: "ctrl-n",
|
|
7069
|
+
111: "ctrl-o",
|
|
7070
|
+
112: "ctrl-p",
|
|
7071
|
+
114: "ctrl-r",
|
|
7072
|
+
115: "ctrl-s",
|
|
7073
|
+
116: "ctrl-t",
|
|
7074
|
+
117: "ctrl-u",
|
|
7075
|
+
118: "ctrl-v",
|
|
7076
|
+
119: "ctrl-w",
|
|
7077
|
+
121: "ctrl-y"
|
|
7078
|
+
};
|
|
7079
|
+
if (mod === 5) {
|
|
7080
|
+
return CTRL_LETTERS[code] ?? null;
|
|
7081
|
+
}
|
|
7082
|
+
if (code === 27) {
|
|
7083
|
+
return "escape";
|
|
7084
|
+
}
|
|
7085
|
+
if (code === 9) {
|
|
7086
|
+
if (mod === 2) {
|
|
7087
|
+
return "shift-tab";
|
|
7088
|
+
}
|
|
7089
|
+
if (mod === 1) {
|
|
7090
|
+
return "tab";
|
|
7091
|
+
}
|
|
7092
|
+
return null;
|
|
7093
|
+
}
|
|
7094
|
+
if (code === 13) {
|
|
7095
|
+
if (mod === 2) {
|
|
7096
|
+
return "shift-enter";
|
|
7097
|
+
}
|
|
7098
|
+
if (mod === 3) {
|
|
7099
|
+
return "alt-enter";
|
|
7100
|
+
}
|
|
7101
|
+
if (mod === 5) {
|
|
7102
|
+
return "ctrl-enter";
|
|
7103
|
+
}
|
|
7104
|
+
if (mod === 1) {
|
|
7105
|
+
return "enter";
|
|
7106
|
+
}
|
|
7107
|
+
return null;
|
|
7108
|
+
}
|
|
7109
|
+
if (code === 127 && mod === 1) {
|
|
7110
|
+
return "backspace";
|
|
7111
|
+
}
|
|
7112
|
+
if (mod === 3) {
|
|
7113
|
+
if (code === 98 || code === 66) {
|
|
7114
|
+
return "alt-b";
|
|
7115
|
+
}
|
|
7116
|
+
if (code === 102 || code === 70) {
|
|
7117
|
+
return "alt-f";
|
|
7118
|
+
}
|
|
7119
|
+
return null;
|
|
7120
|
+
}
|
|
7121
|
+
return null;
|
|
7122
|
+
}
|
|
6810
7123
|
var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_HELP_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
6811
7124
|
var init_screen = __esm({
|
|
6812
7125
|
"src/tui/screen.ts"() {
|
|
@@ -6935,19 +7248,34 @@ var init_screen = __esm({
|
|
|
6935
7248
|
rawStdinHandler;
|
|
6936
7249
|
mouseEnabled;
|
|
6937
7250
|
progressIndicatorEnabled;
|
|
7251
|
+
// Listeners registered on process via installEmergencyCleanup so an
|
|
7252
|
+
// ungraceful exit (SIGTERM, SIGHUP, uncaughtException) still restores
|
|
7253
|
+
// mouse capture / alt-screen / kitty stack / cursor visibility — the
|
|
7254
|
+
// graceful stop() path isn't guaranteed to run in those cases and
|
|
7255
|
+
// would otherwise leave the host terminal wedged.
|
|
7256
|
+
emergencyCleanupInstalled = false;
|
|
7257
|
+
onProcessExit = null;
|
|
7258
|
+
onProcessSignal = null;
|
|
7259
|
+
onProcessUncaught = null;
|
|
6938
7260
|
// Last OSC 9;4 state we wrote (3 = indeterminate, 0 = remove). Used to
|
|
6939
7261
|
// suppress redundant writes when setBanner runs but `status` didn't
|
|
6940
7262
|
// actually change, and to re-emit on start() if a picker round-trip
|
|
6941
7263
|
// cleared the host terminal's indicator.
|
|
6942
7264
|
lastProgressState = 0;
|
|
7265
|
+
// View-only mode. Set once at construction. When true, promptRows()
|
|
7266
|
+
// returns 0 (composer collapses, scrollback expands), drawPrompt()
|
|
7267
|
+
// bails before computing layout, and syncWindowTitle() appends
|
|
7268
|
+
// "[VIEW ONLY]" so the chrome makes the mode obvious.
|
|
7269
|
+
readonly;
|
|
6943
7270
|
constructor(opts) {
|
|
6944
7271
|
this.term = opts.term;
|
|
6945
7272
|
this.dispatcher = opts.dispatcher;
|
|
6946
7273
|
this.onKey = opts.onKey;
|
|
6947
7274
|
this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
|
|
6948
7275
|
this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
|
|
6949
|
-
this.mouseEnabled = opts.mouse ??
|
|
7276
|
+
this.mouseEnabled = opts.mouse ?? false;
|
|
6950
7277
|
this.progressIndicatorEnabled = opts.progressIndicator ?? true;
|
|
7278
|
+
this.readonly = opts.readonly ?? false;
|
|
6951
7279
|
this.resizeHandler = () => this.repaint();
|
|
6952
7280
|
this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
|
|
6953
7281
|
this.mouseHandler = (name) => this.handleMouse(name);
|
|
@@ -6976,6 +7304,7 @@ var init_screen = __esm({
|
|
|
6976
7304
|
}
|
|
6977
7305
|
this.term.on("resize", this.resizeHandler);
|
|
6978
7306
|
this.installBracketedPaste();
|
|
7307
|
+
this.installEmergencyCleanup();
|
|
6979
7308
|
this.lastProgressState = 0;
|
|
6980
7309
|
this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
|
|
6981
7310
|
this.repaint();
|
|
@@ -6993,6 +7322,7 @@ var init_screen = __esm({
|
|
|
6993
7322
|
this.throttledRepaintTimer = null;
|
|
6994
7323
|
}
|
|
6995
7324
|
this.uninstallBracketedPaste();
|
|
7325
|
+
this.uninstallEmergencyCleanup();
|
|
6996
7326
|
this.term.off("key", this.keyHandler);
|
|
6997
7327
|
if (this.mouseEnabled) {
|
|
6998
7328
|
this.term.off("mouse", this.mouseHandler);
|
|
@@ -7028,6 +7358,8 @@ var init_screen = __esm({
|
|
|
7028
7358
|
process.stdout.write("\x1B[>4;0m");
|
|
7029
7359
|
process.stdout.write("\x1B[>5;0m");
|
|
7030
7360
|
process.stdout.write("\x1B[<u");
|
|
7361
|
+
process.stdout.write("\x1B[?1l");
|
|
7362
|
+
process.stdout.write("\x1B>");
|
|
7031
7363
|
const t = this.term;
|
|
7032
7364
|
if (!t.stdin || this.terminalKitStdinHandler === null) {
|
|
7033
7365
|
return;
|
|
@@ -7038,72 +7370,109 @@ var init_screen = __esm({
|
|
|
7038
7370
|
this.pasteActive = false;
|
|
7039
7371
|
this.pasteBuffer = "";
|
|
7040
7372
|
}
|
|
7373
|
+
installEmergencyCleanup() {
|
|
7374
|
+
if (this.emergencyCleanupInstalled) {
|
|
7375
|
+
return;
|
|
7376
|
+
}
|
|
7377
|
+
this.emergencyCleanupInstalled = true;
|
|
7378
|
+
this.onProcessExit = () => emergencyTerminalReset();
|
|
7379
|
+
this.onProcessSignal = (sig) => {
|
|
7380
|
+
emergencyTerminalReset();
|
|
7381
|
+
process.off(sig, this.onProcessSignal);
|
|
7382
|
+
process.kill(process.pid, sig);
|
|
7383
|
+
};
|
|
7384
|
+
this.onProcessUncaught = (err) => {
|
|
7385
|
+
emergencyTerminalReset();
|
|
7386
|
+
process.stderr.write(`
|
|
7387
|
+
uncaught: ${err.stack ?? err.message}
|
|
7388
|
+
`);
|
|
7389
|
+
process.exit(1);
|
|
7390
|
+
};
|
|
7391
|
+
process.on("exit", this.onProcessExit);
|
|
7392
|
+
process.on("SIGTERM", this.onProcessSignal);
|
|
7393
|
+
process.on("SIGHUP", this.onProcessSignal);
|
|
7394
|
+
process.on("uncaughtException", this.onProcessUncaught);
|
|
7395
|
+
}
|
|
7396
|
+
uninstallEmergencyCleanup() {
|
|
7397
|
+
if (!this.emergencyCleanupInstalled) {
|
|
7398
|
+
return;
|
|
7399
|
+
}
|
|
7400
|
+
this.emergencyCleanupInstalled = false;
|
|
7401
|
+
if (this.onProcessExit) {
|
|
7402
|
+
process.off("exit", this.onProcessExit);
|
|
7403
|
+
this.onProcessExit = null;
|
|
7404
|
+
}
|
|
7405
|
+
if (this.onProcessSignal) {
|
|
7406
|
+
process.off("SIGTERM", this.onProcessSignal);
|
|
7407
|
+
process.off("SIGHUP", this.onProcessSignal);
|
|
7408
|
+
this.onProcessSignal = null;
|
|
7409
|
+
}
|
|
7410
|
+
if (this.onProcessUncaught) {
|
|
7411
|
+
process.off("uncaughtException", this.onProcessUncaught);
|
|
7412
|
+
this.onProcessUncaught = null;
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7041
7415
|
handleRawStdin(chunk) {
|
|
7042
|
-
|
|
7043
|
-
if (
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
if (i < parts.length - 1) {
|
|
7062
|
-
this.onKey([{ type: "key", name }]);
|
|
7063
|
-
}
|
|
7416
|
+
const text = chunk.toString("binary");
|
|
7417
|
+
if (this.pasteActive) {
|
|
7418
|
+
this.handleRawStdinSegment(text);
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
const legacyMarkers = [
|
|
7422
|
+
{ seq: "\x1B[27;2;13~", name: "shift-enter" },
|
|
7423
|
+
{ seq: "\x1B[27;5;13~", name: "ctrl-enter" },
|
|
7424
|
+
// Bare LF — universal fallback for terminals without
|
|
7425
|
+
// modifyOtherKeys / kitty protocol. Last so the longer escape
|
|
7426
|
+
// sequences match first and we don't double-fire.
|
|
7427
|
+
{ seq: "\n", name: "ctrl-enter" }
|
|
7428
|
+
];
|
|
7429
|
+
for (const { seq, name } of legacyMarkers) {
|
|
7430
|
+
if (text.includes(seq)) {
|
|
7431
|
+
const parts = text.split(seq);
|
|
7432
|
+
for (let i = 0; i < parts.length; i++) {
|
|
7433
|
+
if (parts[i].length > 0) {
|
|
7434
|
+
this.handleRawStdin(Buffer.from(parts[i], "binary"));
|
|
7064
7435
|
}
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
}
|
|
7068
|
-
const csiUCtrlMap = {
|
|
7069
|
-
97: "ctrl-a",
|
|
7070
|
-
98: "ctrl-b",
|
|
7071
|
-
99: "ctrl-c",
|
|
7072
|
-
100: "ctrl-d",
|
|
7073
|
-
101: "ctrl-e",
|
|
7074
|
-
102: "ctrl-f",
|
|
7075
|
-
103: "ctrl-g",
|
|
7076
|
-
107: "ctrl-k",
|
|
7077
|
-
108: "ctrl-l",
|
|
7078
|
-
110: "ctrl-n",
|
|
7079
|
-
111: "ctrl-o",
|
|
7080
|
-
112: "ctrl-p",
|
|
7081
|
-
114: "ctrl-r",
|
|
7082
|
-
115: "ctrl-s",
|
|
7083
|
-
116: "ctrl-t",
|
|
7084
|
-
117: "ctrl-u",
|
|
7085
|
-
118: "ctrl-v",
|
|
7086
|
-
119: "ctrl-w",
|
|
7087
|
-
121: "ctrl-y"
|
|
7088
|
-
};
|
|
7089
|
-
const csiUCtrlRe = /\x1b\[(\d+);5u/;
|
|
7090
|
-
const m = csiUCtrlRe.exec(text);
|
|
7091
|
-
if (m !== null) {
|
|
7092
|
-
const keyName = csiUCtrlMap[parseInt(m[1], 10)];
|
|
7093
|
-
if (keyName !== void 0) {
|
|
7094
|
-
const parts = text.split(m[0]);
|
|
7095
|
-
for (let i = 0; i < parts.length; i++) {
|
|
7096
|
-
if (parts[i].length > 0)
|
|
7097
|
-
this.handleRawStdin(Buffer.from(parts[i], "binary"));
|
|
7098
|
-
if (i < parts.length - 1)
|
|
7099
|
-
this.onKey([{ type: "key", name: keyName }]);
|
|
7436
|
+
if (i < parts.length - 1) {
|
|
7437
|
+
this.onKey([{ type: "key", name }]);
|
|
7100
7438
|
}
|
|
7101
|
-
return;
|
|
7102
7439
|
}
|
|
7440
|
+
return;
|
|
7103
7441
|
}
|
|
7104
7442
|
}
|
|
7443
|
+
if (text.includes("\x1B[") && /\x1b\[\d+(?:;\d+)?u/.test(text)) {
|
|
7444
|
+
this.handleCsiUStdin(text);
|
|
7445
|
+
return;
|
|
7446
|
+
}
|
|
7105
7447
|
this.handleRawStdinSegment(text);
|
|
7106
7448
|
}
|
|
7449
|
+
// Walk `text` extracting every kitty CSI-u sequence. Each non-CSI-u
|
|
7450
|
+
// span is recursed back into handleRawStdin so paste markers and
|
|
7451
|
+
// legacy-modifyOtherKeys sequences in the same chunk still get
|
|
7452
|
+
// handled; each matched CSI-u is mapped to a KeyEvent (or dropped if
|
|
7453
|
+
// unmapped). Caller has already verified at least one match exists.
|
|
7454
|
+
handleCsiUStdin(text) {
|
|
7455
|
+
const csiU = /\x1b\[(\d+)(?:;(\d+))?u/g;
|
|
7456
|
+
let lastEnd = 0;
|
|
7457
|
+
let m;
|
|
7458
|
+
while ((m = csiU.exec(text)) !== null) {
|
|
7459
|
+
if (m.index > lastEnd) {
|
|
7460
|
+
this.handleRawStdin(
|
|
7461
|
+
Buffer.from(text.slice(lastEnd, m.index), "binary")
|
|
7462
|
+
);
|
|
7463
|
+
}
|
|
7464
|
+
const code = parseInt(m[1], 10);
|
|
7465
|
+
const mod = m[2] !== void 0 ? parseInt(m[2], 10) : 1;
|
|
7466
|
+
const name = mapCsiUToKeyName(code, mod);
|
|
7467
|
+
if (name !== null) {
|
|
7468
|
+
this.onKey([{ type: "key", name }]);
|
|
7469
|
+
}
|
|
7470
|
+
lastEnd = m.index + m[0].length;
|
|
7471
|
+
}
|
|
7472
|
+
if (lastEnd < text.length) {
|
|
7473
|
+
this.handleRawStdin(Buffer.from(text.slice(lastEnd), "binary"));
|
|
7474
|
+
}
|
|
7475
|
+
}
|
|
7107
7476
|
// Inner stdin-segment handler — paste-marker detection and forwarding
|
|
7108
7477
|
// to terminal-kit. Split out so shift-enter interception can call it
|
|
7109
7478
|
// for the non-shift-enter portions of a mixed chunk.
|
|
@@ -7360,7 +7729,8 @@ var init_screen = __esm({
|
|
|
7360
7729
|
const title = this.sessionbar.title?.trim();
|
|
7361
7730
|
const fallback = shortId(this.sessionbar.sessionId) || "hydra";
|
|
7362
7731
|
const raw = title && title.length > 0 ? title : fallback;
|
|
7363
|
-
const
|
|
7732
|
+
const tagged = this.readonly ? `${raw} [VIEW ONLY]` : raw;
|
|
7733
|
+
const clean = tagged.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
7364
7734
|
if (clean === this.lastWindowTitle) {
|
|
7365
7735
|
return;
|
|
7366
7736
|
}
|
|
@@ -7420,6 +7790,57 @@ var init_screen = __esm({
|
|
|
7420
7790
|
this.drawBanner();
|
|
7421
7791
|
this.placeCursor();
|
|
7422
7792
|
}
|
|
7793
|
+
// Runtime toggle for terminal mouse capture. With capture on, the
|
|
7794
|
+
// wheel drives scrollback but text selection requires shift+drag
|
|
7795
|
+
// (terminals route mouse events to the app). With capture off, plain
|
|
7796
|
+
// click-drag selects text but the wheel does nothing in the app —
|
|
7797
|
+
// use PgUp/PgDn for scrollback instead. Bound to ^X so users can
|
|
7798
|
+
// flip on demand without a config reload + restart. Idempotent.
|
|
7799
|
+
//
|
|
7800
|
+
// Re-issuing grabInput() reinstalls terminal-kit's own stdin "data"
|
|
7801
|
+
// listener, so we have to redo the same listener swap that
|
|
7802
|
+
// installBracketedPaste() did at startup — otherwise our raw handler
|
|
7803
|
+
// and terminal-kit's both fire for every keystroke (each character
|
|
7804
|
+
// appears twice in the prompt).
|
|
7805
|
+
setMouseEnabled(enabled) {
|
|
7806
|
+
if (this.mouseEnabled === enabled) {
|
|
7807
|
+
return;
|
|
7808
|
+
}
|
|
7809
|
+
this.mouseEnabled = enabled;
|
|
7810
|
+
if (!this.started) {
|
|
7811
|
+
return;
|
|
7812
|
+
}
|
|
7813
|
+
if (enabled) {
|
|
7814
|
+
this.term.grabInput({ mouse: "button" });
|
|
7815
|
+
this.term.on("mouse", this.mouseHandler);
|
|
7816
|
+
} else {
|
|
7817
|
+
this.term.off("mouse", this.mouseHandler);
|
|
7818
|
+
this.term.grabInput(true);
|
|
7819
|
+
}
|
|
7820
|
+
this.reclaimStdinAfterGrabInput();
|
|
7821
|
+
}
|
|
7822
|
+
// After a grabInput() re-issue, terminal-kit has put its own "data"
|
|
7823
|
+
// listener back on stdin. Pull it back off and reinstall hydra's
|
|
7824
|
+
// rawStdinHandler — keeping the captured terminal-kit handler so our
|
|
7825
|
+
// bracketed-paste extractor can still delegate non-paste bytes to it.
|
|
7826
|
+
// No-op if installBracketedPaste() hasn't run yet (start() does it
|
|
7827
|
+
// before any toggle path can reach here).
|
|
7828
|
+
reclaimStdinAfterGrabInput() {
|
|
7829
|
+
if (this.terminalKitStdinHandler === null) {
|
|
7830
|
+
return;
|
|
7831
|
+
}
|
|
7832
|
+
const t = this.term;
|
|
7833
|
+
if (!t.stdin || typeof t.onStdin !== "function") {
|
|
7834
|
+
return;
|
|
7835
|
+
}
|
|
7836
|
+
this.terminalKitStdinHandler = t.onStdin;
|
|
7837
|
+
t.stdin.removeListener("data", t.onStdin);
|
|
7838
|
+
t.stdin.removeListener("data", this.rawStdinHandler);
|
|
7839
|
+
t.stdin.on("data", this.rawStdinHandler);
|
|
7840
|
+
}
|
|
7841
|
+
isMouseEnabled() {
|
|
7842
|
+
return this.mouseEnabled;
|
|
7843
|
+
}
|
|
7423
7844
|
// Pushed by the app each onKey tick to reflect prompt-history
|
|
7424
7845
|
// reverse-search state in the banner — the only place that mode's
|
|
7425
7846
|
// query is visible. Pass null when not searching.
|
|
@@ -8243,6 +8664,9 @@ var init_screen = __esm({
|
|
|
8243
8664
|
this.drawHelpPrompt();
|
|
8244
8665
|
return;
|
|
8245
8666
|
}
|
|
8667
|
+
if (this.readonly) {
|
|
8668
|
+
return;
|
|
8669
|
+
}
|
|
8246
8670
|
const w = this.term.width;
|
|
8247
8671
|
const room = Math.max(1, w - 2);
|
|
8248
8672
|
const state = this.dispatcher.state();
|
|
@@ -8493,6 +8917,9 @@ var init_screen = __esm({
|
|
|
8493
8917
|
if (this.helpPrompt) {
|
|
8494
8918
|
return this.helpRows();
|
|
8495
8919
|
}
|
|
8920
|
+
if (this.readonly) {
|
|
8921
|
+
return 0;
|
|
8922
|
+
}
|
|
8496
8923
|
const w = this.term.width;
|
|
8497
8924
|
const room = Math.max(1, w - 2);
|
|
8498
8925
|
const state = this.dispatcher.state();
|
|
@@ -8901,6 +9328,8 @@ var init_input = __esm({
|
|
|
8901
9328
|
case "ctrl-w":
|
|
8902
9329
|
this.killWord();
|
|
8903
9330
|
return [];
|
|
9331
|
+
case "ctrl-x":
|
|
9332
|
+
return [{ type: "toggle-mouse" }];
|
|
8904
9333
|
case "ctrl-y":
|
|
8905
9334
|
this.yank();
|
|
8906
9335
|
return [];
|
|
@@ -9483,9 +9912,9 @@ var init_input = __esm({
|
|
|
9483
9912
|
|
|
9484
9913
|
// src/tui/clipboard.ts
|
|
9485
9914
|
import { spawn as nodeSpawn } from "child_process";
|
|
9486
|
-
import
|
|
9487
|
-
import
|
|
9488
|
-
import
|
|
9915
|
+
import fs19 from "fs/promises";
|
|
9916
|
+
import os5 from "os";
|
|
9917
|
+
import path14 from "path";
|
|
9489
9918
|
async function readClipboard(envIn = {}) {
|
|
9490
9919
|
const env = { ...defaultEnv, ...envIn };
|
|
9491
9920
|
if (env.platform === "darwin") {
|
|
@@ -9500,7 +9929,7 @@ async function readClipboard(envIn = {}) {
|
|
|
9500
9929
|
};
|
|
9501
9930
|
}
|
|
9502
9931
|
async function readMacOS(env) {
|
|
9503
|
-
const tmpPath =
|
|
9932
|
+
const tmpPath = path14.join(
|
|
9504
9933
|
env.tmpdir(),
|
|
9505
9934
|
`hydra-clipboard-${Date.now()}-${process.pid}.png`
|
|
9506
9935
|
);
|
|
@@ -9524,7 +9953,7 @@ async function readMacOS(env) {
|
|
|
9524
9953
|
return img;
|
|
9525
9954
|
}
|
|
9526
9955
|
} catch {
|
|
9527
|
-
await
|
|
9956
|
+
await fs19.unlink(tmpPath).catch(() => void 0);
|
|
9528
9957
|
}
|
|
9529
9958
|
try {
|
|
9530
9959
|
const buf = await runCapture(env.spawn, "pbpaste", []);
|
|
@@ -9639,9 +10068,9 @@ async function which(env, cmd) {
|
|
|
9639
10068
|
}
|
|
9640
10069
|
async function readFileAsAttachment(p, unlinkAfter) {
|
|
9641
10070
|
try {
|
|
9642
|
-
const buf = await
|
|
10071
|
+
const buf = await fs19.readFile(p);
|
|
9643
10072
|
if (unlinkAfter) {
|
|
9644
|
-
await
|
|
10073
|
+
await fs19.unlink(p).catch(() => void 0);
|
|
9645
10074
|
}
|
|
9646
10075
|
if (buf.length === 0) {
|
|
9647
10076
|
return { ok: false, reason: "no image on clipboard" };
|
|
@@ -9667,14 +10096,14 @@ async function readFileAsAttachment(p, unlinkAfter) {
|
|
|
9667
10096
|
}
|
|
9668
10097
|
}
|
|
9669
10098
|
function run2(spawn6, cmd, args) {
|
|
9670
|
-
return new Promise((
|
|
10099
|
+
return new Promise((resolve6, reject) => {
|
|
9671
10100
|
const proc = spawn6(cmd, args);
|
|
9672
10101
|
proc.stdout?.on("data", () => void 0);
|
|
9673
10102
|
proc.stderr?.on("data", () => void 0);
|
|
9674
10103
|
proc.on("error", reject);
|
|
9675
10104
|
proc.on("close", (code) => {
|
|
9676
10105
|
if (code === 0) {
|
|
9677
|
-
|
|
10106
|
+
resolve6();
|
|
9678
10107
|
} else {
|
|
9679
10108
|
reject(new Error(`${cmd} exited ${code}`));
|
|
9680
10109
|
}
|
|
@@ -9682,7 +10111,7 @@ function run2(spawn6, cmd, args) {
|
|
|
9682
10111
|
});
|
|
9683
10112
|
}
|
|
9684
10113
|
function runCapture(spawn6, cmd, args) {
|
|
9685
|
-
return new Promise((
|
|
10114
|
+
return new Promise((resolve6, reject) => {
|
|
9686
10115
|
const proc = spawn6(cmd, args);
|
|
9687
10116
|
const chunks = [];
|
|
9688
10117
|
let stdoutEnded = proc.stdout === null;
|
|
@@ -9694,7 +10123,7 @@ function runCapture(spawn6, cmd, args) {
|
|
|
9694
10123
|
}
|
|
9695
10124
|
settled = true;
|
|
9696
10125
|
if (closedCode === 0) {
|
|
9697
|
-
|
|
10126
|
+
resolve6(Buffer.concat(chunks));
|
|
9698
10127
|
} else {
|
|
9699
10128
|
reject(new Error(`${cmd} exited ${closedCode}`));
|
|
9700
10129
|
}
|
|
@@ -9729,7 +10158,7 @@ var init_clipboard = __esm({
|
|
|
9729
10158
|
platform: process.platform,
|
|
9730
10159
|
env: process.env,
|
|
9731
10160
|
spawn: nodeSpawn,
|
|
9732
|
-
tmpdir:
|
|
10161
|
+
tmpdir: os5.tmpdir
|
|
9733
10162
|
};
|
|
9734
10163
|
SUPPORTED_IMAGE_MIMES = [
|
|
9735
10164
|
"image/png",
|
|
@@ -10246,8 +10675,21 @@ var init_format = __esm({
|
|
|
10246
10675
|
import { appendFileSync, statSync, renameSync } from "fs";
|
|
10247
10676
|
import { nanoid as nanoid3 } from "nanoid";
|
|
10248
10677
|
import termkit from "terminal-kit";
|
|
10249
|
-
import
|
|
10250
|
-
import
|
|
10678
|
+
import fs20 from "fs/promises";
|
|
10679
|
+
import path15 from "path";
|
|
10680
|
+
function isReadonlyForbiddenEffect(effect) {
|
|
10681
|
+
switch (effect.type) {
|
|
10682
|
+
case "send":
|
|
10683
|
+
case "amend":
|
|
10684
|
+
case "queue-edit":
|
|
10685
|
+
case "queue-remove":
|
|
10686
|
+
case "plan-toggle":
|
|
10687
|
+
case "attachment-request":
|
|
10688
|
+
return true;
|
|
10689
|
+
default:
|
|
10690
|
+
return false;
|
|
10691
|
+
}
|
|
10692
|
+
}
|
|
10251
10693
|
async function runTuiApp(opts) {
|
|
10252
10694
|
const config = await loadConfig();
|
|
10253
10695
|
const serviceToken = await ensureServiceToken();
|
|
@@ -10266,8 +10708,11 @@ async function runTuiApp(opts) {
|
|
|
10266
10708
|
}
|
|
10267
10709
|
if (exitHint.sessionId) {
|
|
10268
10710
|
const short = stripHydraSessionPrefix(exitHint.sessionId);
|
|
10269
|
-
|
|
10270
|
-
|
|
10711
|
+
const flags = exitHint.readonly ? " --readonly" : "";
|
|
10712
|
+
process.stdout.write(
|
|
10713
|
+
`To resume: hydra-acp tui --resume ${short}${flags}
|
|
10714
|
+
`
|
|
10715
|
+
);
|
|
10271
10716
|
}
|
|
10272
10717
|
}
|
|
10273
10718
|
async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
@@ -10559,10 +11004,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10559
11004
|
if (pendingPermission.toolCallId && toolCallId && pendingPermission.toolCallId !== toolCallId) {
|
|
10560
11005
|
return;
|
|
10561
11006
|
}
|
|
10562
|
-
const
|
|
11007
|
+
const resolve6 = pendingPermission.resolve;
|
|
10563
11008
|
pendingPermission = null;
|
|
10564
11009
|
screen.setPermissionPrompt(null);
|
|
10565
|
-
|
|
11010
|
+
resolve6(result ?? { outcome: { outcome: "cancelled" } });
|
|
10566
11011
|
};
|
|
10567
11012
|
const maybeDismissPermissionByToolUpdate = (update) => {
|
|
10568
11013
|
if (!pendingPermission?.toolCallId) {
|
|
@@ -10595,14 +11040,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10595
11040
|
if (!pendingPermission) {
|
|
10596
11041
|
return;
|
|
10597
11042
|
}
|
|
10598
|
-
const { options, resolve:
|
|
11043
|
+
const { options, resolve: resolve6 } = pendingPermission;
|
|
10599
11044
|
pendingPermission = null;
|
|
10600
11045
|
screen.setPermissionPrompt(null);
|
|
10601
11046
|
if (optionId === null) {
|
|
10602
|
-
|
|
11047
|
+
resolve6({ outcome: { outcome: "cancelled" } });
|
|
10603
11048
|
return;
|
|
10604
11049
|
}
|
|
10605
|
-
|
|
11050
|
+
resolve6({ outcome: { outcome: "selected", optionId } });
|
|
10606
11051
|
void options;
|
|
10607
11052
|
};
|
|
10608
11053
|
conn.onRequest("session/request_permission", async (params) => {
|
|
@@ -10629,12 +11074,12 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10629
11074
|
]);
|
|
10630
11075
|
return { outcome: { outcome: "cancelled" } };
|
|
10631
11076
|
}
|
|
10632
|
-
return new Promise((
|
|
11077
|
+
return new Promise((resolve6) => {
|
|
10633
11078
|
pendingPermission = {
|
|
10634
11079
|
title,
|
|
10635
11080
|
options,
|
|
10636
11081
|
selectedIndex: 0,
|
|
10637
|
-
resolve:
|
|
11082
|
+
resolve: resolve6,
|
|
10638
11083
|
toolCallId
|
|
10639
11084
|
};
|
|
10640
11085
|
refreshPermissionPrompt();
|
|
@@ -10695,6 +11140,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10695
11140
|
ownClientId = created.clientId;
|
|
10696
11141
|
}
|
|
10697
11142
|
exitHint.sessionId = resolvedSessionId;
|
|
11143
|
+
exitHint.readonly = false;
|
|
10698
11144
|
const hydraMeta = extractHydraMeta(created._meta ?? void 0);
|
|
10699
11145
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
10700
11146
|
if (hydraMeta.agentId) {
|
|
@@ -10721,13 +11167,31 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10721
11167
|
const attached = await conn.request("session/attach", {
|
|
10722
11168
|
sessionId: ctx.sessionId,
|
|
10723
11169
|
historyPolicy: "full",
|
|
10724
|
-
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
11170
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
|
|
11171
|
+
...opts.readonly === true ? { readonly: true } : {},
|
|
11172
|
+
// Forward the user-chosen cwd for first-launch imported sessions
|
|
11173
|
+
// via a full resume hint. upstreamSessionId is empty so the
|
|
11174
|
+
// daemon routes through doResurrectFromImport (session-manager.ts)
|
|
11175
|
+
// with the user-supplied cwd instead of silently falling back to
|
|
11176
|
+
// $HOME in resolveImportCwd.
|
|
11177
|
+
...ctx.importAttachHint !== void 0 ? {
|
|
11178
|
+
_meta: {
|
|
11179
|
+
[HYDRA_META_KEY]: {
|
|
11180
|
+
resume: {
|
|
11181
|
+
upstreamSessionId: "",
|
|
11182
|
+
agentId: ctx.importAttachHint.agentId,
|
|
11183
|
+
cwd: ctx.importAttachHint.cwd
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
11186
|
+
}
|
|
11187
|
+
} : {}
|
|
10725
11188
|
});
|
|
10726
11189
|
resolvedSessionId = attached.sessionId;
|
|
10727
11190
|
if (attached.clientId) {
|
|
10728
11191
|
ownClientId = attached.clientId;
|
|
10729
11192
|
}
|
|
10730
11193
|
exitHint.sessionId = resolvedSessionId;
|
|
11194
|
+
exitHint.readonly = opts.readonly === true;
|
|
10731
11195
|
const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
|
|
10732
11196
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
10733
11197
|
if (hydraMeta.agentId) {
|
|
@@ -10767,6 +11231,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10767
11231
|
maxScrollbackLines: config.tui.maxScrollbackLines,
|
|
10768
11232
|
mouse: config.tui.mouse,
|
|
10769
11233
|
progressIndicator: config.tui.progressIndicator,
|
|
11234
|
+
readonly: opts.readonly === true,
|
|
10770
11235
|
onKey: (events) => {
|
|
10771
11236
|
for (const ev of events) {
|
|
10772
11237
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
@@ -10790,6 +11255,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10790
11255
|
}
|
|
10791
11256
|
const effects = dispatcher.feed(ev);
|
|
10792
11257
|
for (const effect of effects) {
|
|
11258
|
+
if (opts.readonly === true && isReadonlyForbiddenEffect(effect)) {
|
|
11259
|
+
continue;
|
|
11260
|
+
}
|
|
10793
11261
|
handleEffect(effect);
|
|
10794
11262
|
}
|
|
10795
11263
|
}
|
|
@@ -10977,8 +11445,8 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10977
11445
|
}
|
|
10978
11446
|
});
|
|
10979
11447
|
let finishSession = null;
|
|
10980
|
-
const sessionDone = new Promise((
|
|
10981
|
-
finishSession =
|
|
11448
|
+
const sessionDone = new Promise((resolve6) => {
|
|
11449
|
+
finishSession = resolve6;
|
|
10982
11450
|
});
|
|
10983
11451
|
const cancelRemoteTurn = () => {
|
|
10984
11452
|
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
@@ -11152,13 +11620,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11152
11620
|
if (choice.kind === "new") {
|
|
11153
11621
|
const { sessionId: _drop, ...rest } = opts;
|
|
11154
11622
|
void _drop;
|
|
11155
|
-
resume({ ...rest, cwd: resolvedCwd, forceNew: true });
|
|
11623
|
+
resume({ ...rest, cwd: resolvedCwd, forceNew: true, readonly: false });
|
|
11156
11624
|
return;
|
|
11157
11625
|
}
|
|
11158
11626
|
const nextOpts = {
|
|
11159
11627
|
...opts,
|
|
11160
11628
|
sessionId: choice.sessionId,
|
|
11161
|
-
cwd: resolvedCwd
|
|
11629
|
+
cwd: resolvedCwd,
|
|
11630
|
+
readonly: choice.readonly === true
|
|
11162
11631
|
};
|
|
11163
11632
|
if (choice.agentId !== void 0) {
|
|
11164
11633
|
nextOpts.agentId = choice.agentId;
|
|
@@ -11178,7 +11647,12 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11178
11647
|
finishSession = null;
|
|
11179
11648
|
process.off("SIGINT", sigintHandler);
|
|
11180
11649
|
void stream.close().catch(() => void 0);
|
|
11181
|
-
const nextOpts = {
|
|
11650
|
+
const nextOpts = {
|
|
11651
|
+
...opts,
|
|
11652
|
+
sessionId: next.sessionId,
|
|
11653
|
+
cwd: resolvedCwd,
|
|
11654
|
+
readonly: false
|
|
11655
|
+
};
|
|
11182
11656
|
if (next.agentId !== void 0)
|
|
11183
11657
|
nextOpts.agentId = next.agentId;
|
|
11184
11658
|
resume(nextOpts);
|
|
@@ -11289,6 +11763,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11289
11763
|
toolsExpanded = !toolsExpanded;
|
|
11290
11764
|
renderToolsBlock();
|
|
11291
11765
|
return;
|
|
11766
|
+
case "toggle-mouse": {
|
|
11767
|
+
const next = !screen.isMouseEnabled();
|
|
11768
|
+
screen.setMouseEnabled(next);
|
|
11769
|
+
screen.notify(
|
|
11770
|
+
next ? "mouse capture on \u2014 wheel scrolls; shift+drag to select text" : "mouse capture off \u2014 click-drag selects text; PgUp/PgDn scrolls"
|
|
11771
|
+
);
|
|
11772
|
+
return;
|
|
11773
|
+
}
|
|
11292
11774
|
case "show-help":
|
|
11293
11775
|
toggleHelpModal();
|
|
11294
11776
|
return;
|
|
@@ -11331,11 +11813,11 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11331
11813
|
}
|
|
11332
11814
|
const mimeType = mimeFromExtension(token);
|
|
11333
11815
|
if (!mimeType) {
|
|
11334
|
-
screen.notify(`unsupported image type: ${
|
|
11816
|
+
screen.notify(`unsupported image type: ${path15.basename(token)}`);
|
|
11335
11817
|
continue;
|
|
11336
11818
|
}
|
|
11337
11819
|
try {
|
|
11338
|
-
const buf = await
|
|
11820
|
+
const buf = await fs20.readFile(token);
|
|
11339
11821
|
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
11340
11822
|
screen.notify(
|
|
11341
11823
|
`image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
|
|
@@ -11345,13 +11827,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11345
11827
|
dispatcher.addAttachment({
|
|
11346
11828
|
mimeType,
|
|
11347
11829
|
data: buf.toString("base64"),
|
|
11348
|
-
name:
|
|
11830
|
+
name: path15.basename(token),
|
|
11349
11831
|
sizeBytes: buf.length
|
|
11350
11832
|
});
|
|
11351
11833
|
added++;
|
|
11352
11834
|
} catch (err) {
|
|
11353
11835
|
screen.notify(
|
|
11354
|
-
`cannot read ${
|
|
11836
|
+
`cannot read ${path15.basename(token)}: ${err.message}`
|
|
11355
11837
|
);
|
|
11356
11838
|
}
|
|
11357
11839
|
}
|
|
@@ -12068,10 +12550,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
12068
12550
|
}
|
|
12069
12551
|
const resetInFlightUiState = () => {
|
|
12070
12552
|
if (pendingPermission) {
|
|
12071
|
-
const
|
|
12553
|
+
const resolve6 = pendingPermission.resolve;
|
|
12072
12554
|
pendingPermission = null;
|
|
12073
12555
|
screen.setPermissionPrompt(null);
|
|
12074
|
-
|
|
12556
|
+
resolve6({ outcome: { outcome: "cancelled" } });
|
|
12075
12557
|
}
|
|
12076
12558
|
closeAgentText();
|
|
12077
12559
|
};
|
|
@@ -12235,6 +12717,22 @@ async function resolveSession(term, config, serviceToken, opts) {
|
|
|
12235
12717
|
if (choice.kind === "new") {
|
|
12236
12718
|
return newCtx(opts, cwd, config);
|
|
12237
12719
|
}
|
|
12720
|
+
opts.readonly = choice.readonly === true;
|
|
12721
|
+
const chosen = sessions.find((s) => s.sessionId === choice.sessionId);
|
|
12722
|
+
const isImportedFirstLaunch = chosen !== void 0 && !!chosen.importedFromMachine && !chosen.upstreamSessionId && !opts.readonly;
|
|
12723
|
+
if (isImportedFirstLaunch) {
|
|
12724
|
+
const promptedCwd = await promptForImportCwd(term, chosen);
|
|
12725
|
+
if (promptedCwd === null) {
|
|
12726
|
+
return null;
|
|
12727
|
+
}
|
|
12728
|
+
const agentId = choice.agentId ?? chosen.agentId ?? "";
|
|
12729
|
+
return {
|
|
12730
|
+
sessionId: choice.sessionId,
|
|
12731
|
+
agentId,
|
|
12732
|
+
cwd: promptedCwd,
|
|
12733
|
+
importAttachHint: { agentId, cwd: promptedCwd }
|
|
12734
|
+
};
|
|
12735
|
+
}
|
|
12238
12736
|
return {
|
|
12239
12737
|
sessionId: choice.sessionId,
|
|
12240
12738
|
agentId: choice.agentId ?? "",
|
|
@@ -12363,8 +12861,8 @@ function createInstallStatusLine(term, baseLabel) {
|
|
|
12363
12861
|
}
|
|
12364
12862
|
function rotateIfBig(target) {
|
|
12365
12863
|
try {
|
|
12366
|
-
const
|
|
12367
|
-
if (
|
|
12864
|
+
const stat5 = statSync(target);
|
|
12865
|
+
if (stat5.size < logMaxBytes) {
|
|
12368
12866
|
return;
|
|
12369
12867
|
}
|
|
12370
12868
|
renameSync(target, `${target}.0`);
|
|
@@ -12388,6 +12886,7 @@ var init_app = __esm({
|
|
|
12388
12886
|
init_history();
|
|
12389
12887
|
init_discovery();
|
|
12390
12888
|
init_picker();
|
|
12889
|
+
init_import_cwd_prompt();
|
|
12391
12890
|
init_screen();
|
|
12392
12891
|
init_input();
|
|
12393
12892
|
init_attachments();
|
|
@@ -12417,6 +12916,7 @@ var init_app = __esm({
|
|
|
12417
12916
|
["^R / ^S", "history reverse / forward search"],
|
|
12418
12917
|
["PgUp / PgDn", "scroll scrollback"],
|
|
12419
12918
|
["Mouse wheel", "scroll scrollback (when mouse capture is on)"],
|
|
12919
|
+
["^X", "toggle mouse capture (wheel scroll vs. text selection)"],
|
|
12420
12920
|
null,
|
|
12421
12921
|
["^C", "cancel turn (twice to exit)"],
|
|
12422
12922
|
["Esc", "cancel turn and prefill draft"],
|
|
@@ -12443,7 +12943,7 @@ var init_tui = __esm({
|
|
|
12443
12943
|
// src/cli.ts
|
|
12444
12944
|
import { readFileSync as readFileSync2 } from "fs";
|
|
12445
12945
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12446
|
-
import { dirname as dirname6, resolve as
|
|
12946
|
+
import { dirname as dirname6, resolve as resolve5 } from "path";
|
|
12447
12947
|
|
|
12448
12948
|
// src/cli/parse-args.ts
|
|
12449
12949
|
var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -12454,6 +12954,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
12454
12954
|
"json",
|
|
12455
12955
|
"new",
|
|
12456
12956
|
"reattach",
|
|
12957
|
+
"readonly",
|
|
12457
12958
|
"replace",
|
|
12458
12959
|
"rotate-token",
|
|
12459
12960
|
"version"
|
|
@@ -12740,10 +13241,10 @@ async function downloadTo(args) {
|
|
|
12740
13241
|
logSink(formatProgress(args.agentId, received, total));
|
|
12741
13242
|
}
|
|
12742
13243
|
});
|
|
12743
|
-
await new Promise((
|
|
13244
|
+
await new Promise((resolve6, reject) => {
|
|
12744
13245
|
nodeStream.on("error", reject);
|
|
12745
13246
|
out.on("error", reject);
|
|
12746
|
-
out.on("finish", () =>
|
|
13247
|
+
out.on("finish", () => resolve6());
|
|
12747
13248
|
nodeStream.pipe(out);
|
|
12748
13249
|
});
|
|
12749
13250
|
logSink(formatProgress(
|
|
@@ -12795,14 +13296,14 @@ async function extract(archivePath, dest) {
|
|
|
12795
13296
|
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
12796
13297
|
}
|
|
12797
13298
|
function run(cmd, args) {
|
|
12798
|
-
return new Promise((
|
|
13299
|
+
return new Promise((resolve6, reject) => {
|
|
12799
13300
|
const child = spawn(cmd, args, {
|
|
12800
13301
|
stdio: ["ignore", "ignore", "inherit"]
|
|
12801
13302
|
});
|
|
12802
13303
|
child.on("error", reject);
|
|
12803
13304
|
child.on("exit", (code, signal) => {
|
|
12804
13305
|
if (code === 0) {
|
|
12805
|
-
|
|
13306
|
+
resolve6();
|
|
12806
13307
|
return;
|
|
12807
13308
|
}
|
|
12808
13309
|
reject(
|
|
@@ -12814,11 +13315,11 @@ function run(cmd, args) {
|
|
|
12814
13315
|
});
|
|
12815
13316
|
}
|
|
12816
13317
|
async function hasCommand(name) {
|
|
12817
|
-
return new Promise((
|
|
13318
|
+
return new Promise((resolve6) => {
|
|
12818
13319
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
12819
13320
|
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
12820
|
-
child.on("error", () =>
|
|
12821
|
-
child.on("exit", (code) =>
|
|
13321
|
+
child.on("error", () => resolve6(false));
|
|
13322
|
+
child.on("exit", (code) => resolve6(code === 0));
|
|
12822
13323
|
});
|
|
12823
13324
|
}
|
|
12824
13325
|
async function fileExists(p) {
|
|
@@ -12936,7 +13437,7 @@ function runNpmInstall(args) {
|
|
|
12936
13437
|
}
|
|
12937
13438
|
async function runNpmInstallOnce(args, attempt) {
|
|
12938
13439
|
try {
|
|
12939
|
-
await new Promise((
|
|
13440
|
+
await new Promise((resolve6, reject) => {
|
|
12940
13441
|
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
12941
13442
|
let child;
|
|
12942
13443
|
try {
|
|
@@ -12978,7 +13479,7 @@ async function runNpmInstallOnce(args, attempt) {
|
|
|
12978
13479
|
});
|
|
12979
13480
|
child.on("exit", (code, signal) => {
|
|
12980
13481
|
if (code === 0) {
|
|
12981
|
-
|
|
13482
|
+
resolve6();
|
|
12982
13483
|
return;
|
|
12983
13484
|
}
|
|
12984
13485
|
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
@@ -13306,13 +13807,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
13306
13807
|
throw new Error("stream is closed");
|
|
13307
13808
|
}
|
|
13308
13809
|
const line = JSON.stringify(message) + "\n";
|
|
13309
|
-
await new Promise((
|
|
13810
|
+
await new Promise((resolve6, reject) => {
|
|
13310
13811
|
stdin.write(line, (err) => {
|
|
13311
13812
|
if (err) {
|
|
13312
13813
|
reject(err);
|
|
13313
13814
|
return;
|
|
13314
13815
|
}
|
|
13315
|
-
|
|
13816
|
+
resolve6();
|
|
13316
13817
|
});
|
|
13317
13818
|
});
|
|
13318
13819
|
},
|
|
@@ -13827,8 +14328,8 @@ var SessionManager = class {
|
|
|
13827
14328
|
}
|
|
13828
14329
|
async resolveImportCwd(cwd) {
|
|
13829
14330
|
try {
|
|
13830
|
-
const
|
|
13831
|
-
if (
|
|
14331
|
+
const stat5 = await fs11.stat(cwd);
|
|
14332
|
+
if (stat5.isDirectory()) {
|
|
13832
14333
|
return cwd;
|
|
13833
14334
|
}
|
|
13834
14335
|
} catch {
|
|
@@ -13998,6 +14499,13 @@ var SessionManager = class {
|
|
|
13998
14499
|
}
|
|
13999
14500
|
return this.histories.load(sessionId).catch(() => []);
|
|
14000
14501
|
}
|
|
14502
|
+
// Read the on-disk history.jsonl for a session without constructing a
|
|
14503
|
+
// Session instance. Used by the daemon's read-only viewer attach path
|
|
14504
|
+
// (cli/src/daemon/acp-ws.ts) to stream replay events to a client for
|
|
14505
|
+
// a cold session without spawning an agent.
|
|
14506
|
+
async loadHistory(sessionId) {
|
|
14507
|
+
return this.histories.load(sessionId);
|
|
14508
|
+
}
|
|
14001
14509
|
async loadFromDisk(sessionId) {
|
|
14002
14510
|
const record = await this.store.read(sessionId);
|
|
14003
14511
|
if (!record) {
|
|
@@ -14744,9 +15252,9 @@ var ExtensionManager = class {
|
|
|
14744
15252
|
} catch {
|
|
14745
15253
|
}
|
|
14746
15254
|
tasks.push(
|
|
14747
|
-
new Promise((
|
|
15255
|
+
new Promise((resolve6) => {
|
|
14748
15256
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
14749
|
-
|
|
15257
|
+
resolve6();
|
|
14750
15258
|
return;
|
|
14751
15259
|
}
|
|
14752
15260
|
const timer = setTimeout(() => {
|
|
@@ -14754,11 +15262,11 @@ var ExtensionManager = class {
|
|
|
14754
15262
|
child.kill("SIGKILL");
|
|
14755
15263
|
} catch {
|
|
14756
15264
|
}
|
|
14757
|
-
|
|
15265
|
+
resolve6();
|
|
14758
15266
|
}, STOP_GRACE_MS);
|
|
14759
15267
|
child.on("exit", () => {
|
|
14760
15268
|
clearTimeout(timer);
|
|
14761
|
-
|
|
15269
|
+
resolve6();
|
|
14762
15270
|
});
|
|
14763
15271
|
})
|
|
14764
15272
|
);
|
|
@@ -14866,8 +15374,8 @@ var ExtensionManager = class {
|
|
|
14866
15374
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
14867
15375
|
return;
|
|
14868
15376
|
}
|
|
14869
|
-
const exited = new Promise((
|
|
14870
|
-
entry.exitWaiters.push(
|
|
15377
|
+
const exited = new Promise((resolve6) => {
|
|
15378
|
+
entry.exitWaiters.push(resolve6);
|
|
14871
15379
|
});
|
|
14872
15380
|
try {
|
|
14873
15381
|
child.kill("SIGTERM");
|
|
@@ -15064,8 +15572,8 @@ var ExtensionManager = class {
|
|
|
15064
15572
|
entry.pid = void 0;
|
|
15065
15573
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
15066
15574
|
const waiters = entry.exitWaiters.splice(0);
|
|
15067
|
-
for (const
|
|
15068
|
-
|
|
15575
|
+
for (const resolve6 of waiters) {
|
|
15576
|
+
resolve6();
|
|
15069
15577
|
}
|
|
15070
15578
|
if (this.stopping || entry.manuallyStopped) {
|
|
15071
15579
|
try {
|
|
@@ -15176,6 +15684,9 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
15176
15684
|
if (activeVersions.has(version)) {
|
|
15177
15685
|
continue;
|
|
15178
15686
|
}
|
|
15687
|
+
if (version.includes(".partial-")) {
|
|
15688
|
+
continue;
|
|
15689
|
+
}
|
|
15179
15690
|
const versionDir = path8.join(agentDir, version);
|
|
15180
15691
|
try {
|
|
15181
15692
|
await fsp4.rm(versionDir, { recursive: true, force: true });
|
|
@@ -16100,6 +16611,16 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16100
16611
|
}
|
|
16101
16612
|
state.attached.clear();
|
|
16102
16613
|
});
|
|
16614
|
+
const denyIfReadonly = (sessionId, method) => {
|
|
16615
|
+
const att = state.attached.get(sessionId);
|
|
16616
|
+
if (att?.readonly) {
|
|
16617
|
+
const err = new Error(
|
|
16618
|
+
`${method} not permitted on a read-only attachment`
|
|
16619
|
+
);
|
|
16620
|
+
err.code = JsonRpcErrorCodes.PermissionDenied;
|
|
16621
|
+
throw err;
|
|
16622
|
+
}
|
|
16623
|
+
};
|
|
16103
16624
|
connection.onRequest("initialize", async (raw) => {
|
|
16104
16625
|
InitializeParams.parse(raw ?? {});
|
|
16105
16626
|
return buildInitializeResult();
|
|
@@ -16126,7 +16647,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16126
16647
|
const { entries: replay } = await session.attach(client, "full");
|
|
16127
16648
|
state.attached.set(session.sessionId, {
|
|
16128
16649
|
sessionId: session.sessionId,
|
|
16129
|
-
clientId: client.clientId
|
|
16650
|
+
clientId: client.clientId,
|
|
16651
|
+
readonly: false
|
|
16130
16652
|
});
|
|
16131
16653
|
setImmediate(() => {
|
|
16132
16654
|
void (async () => {
|
|
@@ -16152,11 +16674,46 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16152
16674
|
connection.onRequest("session/attach", async (raw) => {
|
|
16153
16675
|
const params = SessionAttachParams.parse(raw);
|
|
16154
16676
|
const hydraHints = extractHydraMeta(params._meta).resume;
|
|
16677
|
+
const readonly = params.readonly === true;
|
|
16155
16678
|
app.log.info(
|
|
16156
|
-
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints}`
|
|
16679
|
+
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints} readonly=${readonly}`
|
|
16157
16680
|
);
|
|
16158
16681
|
const lookupId = hydraHints ? params.sessionId : await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
16159
16682
|
let session = deps.manager.get(lookupId);
|
|
16683
|
+
if (!session && readonly) {
|
|
16684
|
+
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
16685
|
+
if (!fromDisk) {
|
|
16686
|
+
const err = new Error(
|
|
16687
|
+
`session ${params.sessionId} not found`
|
|
16688
|
+
);
|
|
16689
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
16690
|
+
throw err;
|
|
16691
|
+
}
|
|
16692
|
+
const history = await deps.manager.loadHistory(lookupId);
|
|
16693
|
+
const viewerClientId = params.clientId ?? `cli_${nanoid2(8)}`;
|
|
16694
|
+
state.attached.set(fromDisk.hydraSessionId, {
|
|
16695
|
+
sessionId: fromDisk.hydraSessionId,
|
|
16696
|
+
clientId: viewerClientId,
|
|
16697
|
+
readonly: true
|
|
16698
|
+
});
|
|
16699
|
+
app.log.info(
|
|
16700
|
+
`session/attach OK (viewer) sessionId=${fromDisk.hydraSessionId} clientId=${viewerClientId} attachedCount=${state.attached.size} replayed=${history.length}`
|
|
16701
|
+
);
|
|
16702
|
+
for (const entry of history) {
|
|
16703
|
+
await connection.notify(entry.method, entry.params).catch(() => void 0);
|
|
16704
|
+
}
|
|
16705
|
+
return {
|
|
16706
|
+
sessionId: fromDisk.hydraSessionId,
|
|
16707
|
+
clientId: viewerClientId,
|
|
16708
|
+
connectedClients: [viewerClientId],
|
|
16709
|
+
// No Session.attach() ran, so no history policy was applied —
|
|
16710
|
+
// the viewer always gets full history. Report "full" so the
|
|
16711
|
+
// wire shape matches the normal attach response.
|
|
16712
|
+
historyPolicy: "full",
|
|
16713
|
+
replayed: history.length,
|
|
16714
|
+
_meta: buildViewerResponseMeta(fromDisk)
|
|
16715
|
+
};
|
|
16716
|
+
}
|
|
16160
16717
|
if (!session) {
|
|
16161
16718
|
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
16162
16719
|
let resurrectParams = fromDisk;
|
|
@@ -16200,10 +16757,11 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16200
16757
|
);
|
|
16201
16758
|
state.attached.set(session.sessionId, {
|
|
16202
16759
|
sessionId: session.sessionId,
|
|
16203
|
-
clientId: client.clientId
|
|
16760
|
+
clientId: client.clientId,
|
|
16761
|
+
readonly
|
|
16204
16762
|
});
|
|
16205
16763
|
app.log.info(
|
|
16206
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length}`
|
|
16764
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}`
|
|
16207
16765
|
);
|
|
16208
16766
|
for (const note of replay) {
|
|
16209
16767
|
await connection.notify(note.method, note.params);
|
|
@@ -16247,6 +16805,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16247
16805
|
});
|
|
16248
16806
|
connection.onRequest("session/prompt", async (raw) => {
|
|
16249
16807
|
const params = SessionPromptParams.parse(raw);
|
|
16808
|
+
denyIfReadonly(params.sessionId, "session/prompt");
|
|
16250
16809
|
const att = state.attached.get(params.sessionId);
|
|
16251
16810
|
if (!att) {
|
|
16252
16811
|
app.log.warn(
|
|
@@ -16295,6 +16854,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16295
16854
|
if (!att) {
|
|
16296
16855
|
return;
|
|
16297
16856
|
}
|
|
16857
|
+
if (att.readonly) {
|
|
16858
|
+
app.log.warn(
|
|
16859
|
+
`session/cancel dropped (readonly attachment) sessionId=${params.sessionId}`
|
|
16860
|
+
);
|
|
16861
|
+
return;
|
|
16862
|
+
}
|
|
16298
16863
|
const session = deps.manager.get(params.sessionId);
|
|
16299
16864
|
if (!session) {
|
|
16300
16865
|
return;
|
|
@@ -16307,11 +16872,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16307
16872
|
};
|
|
16308
16873
|
connection.onNotification("session/cancel", handleCancelParams);
|
|
16309
16874
|
connection.onRequest("session/cancel", async (raw) => {
|
|
16875
|
+
const params = SessionCancelParams.parse(raw);
|
|
16876
|
+
denyIfReadonly(params.sessionId, "session/cancel");
|
|
16310
16877
|
handleCancelParams(raw);
|
|
16311
16878
|
return null;
|
|
16312
16879
|
});
|
|
16313
16880
|
connection.onRequest("hydra-acp/cancel_prompt", async (raw) => {
|
|
16314
16881
|
const params = CancelPromptParams.parse(raw);
|
|
16882
|
+
denyIfReadonly(params.sessionId, "hydra-acp/cancel_prompt");
|
|
16315
16883
|
const session = deps.manager.get(params.sessionId);
|
|
16316
16884
|
if (!session) {
|
|
16317
16885
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -16322,6 +16890,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16322
16890
|
});
|
|
16323
16891
|
connection.onRequest("hydra-acp/update_prompt", async (raw) => {
|
|
16324
16892
|
const params = UpdatePromptParams.parse(raw);
|
|
16893
|
+
denyIfReadonly(params.sessionId, "hydra-acp/update_prompt");
|
|
16325
16894
|
const session = deps.manager.get(params.sessionId);
|
|
16326
16895
|
if (!session) {
|
|
16327
16896
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -16332,6 +16901,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16332
16901
|
});
|
|
16333
16902
|
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
16334
16903
|
const params = AmendPromptParams.parse(raw);
|
|
16904
|
+
denyIfReadonly(params.sessionId, "hydra-acp/amend_prompt");
|
|
16335
16905
|
const att = state.attached.get(params.sessionId);
|
|
16336
16906
|
if (!att) {
|
|
16337
16907
|
const err = new Error("not attached to session");
|
|
@@ -16371,7 +16941,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16371
16941
|
const { entries: replay } = await session.attach(client, "pending_only");
|
|
16372
16942
|
state.attached.set(session.sessionId, {
|
|
16373
16943
|
sessionId: session.sessionId,
|
|
16374
|
-
clientId: client.clientId
|
|
16944
|
+
clientId: client.clientId,
|
|
16945
|
+
readonly: false
|
|
16375
16946
|
});
|
|
16376
16947
|
for (const note of replay) {
|
|
16377
16948
|
await connection.notify(note.method, note.params);
|
|
@@ -16390,6 +16961,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16390
16961
|
};
|
|
16391
16962
|
});
|
|
16392
16963
|
connection.onRequest("session/set_model", async (rawParams) => {
|
|
16964
|
+
const sessionIdField = rawParams?.sessionId;
|
|
16965
|
+
if (typeof sessionIdField === "string") {
|
|
16966
|
+
denyIfReadonly(sessionIdField, "session/set_model");
|
|
16967
|
+
}
|
|
16393
16968
|
const decision = decideSetModel(rawParams, deps.manager);
|
|
16394
16969
|
if (decision.kind === "error") {
|
|
16395
16970
|
app.log.warn(decision.logMessage);
|
|
@@ -16423,6 +16998,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16423
16998
|
err.code = JsonRpcErrorCodes.MethodNotFound;
|
|
16424
16999
|
throw err;
|
|
16425
17000
|
}
|
|
17001
|
+
denyIfReadonly(sessionId, method);
|
|
16426
17002
|
const session = deps.manager.get(sessionId);
|
|
16427
17003
|
if (!session) {
|
|
16428
17004
|
const err = new Error(`session ${sessionId} not found`);
|
|
@@ -16561,6 +17137,38 @@ function decideSetModel(rawParams, manager) {
|
|
|
16561
17137
|
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16562
17138
|
};
|
|
16563
17139
|
}
|
|
17140
|
+
function buildViewerResponseMeta(fromDisk) {
|
|
17141
|
+
const ours = {
|
|
17142
|
+
upstreamSessionId: fromDisk.upstreamSessionId,
|
|
17143
|
+
agentId: fromDisk.agentId,
|
|
17144
|
+
cwd: fromDisk.cwd
|
|
17145
|
+
};
|
|
17146
|
+
if (fromDisk.title !== void 0) {
|
|
17147
|
+
ours.name = fromDisk.title;
|
|
17148
|
+
}
|
|
17149
|
+
if (fromDisk.agentArgs && fromDisk.agentArgs.length > 0) {
|
|
17150
|
+
ours.agentArgs = fromDisk.agentArgs;
|
|
17151
|
+
}
|
|
17152
|
+
if (fromDisk.currentModel !== void 0) {
|
|
17153
|
+
ours.currentModel = fromDisk.currentModel;
|
|
17154
|
+
}
|
|
17155
|
+
if (fromDisk.currentMode !== void 0) {
|
|
17156
|
+
ours.currentMode = fromDisk.currentMode;
|
|
17157
|
+
}
|
|
17158
|
+
if (fromDisk.currentUsage !== void 0) {
|
|
17159
|
+
ours.currentUsage = fromDisk.currentUsage;
|
|
17160
|
+
}
|
|
17161
|
+
if (fromDisk.agentCommands && fromDisk.agentCommands.length > 0) {
|
|
17162
|
+
ours.availableCommands = fromDisk.agentCommands;
|
|
17163
|
+
}
|
|
17164
|
+
if (fromDisk.agentModes && fromDisk.agentModes.length > 0) {
|
|
17165
|
+
ours.availableModes = fromDisk.agentModes;
|
|
17166
|
+
}
|
|
17167
|
+
if (fromDisk.agentModels && fromDisk.agentModels.length > 0) {
|
|
17168
|
+
ours.availableModels = fromDisk.agentModels;
|
|
17169
|
+
}
|
|
17170
|
+
return { [HYDRA_META_KEY]: ours };
|
|
17171
|
+
}
|
|
16564
17172
|
function buildResponseMeta(session) {
|
|
16565
17173
|
const ours = {
|
|
16566
17174
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -16836,9 +17444,9 @@ import * as fs16 from "fs";
|
|
|
16836
17444
|
import * as fsp6 from "fs/promises";
|
|
16837
17445
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
16838
17446
|
const opts = parseLogTailFlags(argv);
|
|
16839
|
-
let
|
|
17447
|
+
let stat5;
|
|
16840
17448
|
try {
|
|
16841
|
-
|
|
17449
|
+
stat5 = await fsp6.stat(logPath);
|
|
16842
17450
|
} catch (err) {
|
|
16843
17451
|
const e = err;
|
|
16844
17452
|
if (e.code === "ENOENT") {
|
|
@@ -16849,7 +17457,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
16849
17457
|
}
|
|
16850
17458
|
throw err;
|
|
16851
17459
|
}
|
|
16852
|
-
let position = await printTail(logPath,
|
|
17460
|
+
let position = await printTail(logPath, stat5.size, opts.tail);
|
|
16853
17461
|
if (!opts.follow) {
|
|
16854
17462
|
return;
|
|
16855
17463
|
}
|
|
@@ -16884,10 +17492,10 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
16884
17492
|
}
|
|
16885
17493
|
});
|
|
16886
17494
|
});
|
|
16887
|
-
await new Promise((
|
|
17495
|
+
await new Promise((resolve6) => {
|
|
16888
17496
|
const finish = () => {
|
|
16889
17497
|
watcher.close();
|
|
16890
|
-
|
|
17498
|
+
resolve6();
|
|
16891
17499
|
};
|
|
16892
17500
|
process.once("SIGINT", finish);
|
|
16893
17501
|
process.once("SIGTERM", finish);
|
|
@@ -17693,7 +18301,7 @@ async function promptPassword(prompt) {
|
|
|
17693
18301
|
if (!process.stdin.isTTY) {
|
|
17694
18302
|
return readLineFromStdin();
|
|
17695
18303
|
}
|
|
17696
|
-
return new Promise((
|
|
18304
|
+
return new Promise((resolve6, reject) => {
|
|
17697
18305
|
const stdin = process.stdin;
|
|
17698
18306
|
const wasRaw = stdin.isRaw === true;
|
|
17699
18307
|
let buffer = "";
|
|
@@ -17710,7 +18318,7 @@ async function promptPassword(prompt) {
|
|
|
17710
18318
|
if (byte === 10 || byte === 13) {
|
|
17711
18319
|
process.stdout.write("\n");
|
|
17712
18320
|
cleanup();
|
|
17713
|
-
|
|
18321
|
+
resolve6(buffer);
|
|
17714
18322
|
return;
|
|
17715
18323
|
}
|
|
17716
18324
|
if (byte === 3) {
|
|
@@ -17736,7 +18344,7 @@ async function promptPassword(prompt) {
|
|
|
17736
18344
|
});
|
|
17737
18345
|
}
|
|
17738
18346
|
function readLineFromStdin() {
|
|
17739
|
-
return new Promise((
|
|
18347
|
+
return new Promise((resolve6, reject) => {
|
|
17740
18348
|
let buffer = "";
|
|
17741
18349
|
process.stdin.setEncoding("utf8");
|
|
17742
18350
|
const onData = (chunk) => {
|
|
@@ -17745,7 +18353,7 @@ function readLineFromStdin() {
|
|
|
17745
18353
|
if (nl !== -1) {
|
|
17746
18354
|
process.stdin.removeListener("data", onData);
|
|
17747
18355
|
process.stdin.removeListener("error", onError);
|
|
17748
|
-
|
|
18356
|
+
resolve6(buffer.slice(0, nl).replace(/\r$/, ""));
|
|
17749
18357
|
}
|
|
17750
18358
|
};
|
|
17751
18359
|
const onError = (err) => {
|
|
@@ -18542,8 +19150,15 @@ async function dispatchTui(flags, base) {
|
|
|
18542
19150
|
const cwd = resolveOption(flags, "cwd");
|
|
18543
19151
|
const resume = flags.reattach === true;
|
|
18544
19152
|
const forceNew = flags.new === true;
|
|
19153
|
+
const readonly = flags.readonly === true;
|
|
19154
|
+
if (readonly && base.sessionId === void 0) {
|
|
19155
|
+
process.stderr.write(
|
|
19156
|
+
"hydra-acp: --readonly requires a session id. Pass --resume <id> --readonly, or open the picker and press `v` on a session.\n"
|
|
19157
|
+
);
|
|
19158
|
+
process.exit(2);
|
|
19159
|
+
}
|
|
18545
19160
|
const { runTui } = await Promise.resolve().then(() => (init_tui(), tui_exports));
|
|
18546
|
-
const tuiOpts = { resume, forceNew };
|
|
19161
|
+
const tuiOpts = { resume, forceNew, readonly };
|
|
18547
19162
|
if (base.sessionId !== void 0) {
|
|
18548
19163
|
tuiOpts.sessionId = base.sessionId;
|
|
18549
19164
|
}
|
|
@@ -18571,7 +19186,7 @@ function readVersion() {
|
|
|
18571
19186
|
try {
|
|
18572
19187
|
const here = dirname6(fileURLToPath2(import.meta.url));
|
|
18573
19188
|
const pkg = JSON.parse(
|
|
18574
|
-
readFileSync2(
|
|
19189
|
+
readFileSync2(resolve5(here, "../package.json"), "utf8")
|
|
18575
19190
|
);
|
|
18576
19191
|
return pkg.version ?? "unknown";
|
|
18577
19192
|
} catch {
|
|
@@ -18618,8 +19233,9 @@ function printHelp() {
|
|
|
18618
19233
|
" hydra-acp auth password [--force] Set the daemon's master password",
|
|
18619
19234
|
" hydra-acp auth [list] List active session tokens",
|
|
18620
19235
|
" hydra-acp auth revoke <id> Revoke a session token",
|
|
18621
|
-
" hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
|
|
19236
|
+
" hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--readonly] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
|
|
18622
19237
|
" --resume <id> attaches to a specific session; --reattach picks the most-recent in cwd.",
|
|
19238
|
+
" --readonly opens a session as a transcript viewer (no agent spawn, no prompting). Requires --resume.",
|
|
18623
19239
|
" Smart default (no flags): shows a picker when sessions exist, else new.",
|
|
18624
19240
|
" hydra-acp --version Print version",
|
|
18625
19241
|
" hydra-acp --help Show this help",
|