@hydra-acp/cli 0.1.24 → 0.1.26
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 +9 -9
- package/dist/cli.js +1882 -171
- package/dist/index.d.ts +135 -0
- package/dist/index.js +1065 -79
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -289,7 +289,15 @@ var init_config = __esm({
|
|
|
289
289
|
// on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
|
|
290
290
|
// running. Set false if your terminal renders this obnoxiously or you
|
|
291
291
|
// just don't want it.
|
|
292
|
-
progressIndicator: z.boolean().default(true)
|
|
292
|
+
progressIndicator: z.boolean().default(true),
|
|
293
|
+
// What the unmodified Enter key does in the prompt composer.
|
|
294
|
+
// "enqueue" (default) — Enter enqueues the prompt (sends immediately
|
|
295
|
+
// when idle, queues behind an in-flight turn); Shift+Enter amends
|
|
296
|
+
// the in-flight turn.
|
|
297
|
+
// "amend" — flips the two: Enter amends the in-flight turn,
|
|
298
|
+
// Shift+Enter enqueues. With no turn in flight either key just
|
|
299
|
+
// enqueues, since there's nothing to amend.
|
|
300
|
+
defaultEnterAction: z.enum(["enqueue", "amend"]).default("enqueue")
|
|
293
301
|
});
|
|
294
302
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
295
303
|
ExtensionBody = z.object({
|
|
@@ -333,7 +341,8 @@ var init_config = __esm({
|
|
|
333
341
|
mouse: true,
|
|
334
342
|
logMaxBytes: 5 * 1024 * 1024,
|
|
335
343
|
cwdColumnMaxWidth: 24,
|
|
336
|
-
progressIndicator: true
|
|
344
|
+
progressIndicator: true,
|
|
345
|
+
defaultEnterAction: "enqueue"
|
|
337
346
|
})
|
|
338
347
|
});
|
|
339
348
|
}
|
|
@@ -413,6 +422,18 @@ function extractHydraMeta(meta) {
|
|
|
413
422
|
if (typeof obj.promptQueueing === "boolean") {
|
|
414
423
|
out.promptQueueing = obj.promptQueueing;
|
|
415
424
|
}
|
|
425
|
+
if (typeof obj.promptCancelling === "boolean") {
|
|
426
|
+
out.promptCancelling = obj.promptCancelling;
|
|
427
|
+
}
|
|
428
|
+
if (typeof obj.promptUpdating === "boolean") {
|
|
429
|
+
out.promptUpdating = obj.promptUpdating;
|
|
430
|
+
}
|
|
431
|
+
if (typeof obj.promptAmending === "boolean") {
|
|
432
|
+
out.promptAmending = obj.promptAmending;
|
|
433
|
+
}
|
|
434
|
+
if (typeof obj.promptPipelining === "boolean") {
|
|
435
|
+
out.promptPipelining = obj.promptPipelining;
|
|
436
|
+
}
|
|
416
437
|
if (Array.isArray(obj.queue)) {
|
|
417
438
|
const entries = [];
|
|
418
439
|
for (const raw of obj.queue) {
|
|
@@ -462,12 +483,35 @@ function extractHydraMeta(meta) {
|
|
|
462
483
|
out.availableModes = modes;
|
|
463
484
|
}
|
|
464
485
|
}
|
|
486
|
+
if (Array.isArray(obj.availableModels)) {
|
|
487
|
+
const models = [];
|
|
488
|
+
for (const raw of obj.availableModels) {
|
|
489
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const m = raw;
|
|
493
|
+
if (typeof m.modelId !== "string") {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const model = { modelId: m.modelId };
|
|
497
|
+
if (typeof m.name === "string") {
|
|
498
|
+
model.name = m.name;
|
|
499
|
+
}
|
|
500
|
+
if (typeof m.description === "string") {
|
|
501
|
+
model.description = m.description;
|
|
502
|
+
}
|
|
503
|
+
models.push(model);
|
|
504
|
+
}
|
|
505
|
+
if (models.length > 0) {
|
|
506
|
+
out.availableModels = models;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
465
509
|
return out;
|
|
466
510
|
}
|
|
467
511
|
function mergeMeta(passthrough, ours) {
|
|
468
512
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
469
513
|
}
|
|
470
|
-
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, ProxyInitializeParams;
|
|
514
|
+
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, AmendPromptParams, AmendPromptResult, PromptAmendedParams, AgentInstallProgressParams, AGENT_INSTALL_PROGRESS_METHOD, ProxyInitializeParams;
|
|
471
515
|
var init_types = __esm({
|
|
472
516
|
"src/acp/types.ts"() {
|
|
473
517
|
"use strict";
|
|
@@ -633,6 +677,51 @@ var init_types = __esm({
|
|
|
633
677
|
updated: z3.boolean(),
|
|
634
678
|
reason: z3.enum(["ok", "not_found", "already_running"])
|
|
635
679
|
});
|
|
680
|
+
AmendPromptParams = z3.object({
|
|
681
|
+
sessionId: z3.string(),
|
|
682
|
+
targetMessageId: z3.string(),
|
|
683
|
+
prompt: z3.array(z3.unknown()),
|
|
684
|
+
replaceQueue: z3.boolean().optional(),
|
|
685
|
+
onTargetCompleted: z3.enum(["reject", "send_anyway"]).optional()
|
|
686
|
+
});
|
|
687
|
+
AmendPromptResult = z3.object({
|
|
688
|
+
amended: z3.boolean(),
|
|
689
|
+
reason: z3.enum([
|
|
690
|
+
"ok",
|
|
691
|
+
"target_completed",
|
|
692
|
+
"target_cancelled",
|
|
693
|
+
"target_not_found"
|
|
694
|
+
]),
|
|
695
|
+
// Present when a prompt was sent or replaced: the amendment's id on
|
|
696
|
+
// success, or the regular follow-up's id when onTargetCompleted is
|
|
697
|
+
// "send_anyway" and the daemon forwarded the prompt anyway.
|
|
698
|
+
messageId: z3.string().optional()
|
|
699
|
+
});
|
|
700
|
+
PromptAmendedParams = z3.object({
|
|
701
|
+
sessionId: z3.string(),
|
|
702
|
+
cancelledMessageId: z3.string(),
|
|
703
|
+
newMessageId: z3.string(),
|
|
704
|
+
prompt: z3.array(z3.unknown()),
|
|
705
|
+
originator: PromptOriginatorSchema,
|
|
706
|
+
amendedAt: z3.number()
|
|
707
|
+
});
|
|
708
|
+
AgentInstallProgressParams = z3.object({
|
|
709
|
+
agentId: z3.string(),
|
|
710
|
+
version: z3.string(),
|
|
711
|
+
source: z3.enum(["binary", "npm"]),
|
|
712
|
+
phase: z3.enum([
|
|
713
|
+
"download_start",
|
|
714
|
+
"download_progress",
|
|
715
|
+
"download_done",
|
|
716
|
+
"extract",
|
|
717
|
+
"install_start",
|
|
718
|
+
"installed"
|
|
719
|
+
]),
|
|
720
|
+
receivedBytes: z3.number().optional(),
|
|
721
|
+
totalBytes: z3.number().optional(),
|
|
722
|
+
packageSpec: z3.string().optional()
|
|
723
|
+
});
|
|
724
|
+
AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/agent_install_progress";
|
|
636
725
|
ProxyInitializeParams = z3.object({
|
|
637
726
|
protocolVersion: z3.number().optional(),
|
|
638
727
|
proxyInfo: z3.object({
|
|
@@ -971,6 +1060,42 @@ function sameAdvertisedModes(a, b) {
|
|
|
971
1060
|
}
|
|
972
1061
|
return true;
|
|
973
1062
|
}
|
|
1063
|
+
function sameAdvertisedModels(a, b) {
|
|
1064
|
+
if (a.length !== b.length) {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
for (let i = 0; i < a.length; i++) {
|
|
1068
|
+
if (a[i]?.modelId !== b[i]?.modelId || a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
function parseModelsList(list) {
|
|
1075
|
+
if (!Array.isArray(list)) {
|
|
1076
|
+
return [];
|
|
1077
|
+
}
|
|
1078
|
+
const out = [];
|
|
1079
|
+
for (const raw of list) {
|
|
1080
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const r = raw;
|
|
1084
|
+
const modelId = typeof r.modelId === "string" && r.modelId.trim() || typeof r.value === "string" && r.value.trim() || typeof r.id === "string" && r.id.trim() || void 0;
|
|
1085
|
+
if (!modelId) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const model = { modelId };
|
|
1089
|
+
if (typeof r.name === "string" && r.name.length > 0) {
|
|
1090
|
+
model.name = r.name;
|
|
1091
|
+
}
|
|
1092
|
+
if (typeof r.description === "string" && r.description.length > 0) {
|
|
1093
|
+
model.description = r.description;
|
|
1094
|
+
}
|
|
1095
|
+
out.push(model);
|
|
1096
|
+
}
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
974
1099
|
function extractAdvertisedModes(params) {
|
|
975
1100
|
const obj = params ?? {};
|
|
976
1101
|
const update = obj.update ?? {};
|
|
@@ -1154,7 +1279,7 @@ function firstLine(text, max) {
|
|
|
1154
1279
|
}
|
|
1155
1280
|
return void 0;
|
|
1156
1281
|
}
|
|
1157
|
-
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, DEFAULT_HISTORY_MAX_ENTRIES, Session, STATE_UPDATE_KINDS;
|
|
1282
|
+
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, DEFAULT_HISTORY_MAX_ENTRIES, RECENTLY_TERMINAL_LIMIT, Session, STATE_UPDATE_KINDS;
|
|
1158
1283
|
var init_session = __esm({
|
|
1159
1284
|
"src/core/session.ts"() {
|
|
1160
1285
|
"use strict";
|
|
@@ -1165,6 +1290,7 @@ var init_session = __esm({
|
|
|
1165
1290
|
generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
1166
1291
|
HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
1167
1292
|
DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
1293
|
+
RECENTLY_TERMINAL_LIMIT = 64;
|
|
1168
1294
|
Session = class {
|
|
1169
1295
|
sessionId;
|
|
1170
1296
|
cwd;
|
|
@@ -1256,14 +1382,36 @@ var init_session = __esm({
|
|
|
1256
1382
|
// Last available_modes_update we observed from the agent. Same
|
|
1257
1383
|
// pattern as commands: cache, persist, broadcast on change.
|
|
1258
1384
|
agentAdvertisedModes = [];
|
|
1385
|
+
// Last availableModels payload we observed (from current_model_update,
|
|
1386
|
+
// a session/new / session/load response, or — for opencode — a
|
|
1387
|
+
// config_option_update where configOptions[i].id === "model").
|
|
1388
|
+
// Cached so a mid-session attach can synthesize a model picker
|
|
1389
|
+
// snapshot, and so session/set_model can validate the requested id
|
|
1390
|
+
// against what the agent claims to support.
|
|
1391
|
+
agentAdvertisedModels = [];
|
|
1259
1392
|
// Persist hooks for snapshot-shaped state. SessionManager hooks these
|
|
1260
1393
|
// to mirror changes into meta.json so cold-resurrect attaches can
|
|
1261
1394
|
// surface the latest snapshot via the attach response _meta.
|
|
1262
1395
|
agentCommandsHandlers = [];
|
|
1263
1396
|
agentModesHandlers = [];
|
|
1397
|
+
agentModelsHandlers = [];
|
|
1264
1398
|
modelHandlers = [];
|
|
1265
1399
|
modeHandlers = [];
|
|
1266
1400
|
usageHandlers = [];
|
|
1401
|
+
// Set by amendPrompt at the start of a cancel-and-resubmit dance.
|
|
1402
|
+
// broadcastTurnComplete reads it to attach the _meta.amended marker
|
|
1403
|
+
// to the cancelled turn's turn_complete notification, and to fire the
|
|
1404
|
+
// dedicated prompt_amended notification. Cleared when the cancelled
|
|
1405
|
+
// turn's task completes (runQueueEntry) OR if the amendment is
|
|
1406
|
+
// cancelled mid-window via cancel_prompt(M2) before drainQueue picks
|
|
1407
|
+
// it up.
|
|
1408
|
+
amendInProgress;
|
|
1409
|
+
// LRU of recently-terminal messageIds → stopReason. Used by
|
|
1410
|
+
// amendPrompt to resolve targets that completed/cancelled before
|
|
1411
|
+
// the amend arrived. Capped at RECENTLY_TERMINAL_LIMIT entries;
|
|
1412
|
+
// older entries fall out and resolve to target_not_found, which is
|
|
1413
|
+
// the correct behavior.
|
|
1414
|
+
recentlyTerminal = /* @__PURE__ */ new Map();
|
|
1267
1415
|
constructor(init) {
|
|
1268
1416
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
1269
1417
|
this.cwd = init.cwd;
|
|
@@ -1282,6 +1430,9 @@ var init_session = __esm({
|
|
|
1282
1430
|
if (init.agentModes && init.agentModes.length > 0) {
|
|
1283
1431
|
this.agentAdvertisedModes = [...init.agentModes];
|
|
1284
1432
|
}
|
|
1433
|
+
if (init.agentModels && init.agentModels.length > 0) {
|
|
1434
|
+
this.agentAdvertisedModels = [...init.agentModels];
|
|
1435
|
+
}
|
|
1285
1436
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
1286
1437
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
1287
1438
|
this.logger = init.logger;
|
|
@@ -1319,6 +1470,23 @@ var init_session = __esm({
|
|
|
1319
1470
|
}
|
|
1320
1471
|
});
|
|
1321
1472
|
}
|
|
1473
|
+
// Re-broadcast our cached availableModels via current_model_update.
|
|
1474
|
+
// Spec shape: { currentModel, availableModels } — we only include the
|
|
1475
|
+
// currentModel field when we know it, so this broadcast can also fire
|
|
1476
|
+
// model-list updates standalone before any current model is set.
|
|
1477
|
+
broadcastAvailableModels() {
|
|
1478
|
+
const update = {
|
|
1479
|
+
sessionUpdate: "current_model_update",
|
|
1480
|
+
availableModels: [...this.agentAdvertisedModels]
|
|
1481
|
+
};
|
|
1482
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
1483
|
+
update.currentModel = this.currentModel;
|
|
1484
|
+
}
|
|
1485
|
+
this.recordAndBroadcast("session/update", {
|
|
1486
|
+
sessionId: this.upstreamSessionId,
|
|
1487
|
+
update
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1322
1490
|
// Register session/update, session/request_permission, and onExit
|
|
1323
1491
|
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
1324
1492
|
// the new agent is plumbed identically. The exit handler's identity
|
|
@@ -1349,6 +1517,10 @@ var init_session = __esm({
|
|
|
1349
1517
|
this.recordAndBroadcast("session/update", params);
|
|
1350
1518
|
return;
|
|
1351
1519
|
}
|
|
1520
|
+
if (this.maybeApplyAgentConfigOption(params)) {
|
|
1521
|
+
this.recordAndBroadcast("session/update", params);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1352
1524
|
if (this.maybeApplyAgentUsage(params)) {
|
|
1353
1525
|
this.recordAndBroadcast("session/update", params);
|
|
1354
1526
|
return;
|
|
@@ -1497,16 +1669,19 @@ var init_session = __esm({
|
|
|
1497
1669
|
recordedAt
|
|
1498
1670
|
});
|
|
1499
1671
|
}
|
|
1500
|
-
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
1672
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0 || this.agentAdvertisedModels.length > 0) {
|
|
1673
|
+
const update = {
|
|
1674
|
+
sessionUpdate: "current_model_update"
|
|
1675
|
+
};
|
|
1676
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
1677
|
+
update.currentModel = this.currentModel;
|
|
1678
|
+
}
|
|
1679
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
1680
|
+
update.availableModels = [...this.agentAdvertisedModels];
|
|
1681
|
+
}
|
|
1501
1682
|
out.push({
|
|
1502
1683
|
method: "session/update",
|
|
1503
|
-
params: {
|
|
1504
|
-
sessionId,
|
|
1505
|
-
update: {
|
|
1506
|
-
sessionUpdate: "current_model_update",
|
|
1507
|
-
currentModel: this.currentModel
|
|
1508
|
-
}
|
|
1509
|
-
},
|
|
1684
|
+
params: { sessionId, update },
|
|
1510
1685
|
recordedAt
|
|
1511
1686
|
});
|
|
1512
1687
|
}
|
|
@@ -1693,7 +1868,7 @@ var init_session = __esm({
|
|
|
1693
1868
|
);
|
|
1694
1869
|
}
|
|
1695
1870
|
}
|
|
1696
|
-
broadcastTurnComplete(originatorClientId, response) {
|
|
1871
|
+
broadcastTurnComplete(originatorClientId, response, promptMessageId, wasAmend) {
|
|
1697
1872
|
const stopReason = response && typeof response === "object" && "stopReason" in response && typeof response.stopReason === "string" ? response.stopReason : void 0;
|
|
1698
1873
|
const update = {
|
|
1699
1874
|
sessionUpdate: "turn_complete",
|
|
@@ -1702,16 +1877,84 @@ var init_session = __esm({
|
|
|
1702
1877
|
if (stopReason !== void 0) {
|
|
1703
1878
|
update.stopReason = stopReason;
|
|
1704
1879
|
}
|
|
1880
|
+
const amend = this.amendInProgress;
|
|
1881
|
+
if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
|
|
1882
|
+
update._meta = {
|
|
1883
|
+
"hydra-acp": {
|
|
1884
|
+
amended: {
|
|
1885
|
+
cancelledMessageId: amend.cancelledMessageId,
|
|
1886
|
+
newMessageId: amend.newMessageId
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1705
1891
|
this.promptStartedAt = void 0;
|
|
1892
|
+
if (promptMessageId !== void 0 && stopReason !== void 0) {
|
|
1893
|
+
this.recordTerminal(promptMessageId, stopReason);
|
|
1894
|
+
}
|
|
1706
1895
|
this.recordAndBroadcast(
|
|
1707
1896
|
"session/update",
|
|
1708
1897
|
{
|
|
1709
1898
|
sessionId: this.sessionId,
|
|
1710
1899
|
update
|
|
1711
1900
|
},
|
|
1712
|
-
originatorClientId
|
|
1901
|
+
wasAmend ? void 0 : originatorClientId
|
|
1902
|
+
);
|
|
1903
|
+
if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
|
|
1904
|
+
this.broadcastPromptAmended(amend);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
// Record that a prompt's turn has ended, with its terminal stopReason.
|
|
1908
|
+
// Used by amendPrompt to resolve targetMessageIds that completed/cancelled
|
|
1909
|
+
// before the amend arrived. LRU-trimmed at RECENTLY_TERMINAL_LIMIT.
|
|
1910
|
+
recordTerminal(messageId, stopReason) {
|
|
1911
|
+
this.recentlyTerminal.set(messageId, {
|
|
1912
|
+
stopReason,
|
|
1913
|
+
terminatedAt: Date.now()
|
|
1914
|
+
});
|
|
1915
|
+
while (this.recentlyTerminal.size > RECENTLY_TERMINAL_LIMIT) {
|
|
1916
|
+
const oldest = this.recentlyTerminal.keys().next().value;
|
|
1917
|
+
if (oldest === void 0) {
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
this.recentlyTerminal.delete(oldest);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
// Fire hydra-acp/prompt_amended for the M1→M2 linkage. The amendment's
|
|
1924
|
+
// current content is read live from the queue entry so any update_prompt
|
|
1925
|
+
// calls during the amend window are reflected. Best-effort: if M2 has
|
|
1926
|
+
// already been cancelled out of the queue by the time we get here, we
|
|
1927
|
+
// skip — the amendInProgress clearing in cancelQueuedPrompt should have
|
|
1928
|
+
// prevented this code path from running in that case.
|
|
1929
|
+
broadcastPromptAmended(amend) {
|
|
1930
|
+
const entry = this.findUserEntry(amend.newMessageId);
|
|
1931
|
+
if (!entry) {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const params = {
|
|
1935
|
+
sessionId: this.sessionId,
|
|
1936
|
+
cancelledMessageId: amend.cancelledMessageId,
|
|
1937
|
+
newMessageId: amend.newMessageId,
|
|
1938
|
+
prompt: entry.prompt,
|
|
1939
|
+
originator: entry.originator,
|
|
1940
|
+
amendedAt: Date.now()
|
|
1941
|
+
};
|
|
1942
|
+
this.broadcastQueueNotification(
|
|
1943
|
+
"hydra-acp/prompt_amended",
|
|
1944
|
+
params
|
|
1713
1945
|
);
|
|
1714
1946
|
}
|
|
1947
|
+
// Look up a user-prompt queue entry by messageId, searching both the
|
|
1948
|
+
// currentEntry slot and the waiting queue.
|
|
1949
|
+
findUserEntry(messageId) {
|
|
1950
|
+
if (this.currentEntry?.messageId === messageId && this.currentEntry.kind === "user") {
|
|
1951
|
+
return this.currentEntry;
|
|
1952
|
+
}
|
|
1953
|
+
const queued = this.promptQueue.find(
|
|
1954
|
+
(e) => e.messageId === messageId && e.kind === "user"
|
|
1955
|
+
);
|
|
1956
|
+
return queued?.kind === "user" ? queued : void 0;
|
|
1957
|
+
}
|
|
1715
1958
|
// Total visible-or-running entries: the in-flight head (if any) plus
|
|
1716
1959
|
// the queue's user-visible waiting entries. Internal entries don't
|
|
1717
1960
|
// count — they're an implementation detail and the wire never
|
|
@@ -1723,9 +1966,9 @@ var init_session = __esm({
|
|
|
1723
1966
|
}
|
|
1724
1967
|
return count;
|
|
1725
1968
|
}
|
|
1726
|
-
broadcastQueueAdded(entry) {
|
|
1969
|
+
broadcastQueueAdded(entry, options) {
|
|
1727
1970
|
const depth = this.visibleQueueDepth();
|
|
1728
|
-
const position = Math.max(0, depth - 1);
|
|
1971
|
+
const position = options?.position ?? Math.max(0, depth - 1);
|
|
1729
1972
|
const params = {
|
|
1730
1973
|
sessionId: this.sessionId,
|
|
1731
1974
|
messageId: entry.messageId,
|
|
@@ -1735,6 +1978,11 @@ var init_session = __esm({
|
|
|
1735
1978
|
queueDepth: depth,
|
|
1736
1979
|
enqueuedAt: entry.enqueuedAt
|
|
1737
1980
|
};
|
|
1981
|
+
if (options?.amending !== void 0) {
|
|
1982
|
+
params._meta = {
|
|
1983
|
+
"hydra-acp": { amending: options.amending }
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1738
1986
|
this.broadcastQueueNotification("hydra-acp/prompt_queue_added", params);
|
|
1739
1987
|
}
|
|
1740
1988
|
broadcastQueueUpdated(messageId, prompt) {
|
|
@@ -1857,6 +2105,9 @@ var init_session = __esm({
|
|
|
1857
2105
|
this.broadcastQueueRemoved(messageId, "cancelled");
|
|
1858
2106
|
this.persistRewrite();
|
|
1859
2107
|
}
|
|
2108
|
+
if (this.amendInProgress?.newMessageId === messageId) {
|
|
2109
|
+
this.amendInProgress = void 0;
|
|
2110
|
+
}
|
|
1860
2111
|
entry.resolve({ stopReason: "cancelled" });
|
|
1861
2112
|
return { cancelled: true, reason: "ok" };
|
|
1862
2113
|
}
|
|
@@ -1878,6 +2129,143 @@ var init_session = __esm({
|
|
|
1878
2129
|
this.persistRewrite();
|
|
1879
2130
|
return { updated: true, reason: "ok" };
|
|
1880
2131
|
}
|
|
2132
|
+
// Amend the head prompt: cancel the in-flight turn and submit a
|
|
2133
|
+
// replacement that sits at the head of the queue. Resolves the
|
|
2134
|
+
// request immediately (the caller doesn't wait on cancel-settle).
|
|
2135
|
+
// Honours race outcomes — if the target finished or was cancelled
|
|
2136
|
+
// before this arrived, the request resolves with an outcome explaining
|
|
2137
|
+
// why and (depending on onTargetCompleted) optionally forwards as a
|
|
2138
|
+
// plain prompt. Queued targets are edited in place (same machinery
|
|
2139
|
+
// as updateQueuedPrompt).
|
|
2140
|
+
amendPrompt(clientId, params) {
|
|
2141
|
+
const client = this.clients.get(clientId);
|
|
2142
|
+
if (!client) {
|
|
2143
|
+
throw withCode(
|
|
2144
|
+
new Error("client not attached"),
|
|
2145
|
+
JsonRpcErrorCodes.SessionNotFound
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
const { targetMessageId, prompt, replaceQueue, onTargetCompleted } = params;
|
|
2149
|
+
if (this.currentEntry?.messageId === targetMessageId && this.currentEntry.kind === "user" && !this.currentEntry.cancelled && this.amendInProgress === void 0) {
|
|
2150
|
+
return this.amendOnHead(client, prompt, targetMessageId, replaceQueue);
|
|
2151
|
+
}
|
|
2152
|
+
const queuedEntry = this.promptQueue.find(
|
|
2153
|
+
(e) => e.messageId === targetMessageId && e.kind === "user"
|
|
2154
|
+
);
|
|
2155
|
+
if (queuedEntry && queuedEntry.kind === "user" && !queuedEntry.cancelled) {
|
|
2156
|
+
queuedEntry.prompt = prompt;
|
|
2157
|
+
this.broadcastQueueUpdated(targetMessageId, prompt);
|
|
2158
|
+
this.persistRewrite();
|
|
2159
|
+
return { amended: true, reason: "ok", messageId: targetMessageId };
|
|
2160
|
+
}
|
|
2161
|
+
const terminal = this.recentlyTerminal.get(targetMessageId);
|
|
2162
|
+
if (terminal) {
|
|
2163
|
+
if (terminal.stopReason === "cancelled") {
|
|
2164
|
+
return { amended: false, reason: "target_cancelled" };
|
|
2165
|
+
}
|
|
2166
|
+
if (onTargetCompleted === "send_anyway") {
|
|
2167
|
+
const newMessageId = this.enqueueAmendmentAsFollowUp(client, prompt);
|
|
2168
|
+
return {
|
|
2169
|
+
amended: false,
|
|
2170
|
+
reason: "target_completed",
|
|
2171
|
+
messageId: newMessageId
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
return { amended: false, reason: "target_completed" };
|
|
2175
|
+
}
|
|
2176
|
+
return { amended: false, reason: "target_not_found" };
|
|
2177
|
+
}
|
|
2178
|
+
// Head-of-queue amendment: splice M2 in front of any waiting entries,
|
|
2179
|
+
// broadcast the amend window's queue_added with the amending hint,
|
|
2180
|
+
// mark amendInProgress so the cancelled turn's broadcastTurnComplete
|
|
2181
|
+
// attaches the _meta marker and fires prompt_amended, then fire the
|
|
2182
|
+
// upstream session/cancel without awaiting it. drainQueue is already
|
|
2183
|
+
// running on the head; when its session/prompt returns, it advances
|
|
2184
|
+
// to M2 in the normal way.
|
|
2185
|
+
amendOnHead(client, prompt, targetMessageId, replaceQueue) {
|
|
2186
|
+
const newMessageId = generateMessageId();
|
|
2187
|
+
const originator = { clientId: client.clientId };
|
|
2188
|
+
if (client.clientInfo?.name) {
|
|
2189
|
+
originator.name = client.clientInfo.name;
|
|
2190
|
+
}
|
|
2191
|
+
if (client.clientInfo?.version) {
|
|
2192
|
+
originator.version = client.clientInfo.version;
|
|
2193
|
+
}
|
|
2194
|
+
if (replaceQueue) {
|
|
2195
|
+
const survivors = [];
|
|
2196
|
+
for (const entry2 of this.promptQueue) {
|
|
2197
|
+
if (entry2.kind === "user" && !entry2.cancelled) {
|
|
2198
|
+
entry2.cancelled = true;
|
|
2199
|
+
this.broadcastQueueRemoved(entry2.messageId, "cancelled");
|
|
2200
|
+
entry2.resolve({ stopReason: "cancelled" });
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
survivors.push(entry2);
|
|
2204
|
+
}
|
|
2205
|
+
this.promptQueue = survivors;
|
|
2206
|
+
}
|
|
2207
|
+
const entry = {
|
|
2208
|
+
kind: "user",
|
|
2209
|
+
messageId: newMessageId,
|
|
2210
|
+
originator,
|
|
2211
|
+
clientId: client.clientId,
|
|
2212
|
+
prompt,
|
|
2213
|
+
enqueuedAt: Date.now(),
|
|
2214
|
+
cancelled: false,
|
|
2215
|
+
wasAmend: true,
|
|
2216
|
+
// No-op resolve/reject: there's no client request awaiting M2's
|
|
2217
|
+
// session/prompt response. The amend_prompt request has already
|
|
2218
|
+
// returned by this point. drainQueue calls these unconditionally
|
|
2219
|
+
// when runQueueEntry settles; making them no-ops is safe.
|
|
2220
|
+
resolve: () => void 0,
|
|
2221
|
+
reject: () => void 0
|
|
2222
|
+
};
|
|
2223
|
+
this.promptQueue.unshift(entry);
|
|
2224
|
+
this.persistRewrite();
|
|
2225
|
+
this.broadcastQueueAdded(entry, {
|
|
2226
|
+
amending: targetMessageId,
|
|
2227
|
+
position: 1
|
|
2228
|
+
});
|
|
2229
|
+
this.amendInProgress = {
|
|
2230
|
+
cancelledMessageId: targetMessageId,
|
|
2231
|
+
newMessageId
|
|
2232
|
+
};
|
|
2233
|
+
void this.agent.connection.notify("session/cancel", { sessionId: this.upstreamSessionId }).catch(() => void 0);
|
|
2234
|
+
return {
|
|
2235
|
+
amended: true,
|
|
2236
|
+
reason: "ok",
|
|
2237
|
+
messageId: newMessageId
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
// Send the amendment as a plain follow-up prompt — used when the
|
|
2241
|
+
// target already completed and the caller opted in to send_anyway.
|
|
2242
|
+
// Returns the new prompt's messageId so the result can surface it.
|
|
2243
|
+
enqueueAmendmentAsFollowUp(client, prompt) {
|
|
2244
|
+
const messageId = generateMessageId();
|
|
2245
|
+
const originator = { clientId: client.clientId };
|
|
2246
|
+
if (client.clientInfo?.name) {
|
|
2247
|
+
originator.name = client.clientInfo.name;
|
|
2248
|
+
}
|
|
2249
|
+
if (client.clientInfo?.version) {
|
|
2250
|
+
originator.version = client.clientInfo.version;
|
|
2251
|
+
}
|
|
2252
|
+
const entry = {
|
|
2253
|
+
kind: "user",
|
|
2254
|
+
messageId,
|
|
2255
|
+
originator,
|
|
2256
|
+
clientId: client.clientId,
|
|
2257
|
+
prompt,
|
|
2258
|
+
enqueuedAt: Date.now(),
|
|
2259
|
+
cancelled: false,
|
|
2260
|
+
resolve: () => void 0,
|
|
2261
|
+
reject: () => void 0
|
|
2262
|
+
};
|
|
2263
|
+
this.promptQueue.push(entry);
|
|
2264
|
+
this.persistRewrite();
|
|
2265
|
+
this.broadcastQueueAdded(entry);
|
|
2266
|
+
void this.drainQueue();
|
|
2267
|
+
return messageId;
|
|
2268
|
+
}
|
|
1881
2269
|
async cancel(clientId) {
|
|
1882
2270
|
const client = this.clients.get(clientId);
|
|
1883
2271
|
if (!client) {
|
|
@@ -1928,6 +2316,18 @@ var init_session = __esm({
|
|
|
1928
2316
|
onTitleChange(handler) {
|
|
1929
2317
|
this.titleHandlers.push(handler);
|
|
1930
2318
|
}
|
|
2319
|
+
// External entry point for retitling a live session from outside the
|
|
2320
|
+
// ACP slash-command path (e.g. PATCH /v1/sessions/:id from the picker).
|
|
2321
|
+
// Goes through the same enqueuePrompt path as /hydra title so it
|
|
2322
|
+
// serializes after any in-flight turn and shares broadcast/persistence.
|
|
2323
|
+
retitle(title) {
|
|
2324
|
+
return this.runTitleCommand(title);
|
|
2325
|
+
}
|
|
2326
|
+
// External entry point for the LLM-regen title path (T in the picker,
|
|
2327
|
+
// equivalent to bare /hydra title with no arg).
|
|
2328
|
+
retitleFromAgent() {
|
|
2329
|
+
return this.runTitleCommand("");
|
|
2330
|
+
}
|
|
1931
2331
|
// Update the canonical title and broadcast a session_info_update to
|
|
1932
2332
|
// every attached client. Clients that already speak the spec's
|
|
1933
2333
|
// session_info_update need no hydra-specific wiring to pick this up.
|
|
@@ -1975,12 +2375,19 @@ var init_session = __esm({
|
|
|
1975
2375
|
// Apply an agent-emitted current_model_update. Returns true if the
|
|
1976
2376
|
// notification was a model update (caller still needs to broadcast
|
|
1977
2377
|
// it). Returns false otherwise so the caller can try the next kind.
|
|
2378
|
+
// current_model_update can carry availableModels in the same payload
|
|
2379
|
+
// (per ACP spec); we cache that list too so session/set_model can
|
|
2380
|
+
// validate against it.
|
|
1978
2381
|
maybeApplyAgentModel(params) {
|
|
1979
2382
|
const obj = params ?? {};
|
|
1980
2383
|
const update = obj.update ?? {};
|
|
1981
2384
|
if (update.sessionUpdate !== "current_model_update") {
|
|
1982
2385
|
return false;
|
|
1983
2386
|
}
|
|
2387
|
+
const models = parseModelsList(update.availableModels);
|
|
2388
|
+
if (models.length > 0) {
|
|
2389
|
+
this.setAgentAdvertisedModels(models);
|
|
2390
|
+
}
|
|
1984
2391
|
const raw = typeof update.currentModel === "string" ? update.currentModel : typeof update.model === "string" ? update.model : void 0;
|
|
1985
2392
|
if (raw === void 0) {
|
|
1986
2393
|
return true;
|
|
@@ -1998,6 +2405,55 @@ var init_session = __esm({
|
|
|
1998
2405
|
}
|
|
1999
2406
|
return true;
|
|
2000
2407
|
}
|
|
2408
|
+
// Apply an opencode-style config_option_update. opencode emits this
|
|
2409
|
+
// (not the spec-shaped current_model_update / available_models_update)
|
|
2410
|
+
// to carry both the current model and the list of available models.
|
|
2411
|
+
// The payload is `configOptions: [{ id: "model", currentValue, options:
|
|
2412
|
+
// [{ value, name }] }, ...]`. We harvest only the entry whose id is
|
|
2413
|
+
// "model" — other ids ("mode", "effort", etc.) are opencode-internal
|
|
2414
|
+
// and not consumed by hydra. Returns true when we recognized and
|
|
2415
|
+
// handled the notification so the wireAgent loop can stop trying
|
|
2416
|
+
// further extractors (the broadcast still fires; clients that grok
|
|
2417
|
+
// config_option_update render it directly).
|
|
2418
|
+
maybeApplyAgentConfigOption(params) {
|
|
2419
|
+
const obj = params ?? {};
|
|
2420
|
+
const update = obj.update ?? {};
|
|
2421
|
+
if (update.sessionUpdate !== "config_option_update") {
|
|
2422
|
+
return false;
|
|
2423
|
+
}
|
|
2424
|
+
const list = update.configOptions;
|
|
2425
|
+
if (!Array.isArray(list)) {
|
|
2426
|
+
return true;
|
|
2427
|
+
}
|
|
2428
|
+
for (const raw of list) {
|
|
2429
|
+
if (!raw || typeof raw !== "object") {
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
const opt = raw;
|
|
2433
|
+
if (opt.id !== "model") {
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
const models = parseModelsList(opt.options);
|
|
2437
|
+
if (models.length > 0) {
|
|
2438
|
+
this.setAgentAdvertisedModels(models);
|
|
2439
|
+
}
|
|
2440
|
+
const cv = opt.currentValue;
|
|
2441
|
+
if (typeof cv === "string") {
|
|
2442
|
+
const trimmed = cv.trim();
|
|
2443
|
+
if (trimmed && trimmed !== this.currentModel) {
|
|
2444
|
+
this.currentModel = trimmed;
|
|
2445
|
+
for (const handler of this.modelHandlers) {
|
|
2446
|
+
try {
|
|
2447
|
+
handler(trimmed);
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
break;
|
|
2454
|
+
}
|
|
2455
|
+
return true;
|
|
2456
|
+
}
|
|
2001
2457
|
maybeApplyAgentMode(params) {
|
|
2002
2458
|
const obj = params ?? {};
|
|
2003
2459
|
const update = obj.update ?? {};
|
|
@@ -2096,6 +2552,20 @@ var init_session = __esm({
|
|
|
2096
2552
|
}
|
|
2097
2553
|
this.broadcastAvailableModes();
|
|
2098
2554
|
}
|
|
2555
|
+
setAgentAdvertisedModels(models) {
|
|
2556
|
+
if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
|
|
2557
|
+
this.broadcastAvailableModels();
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
this.agentAdvertisedModels = models;
|
|
2561
|
+
for (const handler of this.agentModelsHandlers) {
|
|
2562
|
+
try {
|
|
2563
|
+
handler(models);
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
this.broadcastAvailableModels();
|
|
2568
|
+
}
|
|
2099
2569
|
// Subscribe to snapshot-state updates. SessionManager wires these to
|
|
2100
2570
|
// persist the new value into meta.json so cold resurrect can restore
|
|
2101
2571
|
// them via the attach response _meta.
|
|
@@ -2105,6 +2575,9 @@ var init_session = __esm({
|
|
|
2105
2575
|
onAgentModesChange(handler) {
|
|
2106
2576
|
this.agentModesHandlers.push(handler);
|
|
2107
2577
|
}
|
|
2578
|
+
onAgentModelsChange(handler) {
|
|
2579
|
+
this.agentModelsHandlers.push(handler);
|
|
2580
|
+
}
|
|
2108
2581
|
onModelChange(handler) {
|
|
2109
2582
|
this.modelHandlers.push(handler);
|
|
2110
2583
|
}
|
|
@@ -2130,6 +2603,15 @@ var init_session = __esm({
|
|
|
2130
2603
|
availableModes() {
|
|
2131
2604
|
return [...this.agentAdvertisedModes];
|
|
2132
2605
|
}
|
|
2606
|
+
// The agent's advertised models list. Used by acp-ws.ts's dedicated
|
|
2607
|
+
// session/set_model handler to validate the requested modelId before
|
|
2608
|
+
// forwarding to the agent (catches cross-agent set_model storms from
|
|
2609
|
+
// clients that assume a different agent is on the other end). When
|
|
2610
|
+
// the agent never advertised any models, returns [] and the
|
|
2611
|
+
// set_model handler falls back to pass-through.
|
|
2612
|
+
availableModels() {
|
|
2613
|
+
return [...this.agentAdvertisedModels];
|
|
2614
|
+
}
|
|
2133
2615
|
// Pick up an agent-emitted session_info_update and store its title
|
|
2134
2616
|
// as our canonical record. The notification is also forwarded to
|
|
2135
2617
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -2277,6 +2759,12 @@ var init_session = __esm({
|
|
|
2277
2759
|
this.agentMeta = fresh.agentMeta;
|
|
2278
2760
|
this.agentAdvertisedCommands = [];
|
|
2279
2761
|
this.broadcastMergedCommands();
|
|
2762
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
2763
|
+
this.setAgentAdvertisedModels([]);
|
|
2764
|
+
}
|
|
2765
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
2766
|
+
this.setAgentAdvertisedModes([]);
|
|
2767
|
+
}
|
|
2280
2768
|
await oldAgent.kill().catch(() => void 0);
|
|
2281
2769
|
if (transcript) {
|
|
2282
2770
|
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
@@ -2736,6 +3224,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2736
3224
|
try {
|
|
2737
3225
|
const result = await this.runQueueEntry(next);
|
|
2738
3226
|
next.resolve(result);
|
|
3227
|
+
await Promise.resolve();
|
|
2739
3228
|
} catch (err) {
|
|
2740
3229
|
next.reject(err);
|
|
2741
3230
|
} finally {
|
|
@@ -2772,12 +3261,33 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2772
3261
|
}
|
|
2773
3262
|
);
|
|
2774
3263
|
} catch (err) {
|
|
2775
|
-
this.broadcastTurnComplete(
|
|
3264
|
+
this.broadcastTurnComplete(
|
|
3265
|
+
entry.clientId,
|
|
3266
|
+
{ stopReason: "error" },
|
|
3267
|
+
entry.messageId,
|
|
3268
|
+
entry.wasAmend
|
|
3269
|
+
);
|
|
3270
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
2776
3271
|
throw err;
|
|
2777
3272
|
}
|
|
2778
|
-
this.broadcastTurnComplete(
|
|
3273
|
+
this.broadcastTurnComplete(
|
|
3274
|
+
entry.clientId,
|
|
3275
|
+
response,
|
|
3276
|
+
entry.messageId,
|
|
3277
|
+
entry.wasAmend
|
|
3278
|
+
);
|
|
3279
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
2779
3280
|
return response;
|
|
2780
3281
|
}
|
|
3282
|
+
// Clear amendInProgress once the cancelled turn's task has fully
|
|
3283
|
+
// settled. broadcastTurnComplete needs the marker still set when it
|
|
3284
|
+
// fires, so the clear must happen *after*. Called from runQueueEntry's
|
|
3285
|
+
// settle path for both success and error.
|
|
3286
|
+
clearAmendIfMatches(messageId) {
|
|
3287
|
+
if (this.amendInProgress?.cancelledMessageId === messageId) {
|
|
3288
|
+
this.amendInProgress = void 0;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
2781
3291
|
};
|
|
2782
3292
|
STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
|
|
2783
3293
|
"session_info_update",
|
|
@@ -2821,11 +3331,12 @@ function recordFromMemorySession(args) {
|
|
|
2821
3331
|
currentUsage: args.currentUsage,
|
|
2822
3332
|
agentCommands: args.agentCommands,
|
|
2823
3333
|
agentModes: args.agentModes,
|
|
3334
|
+
agentModels: args.agentModels,
|
|
2824
3335
|
createdAt: args.createdAt ?? now,
|
|
2825
3336
|
updatedAt: args.updatedAt ?? now
|
|
2826
3337
|
};
|
|
2827
3338
|
}
|
|
2828
|
-
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
3339
|
+
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedAgentModel, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
2829
3340
|
var init_session_store = __esm({
|
|
2830
3341
|
"src/core/session-store.ts"() {
|
|
2831
3342
|
"use strict";
|
|
@@ -2842,6 +3353,11 @@ var init_session_store = __esm({
|
|
|
2842
3353
|
name: z4.string().optional(),
|
|
2843
3354
|
description: z4.string().optional()
|
|
2844
3355
|
});
|
|
3356
|
+
PersistedAgentModel = z4.object({
|
|
3357
|
+
modelId: z4.string(),
|
|
3358
|
+
name: z4.string().optional(),
|
|
3359
|
+
description: z4.string().optional()
|
|
3360
|
+
});
|
|
2845
3361
|
PersistedUsage = z4.object({
|
|
2846
3362
|
used: z4.number().optional(),
|
|
2847
3363
|
size: z4.number().optional(),
|
|
@@ -2888,6 +3404,7 @@ var init_session_store = __esm({
|
|
|
2888
3404
|
currentUsage: PersistedUsage.optional(),
|
|
2889
3405
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
2890
3406
|
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
3407
|
+
agentModels: z4.array(PersistedAgentModel).optional(),
|
|
2891
3408
|
createdAt: z4.string(),
|
|
2892
3409
|
updatedAt: z4.string()
|
|
2893
3410
|
});
|
|
@@ -3383,8 +3900,55 @@ function mapToolCallUpdate(u) {
|
|
|
3383
3900
|
if (status !== void 0) {
|
|
3384
3901
|
event.status = status;
|
|
3385
3902
|
}
|
|
3903
|
+
if (status === "failed") {
|
|
3904
|
+
const errorText = extractToolFailureText(u);
|
|
3905
|
+
if (errorText !== null) {
|
|
3906
|
+
event.errorText = errorText;
|
|
3907
|
+
}
|
|
3908
|
+
if (isUpstreamInterrupted(u, errorText)) {
|
|
3909
|
+
event.upstreamInterrupted = true;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3386
3912
|
return event;
|
|
3387
3913
|
}
|
|
3914
|
+
function extractToolFailureText(u) {
|
|
3915
|
+
const content = u.content;
|
|
3916
|
+
if (Array.isArray(content)) {
|
|
3917
|
+
for (const block of content) {
|
|
3918
|
+
if (!block || typeof block !== "object") {
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
const b = block;
|
|
3922
|
+
const text = extractContentText(b.content);
|
|
3923
|
+
if (text !== null && text.length > 0) {
|
|
3924
|
+
return text;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
const rawOutput = u.rawOutput;
|
|
3929
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
3930
|
+
const err = rawOutput.error;
|
|
3931
|
+
if (typeof err === "string" && err.length > 0) {
|
|
3932
|
+
return sanitizeWireText(err);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
return null;
|
|
3936
|
+
}
|
|
3937
|
+
function isUpstreamInterrupted(u, errorText) {
|
|
3938
|
+
const rawOutput = u.rawOutput;
|
|
3939
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
3940
|
+
const meta = rawOutput.metadata;
|
|
3941
|
+
if (meta && typeof meta === "object") {
|
|
3942
|
+
if (meta.interrupted === true) {
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
if (errorText !== null && errorText.toLowerCase().includes("tool execution aborted")) {
|
|
3948
|
+
return true;
|
|
3949
|
+
}
|
|
3950
|
+
return false;
|
|
3951
|
+
}
|
|
3388
3952
|
function mapPlan(u) {
|
|
3389
3953
|
const entries = u.entries;
|
|
3390
3954
|
if (!Array.isArray(entries)) {
|
|
@@ -3427,7 +3991,16 @@ function mapModel(u) {
|
|
|
3427
3991
|
}
|
|
3428
3992
|
function mapTurnComplete(u) {
|
|
3429
3993
|
const stopReason = readString(u, "stopReason");
|
|
3430
|
-
|
|
3994
|
+
const meta = u._meta;
|
|
3995
|
+
const amended = meta?.["hydra-acp"]?.amended !== void 0 && meta["hydra-acp"].amended !== null;
|
|
3996
|
+
const out = { kind: "turn-complete" };
|
|
3997
|
+
if (stopReason !== void 0) {
|
|
3998
|
+
out.stopReason = stopReason;
|
|
3999
|
+
}
|
|
4000
|
+
if (amended) {
|
|
4001
|
+
out.amended = true;
|
|
4002
|
+
}
|
|
4003
|
+
return out;
|
|
3431
4004
|
}
|
|
3432
4005
|
function extractContentText(content) {
|
|
3433
4006
|
if (typeof content === "string") {
|
|
@@ -4761,6 +5334,34 @@ async function killSession(config, serviceToken, id, fetchImpl = fetch) {
|
|
|
4761
5334
|
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
4762
5335
|
}
|
|
4763
5336
|
}
|
|
5337
|
+
async function renameSession(config, serviceToken, id, title, fetchImpl = fetch) {
|
|
5338
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
5339
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
5340
|
+
method: "PATCH",
|
|
5341
|
+
headers: {
|
|
5342
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
5343
|
+
"Content-Type": "application/json"
|
|
5344
|
+
},
|
|
5345
|
+
body: JSON.stringify({ title })
|
|
5346
|
+
});
|
|
5347
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
5348
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
async function regenSessionTitle(config, serviceToken, id, fetchImpl = fetch) {
|
|
5352
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
5353
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
5354
|
+
method: "PATCH",
|
|
5355
|
+
headers: {
|
|
5356
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
5357
|
+
"Content-Type": "application/json"
|
|
5358
|
+
},
|
|
5359
|
+
body: JSON.stringify({ regen: true })
|
|
5360
|
+
});
|
|
5361
|
+
if (!response.ok && response.status !== 202 && response.status !== 204 && response.status !== 404 && response.status !== 409) {
|
|
5362
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
4764
5365
|
async function deleteSession(config, serviceToken, id, fetchImpl = fetch) {
|
|
4765
5366
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
4766
5367
|
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
@@ -4831,6 +5432,7 @@ async function pickSession(term, opts) {
|
|
|
4831
5432
|
let cwdOnly = false;
|
|
4832
5433
|
let mode = "normal";
|
|
4833
5434
|
let pendingAction = null;
|
|
5435
|
+
let renameBuffer = "";
|
|
4834
5436
|
let transientStatus = null;
|
|
4835
5437
|
let termHeight = readTermHeight(term);
|
|
4836
5438
|
let termWidth = readTermWidth(term);
|
|
@@ -4939,6 +5541,12 @@ async function pickSession(term, opts) {
|
|
|
4939
5541
|
term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
|
|
4940
5542
|
return;
|
|
4941
5543
|
}
|
|
5544
|
+
if (mode === "rename" && pendingAction) {
|
|
5545
|
+
term.brightYellow.noFormat(` title: ${renameBuffer}`);
|
|
5546
|
+
term.bgBrightYellow(" ");
|
|
5547
|
+
term.dim.noFormat(" Enter saves \xB7 Esc cancels");
|
|
5548
|
+
return;
|
|
5549
|
+
}
|
|
4942
5550
|
if (transientStatus !== null) {
|
|
4943
5551
|
term.dim.noFormat(` ${transientStatus}`);
|
|
4944
5552
|
return;
|
|
@@ -5056,6 +5664,37 @@ async function pickSession(term, opts) {
|
|
|
5056
5664
|
renderFromScratch();
|
|
5057
5665
|
}
|
|
5058
5666
|
};
|
|
5667
|
+
const performRename = async (title) => {
|
|
5668
|
+
if (!pendingAction) {
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
const target = pendingAction;
|
|
5672
|
+
mode = "busy";
|
|
5673
|
+
paintIndicator();
|
|
5674
|
+
try {
|
|
5675
|
+
await renameSession(opts.config, opts.serviceToken, target.sessionId, title);
|
|
5676
|
+
mode = "normal";
|
|
5677
|
+
pendingAction = null;
|
|
5678
|
+
renameBuffer = "";
|
|
5679
|
+
await refresh(target.sessionId);
|
|
5680
|
+
} catch (err) {
|
|
5681
|
+
mode = "normal";
|
|
5682
|
+
pendingAction = null;
|
|
5683
|
+
renameBuffer = "";
|
|
5684
|
+
transientStatus = `rename failed: ${err.message}`;
|
|
5685
|
+
paintIndicator();
|
|
5686
|
+
}
|
|
5687
|
+
};
|
|
5688
|
+
const performRegen = async (target) => {
|
|
5689
|
+
try {
|
|
5690
|
+
await regenSessionTitle(opts.config, opts.serviceToken, target.sessionId);
|
|
5691
|
+
transientStatus = "title regen queued (press r to refresh)";
|
|
5692
|
+
paintIndicator();
|
|
5693
|
+
} catch (err) {
|
|
5694
|
+
transientStatus = `regen failed: ${err.message}`;
|
|
5695
|
+
paintIndicator();
|
|
5696
|
+
}
|
|
5697
|
+
};
|
|
5059
5698
|
const performAction = async (kind) => {
|
|
5060
5699
|
if (!pendingAction) {
|
|
5061
5700
|
return;
|
|
@@ -5128,19 +5767,65 @@ async function pickSession(term, opts) {
|
|
|
5128
5767
|
renderFromScratch();
|
|
5129
5768
|
return;
|
|
5130
5769
|
}
|
|
5131
|
-
if (mode === "
|
|
5132
|
-
if (
|
|
5133
|
-
const
|
|
5134
|
-
|
|
5770
|
+
if (mode === "rename") {
|
|
5771
|
+
if (name === "ENTER" || name === "KP_ENTER") {
|
|
5772
|
+
const trimmed = renameBuffer.trim();
|
|
5773
|
+
if (trimmed.length === 0) {
|
|
5774
|
+
mode = "normal";
|
|
5775
|
+
pendingAction = null;
|
|
5776
|
+
renameBuffer = "";
|
|
5777
|
+
paintIndicator();
|
|
5778
|
+
return;
|
|
5779
|
+
}
|
|
5780
|
+
void performRename(trimmed);
|
|
5135
5781
|
return;
|
|
5136
5782
|
}
|
|
5137
|
-
if (name === "ESCAPE" || name === "CTRL_C"
|
|
5783
|
+
if (name === "ESCAPE" || name === "CTRL_C") {
|
|
5138
5784
|
mode = "normal";
|
|
5139
5785
|
pendingAction = null;
|
|
5786
|
+
renameBuffer = "";
|
|
5140
5787
|
paintIndicator();
|
|
5141
5788
|
return;
|
|
5142
5789
|
}
|
|
5143
|
-
|
|
5790
|
+
if (name === "BACKSPACE") {
|
|
5791
|
+
if (renameBuffer.length > 0) {
|
|
5792
|
+
renameBuffer = renameBuffer.slice(0, -1);
|
|
5793
|
+
paintIndicator();
|
|
5794
|
+
}
|
|
5795
|
+
return;
|
|
5796
|
+
}
|
|
5797
|
+
if (name === "CTRL_U") {
|
|
5798
|
+
renameBuffer = "";
|
|
5799
|
+
paintIndicator();
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
if (name === "CTRL_W") {
|
|
5803
|
+
const trimmedRight = renameBuffer.replace(/\s+$/, "");
|
|
5804
|
+
const lastSpace = trimmedRight.lastIndexOf(" ");
|
|
5805
|
+
renameBuffer = lastSpace >= 0 ? trimmedRight.slice(0, lastSpace) : "";
|
|
5806
|
+
paintIndicator();
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
if (data?.isCharacter) {
|
|
5810
|
+
renameBuffer += name;
|
|
5811
|
+
paintIndicator();
|
|
5812
|
+
return;
|
|
5813
|
+
}
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
if (mode === "confirm-kill" || mode === "confirm-delete") {
|
|
5817
|
+
if (data?.isCharacter && (name === "y" || name === "Y")) {
|
|
5818
|
+
const kind = mode === "confirm-kill" ? "kill" : "delete";
|
|
5819
|
+
void performAction(kind);
|
|
5820
|
+
return;
|
|
5821
|
+
}
|
|
5822
|
+
if (name === "ESCAPE" || name === "CTRL_C" || name === "ENTER" || name === "KP_ENTER" || data?.isCharacter && (name === "n" || name === "N")) {
|
|
5823
|
+
mode = "normal";
|
|
5824
|
+
pendingAction = null;
|
|
5825
|
+
paintIndicator();
|
|
5826
|
+
return;
|
|
5827
|
+
}
|
|
5828
|
+
return;
|
|
5144
5829
|
}
|
|
5145
5830
|
clearTransient();
|
|
5146
5831
|
if (!searchActive && data?.isCharacter && name === "?") {
|
|
@@ -5230,6 +5915,29 @@ async function pickSession(term, opts) {
|
|
|
5230
5915
|
paintIndicator();
|
|
5231
5916
|
return;
|
|
5232
5917
|
}
|
|
5918
|
+
if (name === "t" && selectedIdx > 0) {
|
|
5919
|
+
const session = visible[selectedIdx - 1];
|
|
5920
|
+
if (!session) {
|
|
5921
|
+
return;
|
|
5922
|
+
}
|
|
5923
|
+
pendingAction = {
|
|
5924
|
+
sessionId: session.sessionId,
|
|
5925
|
+
cwd: session.cwd,
|
|
5926
|
+
status: session.status
|
|
5927
|
+
};
|
|
5928
|
+
renameBuffer = session.title ?? "";
|
|
5929
|
+
mode = "rename";
|
|
5930
|
+
paintIndicator();
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5933
|
+
if (name === "T" && selectedIdx > 0) {
|
|
5934
|
+
const session = visible[selectedIdx - 1];
|
|
5935
|
+
if (!session || session.status !== "live") {
|
|
5936
|
+
return;
|
|
5937
|
+
}
|
|
5938
|
+
void performRegen({ sessionId: session.sessionId });
|
|
5939
|
+
return;
|
|
5940
|
+
}
|
|
5233
5941
|
if ((name === "d" || name === "D") && selectedIdx > 0) {
|
|
5234
5942
|
const session = visible[selectedIdx - 1];
|
|
5235
5943
|
if (!session) {
|
|
@@ -5360,6 +6068,8 @@ var init_picker = __esm({
|
|
|
5360
6068
|
null,
|
|
5361
6069
|
["k", "kill the selected live session"],
|
|
5362
6070
|
["d", "delete the selected cold session"],
|
|
6071
|
+
["t", "retitle the selected session"],
|
|
6072
|
+
["T", "regenerate title via agent (live session)"],
|
|
5363
6073
|
null,
|
|
5364
6074
|
["c", "create new session"],
|
|
5365
6075
|
["?", "toggle this help"],
|
|
@@ -5947,6 +6657,12 @@ function mapKeyName(name) {
|
|
|
5947
6657
|
case "ALT_ENTER":
|
|
5948
6658
|
case "META_ENTER":
|
|
5949
6659
|
return "alt-enter";
|
|
6660
|
+
case "SHIFT_ENTER":
|
|
6661
|
+
return "shift-enter";
|
|
6662
|
+
case "CTRL_ENTER":
|
|
6663
|
+
return "ctrl-enter";
|
|
6664
|
+
case "CTRL_J":
|
|
6665
|
+
return "ctrl-enter";
|
|
5950
6666
|
case "ALT_B":
|
|
5951
6667
|
case "META_B":
|
|
5952
6668
|
return "alt-b";
|
|
@@ -6216,12 +6932,15 @@ var init_screen = __esm({
|
|
|
6216
6932
|
this.term.fullscreen(false);
|
|
6217
6933
|
this.term("\n");
|
|
6218
6934
|
}
|
|
6219
|
-
// Enables bracketed paste mode on the terminal and
|
|
6220
|
-
// see the \x1b[200~/\x1b[201~ markers
|
|
6221
|
-
//
|
|
6222
|
-
//
|
|
6935
|
+
// Enables bracketed paste mode + modifyOtherKeys on the terminal and
|
|
6936
|
+
// rewires stdin so we see the \x1b[200~/\x1b[201~ paste markers and
|
|
6937
|
+
// CSI-u modified-key sequences (Shift+Enter etc.) BEFORE terminal-kit's
|
|
6938
|
+
// key parser. Non-special data is forwarded to terminal-kit unchanged.
|
|
6223
6939
|
installBracketedPaste() {
|
|
6224
6940
|
process.stdout.write("\x1B[?2004h");
|
|
6941
|
+
process.stdout.write("\x1B[>4;2m");
|
|
6942
|
+
process.stdout.write("\x1B[>5;1m");
|
|
6943
|
+
process.stdout.write("\x1B[>1u");
|
|
6225
6944
|
const t = this.term;
|
|
6226
6945
|
if (!t.stdin || typeof t.onStdin !== "function") {
|
|
6227
6946
|
return;
|
|
@@ -6232,6 +6951,9 @@ var init_screen = __esm({
|
|
|
6232
6951
|
}
|
|
6233
6952
|
uninstallBracketedPaste() {
|
|
6234
6953
|
process.stdout.write("\x1B[?2004l");
|
|
6954
|
+
process.stdout.write("\x1B[>4;0m");
|
|
6955
|
+
process.stdout.write("\x1B[>5;0m");
|
|
6956
|
+
process.stdout.write("\x1B[<u");
|
|
6235
6957
|
const t = this.term;
|
|
6236
6958
|
if (!t.stdin || this.terminalKitStdinHandler === null) {
|
|
6237
6959
|
return;
|
|
@@ -6244,6 +6966,38 @@ var init_screen = __esm({
|
|
|
6244
6966
|
}
|
|
6245
6967
|
handleRawStdin(chunk) {
|
|
6246
6968
|
let text = chunk.toString("binary");
|
|
6969
|
+
if (!this.pasteActive) {
|
|
6970
|
+
const markers = [
|
|
6971
|
+
{ seq: "\x1B[13;2u", name: "shift-enter" },
|
|
6972
|
+
{ seq: "\x1B[27;2;13~", name: "shift-enter" },
|
|
6973
|
+
{ seq: "\x1B[13;5u", name: "ctrl-enter" },
|
|
6974
|
+
{ seq: "\x1B[27;5;13~", name: "ctrl-enter" },
|
|
6975
|
+
// Bare LF — universal fallback for terminals without
|
|
6976
|
+
// modifyOtherKeys / kitty protocol. Last so the longer escape
|
|
6977
|
+
// sequences match first and we don't double-fire.
|
|
6978
|
+
{ seq: "\n", name: "ctrl-enter" }
|
|
6979
|
+
];
|
|
6980
|
+
for (const { seq, name } of markers) {
|
|
6981
|
+
if (text.includes(seq)) {
|
|
6982
|
+
const parts = text.split(seq);
|
|
6983
|
+
for (let i = 0; i < parts.length; i++) {
|
|
6984
|
+
if (parts[i].length > 0) {
|
|
6985
|
+
this.handleRawStdin(Buffer.from(parts[i], "binary"));
|
|
6986
|
+
}
|
|
6987
|
+
if (i < parts.length - 1) {
|
|
6988
|
+
this.onKey([{ type: "key", name }]);
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
return;
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
}
|
|
6995
|
+
this.handleRawStdinSegment(text);
|
|
6996
|
+
}
|
|
6997
|
+
// Inner stdin-segment handler — paste-marker detection and forwarding
|
|
6998
|
+
// to terminal-kit. Split out so shift-enter interception can call it
|
|
6999
|
+
// for the non-shift-enter portions of a mixed chunk.
|
|
7000
|
+
handleRawStdinSegment(text) {
|
|
6247
7001
|
const startMarker = "\x1B[200~";
|
|
6248
7002
|
const endMarker = "\x1B[201~";
|
|
6249
7003
|
while (text.length > 0) {
|
|
@@ -7534,11 +8288,16 @@ var init_screen = __esm({
|
|
|
7534
8288
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
7535
8289
|
const right = this.bannerRightContent();
|
|
7536
8290
|
const rightSig = right ? `${right.kind}|${right.text}` : "";
|
|
7537
|
-
const
|
|
8291
|
+
const stalled = this.banner.status === "busy" && this.banner.stalled === true;
|
|
8292
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${stalled ? "1" : "0"}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
|
|
7538
8293
|
this.paintRow(row, sig, () => {
|
|
7539
8294
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
7540
8295
|
if (this.banner.status === "busy") {
|
|
7541
|
-
|
|
8296
|
+
if (stalled) {
|
|
8297
|
+
this.term.brightRed(`${dot} stalled`);
|
|
8298
|
+
} else {
|
|
8299
|
+
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
8300
|
+
}
|
|
7542
8301
|
if (elapsedStr) {
|
|
7543
8302
|
this.term(" ").dim(elapsedStr);
|
|
7544
8303
|
}
|
|
@@ -7944,6 +8703,9 @@ var init_input = __esm({
|
|
|
7944
8703
|
switch (name) {
|
|
7945
8704
|
case "enter":
|
|
7946
8705
|
return this.send();
|
|
8706
|
+
case "shift-enter":
|
|
8707
|
+
case "ctrl-enter":
|
|
8708
|
+
return this.amend();
|
|
7947
8709
|
case "alt-enter":
|
|
7948
8710
|
this.insertNewline();
|
|
7949
8711
|
return [];
|
|
@@ -8130,22 +8892,64 @@ var init_input = __esm({
|
|
|
8130
8892
|
this.setCurrentLine(line + next);
|
|
8131
8893
|
}
|
|
8132
8894
|
}
|
|
8895
|
+
// ^U: kill from cursor to start of current line. At col 0 with a line
|
|
8896
|
+
// above:
|
|
8897
|
+
// - If the current line is empty, collapse it (kill just the
|
|
8898
|
+
// newline) so the cursor lands at the end of the previous line.
|
|
8899
|
+
// Don't slurp that line's contents.
|
|
8900
|
+
// - Otherwise, kill the previous line entirely + the joining
|
|
8901
|
+
// newline, so ^U from the start of a non-empty line walks up
|
|
8902
|
+
// line-by-line.
|
|
8903
|
+
// Single-line behavior is unchanged.
|
|
8133
8904
|
killLine() {
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
this.
|
|
8905
|
+
if (this.col > 0) {
|
|
8906
|
+
const line = this.currentLine();
|
|
8907
|
+
this.killBuffer = line.slice(0, this.col);
|
|
8908
|
+
this.setCurrentLine(line.slice(this.col));
|
|
8909
|
+
this.col = 0;
|
|
8910
|
+
return;
|
|
8138
8911
|
}
|
|
8139
|
-
|
|
8140
|
-
|
|
8912
|
+
if (this.row === 0) {
|
|
8913
|
+
return;
|
|
8914
|
+
}
|
|
8915
|
+
if (this.currentLine().length === 0) {
|
|
8916
|
+
this.killBuffer = "\n";
|
|
8917
|
+
this.buffer.splice(this.row, 1);
|
|
8918
|
+
this.row -= 1;
|
|
8919
|
+
this.col = this.currentLine().length;
|
|
8920
|
+
return;
|
|
8921
|
+
}
|
|
8922
|
+
const prev = this.buffer[this.row - 1] ?? "";
|
|
8923
|
+
this.killBuffer = prev + "\n";
|
|
8924
|
+
this.buffer.splice(this.row - 1, 1);
|
|
8925
|
+
this.row -= 1;
|
|
8141
8926
|
}
|
|
8927
|
+
// ^K: kill from cursor to end of current line. At end-of-line with a
|
|
8928
|
+
// line below:
|
|
8929
|
+
// - If the current line is empty, collapse it (kill just the
|
|
8930
|
+
// newline) so what was the next line takes its place. Don't slurp
|
|
8931
|
+
// that line's contents.
|
|
8932
|
+
// - Otherwise, kill the joining newline + the entire next line, so
|
|
8933
|
+
// ^K from the end of a non-empty line walks down line-by-line.
|
|
8934
|
+
// Single-line behavior is unchanged.
|
|
8142
8935
|
killToEnd() {
|
|
8143
8936
|
const line = this.currentLine();
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
this.
|
|
8937
|
+
if (this.col < line.length) {
|
|
8938
|
+
this.killBuffer = line.slice(this.col);
|
|
8939
|
+
this.setCurrentLine(line.slice(0, this.col));
|
|
8940
|
+
return;
|
|
8941
|
+
}
|
|
8942
|
+
if (this.row >= this.buffer.length - 1) {
|
|
8943
|
+
return;
|
|
8147
8944
|
}
|
|
8148
|
-
|
|
8945
|
+
if (line.length === 0) {
|
|
8946
|
+
this.killBuffer = "\n";
|
|
8947
|
+
this.buffer.splice(this.row, 1);
|
|
8948
|
+
return;
|
|
8949
|
+
}
|
|
8950
|
+
const next = this.buffer[this.row + 1] ?? "";
|
|
8951
|
+
this.killBuffer = "\n" + next;
|
|
8952
|
+
this.buffer.splice(this.row + 1, 1);
|
|
8149
8953
|
}
|
|
8150
8954
|
killWord() {
|
|
8151
8955
|
const line = this.currentLine();
|
|
@@ -8494,6 +9298,31 @@ var init_input = __esm({
|
|
|
8494
9298
|
this.clearBuffer();
|
|
8495
9299
|
return [{ type: "send", text, planMode, attachments }];
|
|
8496
9300
|
}
|
|
9301
|
+
// Shift+Enter: amend the in-flight turn. Editing a queued slot
|
|
9302
|
+
// delegates to the existing queue-edit / queue-remove path — Shift+Enter
|
|
9303
|
+
// there has no special meaning since the entry is already queued (not
|
|
9304
|
+
// running). With an empty draft and no attachments we emit nothing
|
|
9305
|
+
// (no-op). Otherwise emit an "amend" effect; the app decides whether
|
|
9306
|
+
// to route through amend_prompt or fall through to a regular send.
|
|
9307
|
+
amend() {
|
|
9308
|
+
const text = this.bufferText();
|
|
9309
|
+
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
9310
|
+
const index = this.queueIndex;
|
|
9311
|
+
const attachments2 = [...this.attachments];
|
|
9312
|
+
this.clearBuffer();
|
|
9313
|
+
if (text.trim().length === 0) {
|
|
9314
|
+
return [{ type: "queue-remove", index }];
|
|
9315
|
+
}
|
|
9316
|
+
return [{ type: "queue-edit", index, text, attachments: attachments2 }];
|
|
9317
|
+
}
|
|
9318
|
+
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
9319
|
+
return [];
|
|
9320
|
+
}
|
|
9321
|
+
const planMode = this.planMode;
|
|
9322
|
+
const attachments = [...this.attachments];
|
|
9323
|
+
this.clearBuffer();
|
|
9324
|
+
return [{ type: "amend", text, planMode, attachments }];
|
|
9325
|
+
}
|
|
8497
9326
|
// Home: jump to the very start of the prompt buffer. If we're already
|
|
8498
9327
|
// there, fall through to scrolling the scrollback to its top.
|
|
8499
9328
|
handleHome() {
|
|
@@ -8605,26 +9434,34 @@ async function readLinux(env) {
|
|
|
8605
9434
|
reason: "install wl-clipboard (Wayland) or xclip (X11) to paste from the clipboard"
|
|
8606
9435
|
};
|
|
8607
9436
|
}
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
9437
|
+
const targets = await listTargets(env, tool);
|
|
9438
|
+
const imageMime = pickImageTarget(targets);
|
|
9439
|
+
if (imageMime) {
|
|
9440
|
+
try {
|
|
9441
|
+
const buf = await runCapture(
|
|
9442
|
+
env.spawn,
|
|
9443
|
+
tool.cmd,
|
|
9444
|
+
tool.imageArgs(imageMime)
|
|
9445
|
+
);
|
|
9446
|
+
if (buf.length > 0) {
|
|
9447
|
+
if (buf.length > MAX_ATTACHMENT_BYTES) {
|
|
9448
|
+
return {
|
|
9449
|
+
ok: false,
|
|
9450
|
+
reason: `clipboard image is ${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)}`
|
|
9451
|
+
};
|
|
9452
|
+
}
|
|
8612
9453
|
return {
|
|
8613
|
-
ok:
|
|
8614
|
-
|
|
9454
|
+
ok: true,
|
|
9455
|
+
kind: "image",
|
|
9456
|
+
attachment: {
|
|
9457
|
+
mimeType: imageMime,
|
|
9458
|
+
data: buf.toString("base64"),
|
|
9459
|
+
sizeBytes: buf.length
|
|
9460
|
+
}
|
|
8615
9461
|
};
|
|
8616
9462
|
}
|
|
8617
|
-
|
|
8618
|
-
ok: true,
|
|
8619
|
-
kind: "image",
|
|
8620
|
-
attachment: {
|
|
8621
|
-
mimeType: "image/png",
|
|
8622
|
-
data: buf.toString("base64"),
|
|
8623
|
-
sizeBytes: buf.length
|
|
8624
|
-
}
|
|
8625
|
-
};
|
|
9463
|
+
} catch {
|
|
8626
9464
|
}
|
|
8627
|
-
} catch {
|
|
8628
9465
|
}
|
|
8629
9466
|
try {
|
|
8630
9467
|
const buf = await runCapture(env.spawn, tool.cmd, tool.textArgs);
|
|
@@ -8644,7 +9481,8 @@ async function detectLinuxTool(env) {
|
|
|
8644
9481
|
if (env.env.WAYLAND_DISPLAY && await which(env, "wl-paste")) {
|
|
8645
9482
|
return {
|
|
8646
9483
|
cmd: "wl-paste",
|
|
8647
|
-
|
|
9484
|
+
listTargetsArgs: ["--list-types"],
|
|
9485
|
+
imageArgs: (mime) => ["-t", mime],
|
|
8648
9486
|
// -n: drop trailing newline wl-paste adds by default. We further
|
|
8649
9487
|
// normalize line endings below, but this avoids a spurious
|
|
8650
9488
|
// empty trailing row from a single-line clipboard text.
|
|
@@ -8654,12 +9492,30 @@ async function detectLinuxTool(env) {
|
|
|
8654
9492
|
if (env.env.DISPLAY && await which(env, "xclip")) {
|
|
8655
9493
|
return {
|
|
8656
9494
|
cmd: "xclip",
|
|
8657
|
-
|
|
9495
|
+
listTargetsArgs: ["-selection", "clipboard", "-t", "TARGETS", "-o"],
|
|
9496
|
+
imageArgs: (mime) => ["-selection", "clipboard", "-t", mime, "-o"],
|
|
8658
9497
|
textArgs: ["-selection", "clipboard", "-o"]
|
|
8659
9498
|
};
|
|
8660
9499
|
}
|
|
8661
9500
|
return null;
|
|
8662
9501
|
}
|
|
9502
|
+
function pickImageTarget(targets) {
|
|
9503
|
+
const offered = new Set(targets.map((t) => t.toLowerCase()));
|
|
9504
|
+
for (const mime of SUPPORTED_IMAGE_MIMES) {
|
|
9505
|
+
if (offered.has(mime)) {
|
|
9506
|
+
return mime;
|
|
9507
|
+
}
|
|
9508
|
+
}
|
|
9509
|
+
return null;
|
|
9510
|
+
}
|
|
9511
|
+
async function listTargets(env, tool) {
|
|
9512
|
+
try {
|
|
9513
|
+
const buf = await runCapture(env.spawn, tool.cmd, tool.listTargetsArgs);
|
|
9514
|
+
return buf.toString("utf-8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
9515
|
+
} catch {
|
|
9516
|
+
return [];
|
|
9517
|
+
}
|
|
9518
|
+
}
|
|
8663
9519
|
function normalizeText(text) {
|
|
8664
9520
|
return text.replace(/\r\n?/g, "\n");
|
|
8665
9521
|
}
|
|
@@ -8754,7 +9610,7 @@ function runCapture(spawn6, cmd, args) {
|
|
|
8754
9610
|
});
|
|
8755
9611
|
});
|
|
8756
9612
|
}
|
|
8757
|
-
var defaultEnv;
|
|
9613
|
+
var defaultEnv, SUPPORTED_IMAGE_MIMES;
|
|
8758
9614
|
var init_clipboard = __esm({
|
|
8759
9615
|
"src/tui/clipboard.ts"() {
|
|
8760
9616
|
"use strict";
|
|
@@ -8765,6 +9621,12 @@ var init_clipboard = __esm({
|
|
|
8765
9621
|
spawn: nodeSpawn,
|
|
8766
9622
|
tmpdir: os4.tmpdir
|
|
8767
9623
|
};
|
|
9624
|
+
SUPPORTED_IMAGE_MIMES = [
|
|
9625
|
+
"image/png",
|
|
9626
|
+
"image/jpeg",
|
|
9627
|
+
"image/gif",
|
|
9628
|
+
"image/webp"
|
|
9629
|
+
];
|
|
8768
9630
|
}
|
|
8769
9631
|
});
|
|
8770
9632
|
|
|
@@ -8812,8 +9674,29 @@ var init_completion = __esm({
|
|
|
8812
9674
|
}
|
|
8813
9675
|
});
|
|
8814
9676
|
|
|
9677
|
+
// src/tui/reconnect-state.ts
|
|
9678
|
+
function parseReattachResponse(result) {
|
|
9679
|
+
const out = {};
|
|
9680
|
+
if (!result || typeof result !== "object") {
|
|
9681
|
+
return out;
|
|
9682
|
+
}
|
|
9683
|
+
const r = result;
|
|
9684
|
+
if (typeof r.historyPolicy === "string") {
|
|
9685
|
+
out.appliedPolicy = r.historyPolicy;
|
|
9686
|
+
}
|
|
9687
|
+
if (typeof r.clientId === "string" && r.clientId.length > 0) {
|
|
9688
|
+
out.clientId = r.clientId;
|
|
9689
|
+
}
|
|
9690
|
+
return out;
|
|
9691
|
+
}
|
|
9692
|
+
var init_reconnect_state = __esm({
|
|
9693
|
+
"src/tui/reconnect-state.ts"() {
|
|
9694
|
+
"use strict";
|
|
9695
|
+
}
|
|
9696
|
+
});
|
|
9697
|
+
|
|
8815
9698
|
// src/tui/format.ts
|
|
8816
|
-
import
|
|
9699
|
+
import chalk2 from "chalk";
|
|
8817
9700
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
8818
9701
|
function formatEvent(event) {
|
|
8819
9702
|
switch (event.kind) {
|
|
@@ -8905,7 +9788,8 @@ function parseAgentMarkdown(text) {
|
|
|
8905
9788
|
codeBuffer = [];
|
|
8906
9789
|
codeLang = "";
|
|
8907
9790
|
};
|
|
8908
|
-
for (
|
|
9791
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9792
|
+
const line = lines[i];
|
|
8909
9793
|
const fence = line.match(/^\s*```\s*(\w*)\s*$/);
|
|
8910
9794
|
if (fence) {
|
|
8911
9795
|
if (!inCode) {
|
|
@@ -8933,6 +9817,19 @@ function parseAgentMarkdown(text) {
|
|
|
8933
9817
|
});
|
|
8934
9818
|
continue;
|
|
8935
9819
|
}
|
|
9820
|
+
const next = lines[i + 1];
|
|
9821
|
+
if (line.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(line).length === parseTableRow(next).length) {
|
|
9822
|
+
const header = parseTableRow(line);
|
|
9823
|
+
const body = [];
|
|
9824
|
+
let j = i + 2;
|
|
9825
|
+
while (j < lines.length && lines[j].includes("|")) {
|
|
9826
|
+
body.push(parseTableRow(lines[j]));
|
|
9827
|
+
j++;
|
|
9828
|
+
}
|
|
9829
|
+
out.push(...formatTable(header, body));
|
|
9830
|
+
i = j - 1;
|
|
9831
|
+
continue;
|
|
9832
|
+
}
|
|
8936
9833
|
const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
|
|
8937
9834
|
if (bullet) {
|
|
8938
9835
|
const indent = bullet[1] ?? "";
|
|
@@ -8967,6 +9864,70 @@ function parseAgentMarkdown(text) {
|
|
|
8967
9864
|
}
|
|
8968
9865
|
return out;
|
|
8969
9866
|
}
|
|
9867
|
+
function parseTableRow(line) {
|
|
9868
|
+
let s = line.trim();
|
|
9869
|
+
if (s.startsWith("|")) {
|
|
9870
|
+
s = s.slice(1);
|
|
9871
|
+
}
|
|
9872
|
+
if (s.endsWith("|")) {
|
|
9873
|
+
s = s.slice(0, -1);
|
|
9874
|
+
}
|
|
9875
|
+
return s.split("|").map((c) => c.trim());
|
|
9876
|
+
}
|
|
9877
|
+
function isTableSeparatorLine(line) {
|
|
9878
|
+
if (!line.includes("|")) {
|
|
9879
|
+
return false;
|
|
9880
|
+
}
|
|
9881
|
+
const cells = parseTableRow(line);
|
|
9882
|
+
if (cells.length === 0) {
|
|
9883
|
+
return false;
|
|
9884
|
+
}
|
|
9885
|
+
return cells.every((c) => /^:?-+:?$/.test(c));
|
|
9886
|
+
}
|
|
9887
|
+
function formatTable(header, body) {
|
|
9888
|
+
const cols = header.length;
|
|
9889
|
+
const widths = new Array(cols).fill(0);
|
|
9890
|
+
for (let c = 0; c < cols; c++) {
|
|
9891
|
+
widths[c] = header[c]?.length ?? 0;
|
|
9892
|
+
}
|
|
9893
|
+
for (const row of body) {
|
|
9894
|
+
for (let c = 0; c < cols; c++) {
|
|
9895
|
+
const cell = row[c] ?? "";
|
|
9896
|
+
if (cell.length > widths[c]) {
|
|
9897
|
+
widths[c] = cell.length;
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
}
|
|
9901
|
+
const renderRow = (cells, style) => {
|
|
9902
|
+
const padded = [];
|
|
9903
|
+
for (let c = 0; c < cols; c++) {
|
|
9904
|
+
const cell = cells[c] ?? "";
|
|
9905
|
+
const w = widths[c];
|
|
9906
|
+
const marked = applyInlineMarkup(cell);
|
|
9907
|
+
padded.push(marked + " ".repeat(Math.max(0, w - cell.length)));
|
|
9908
|
+
}
|
|
9909
|
+
return {
|
|
9910
|
+
prefix: " ",
|
|
9911
|
+
body: padded.join(" \u2502 "),
|
|
9912
|
+
bodyStyle: style
|
|
9913
|
+
};
|
|
9914
|
+
};
|
|
9915
|
+
const out = [];
|
|
9916
|
+
out.push(renderRow(header, "heading-3"));
|
|
9917
|
+
const rules = [];
|
|
9918
|
+
for (let c = 0; c < cols; c++) {
|
|
9919
|
+
rules.push("\u2500".repeat(widths[c]));
|
|
9920
|
+
}
|
|
9921
|
+
out.push({
|
|
9922
|
+
prefix: " ",
|
|
9923
|
+
body: rules.join("\u2500\u253C\u2500"),
|
|
9924
|
+
bodyStyle: "dim"
|
|
9925
|
+
});
|
|
9926
|
+
for (const row of body) {
|
|
9927
|
+
out.push(renderRow(row, "agent"));
|
|
9928
|
+
}
|
|
9929
|
+
return out;
|
|
9930
|
+
}
|
|
8970
9931
|
function highlightFencedBlock(lang, lines) {
|
|
8971
9932
|
if (lang.length === 0 || !supportsLanguage(lang)) {
|
|
8972
9933
|
return lines.map((body) => ({ body, ansi: false }));
|
|
@@ -9028,12 +9989,22 @@ function formatToolLine2(state) {
|
|
|
9028
9989
|
} else {
|
|
9029
9990
|
title = `${initial} \xB7 ${latest}`;
|
|
9030
9991
|
}
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9992
|
+
const lines = [
|
|
9993
|
+
{
|
|
9994
|
+
prefix: ` ${toolStatusIcon(state.status)} `,
|
|
9995
|
+
prefixStyle: toolIconStyle(state.status),
|
|
9996
|
+
body: title,
|
|
9997
|
+
bodyStyle: toolStatusStyle(state.status)
|
|
9998
|
+
}
|
|
9999
|
+
];
|
|
10000
|
+
if (state.status === "failed" && state.errorText) {
|
|
10001
|
+
lines.push({
|
|
10002
|
+
prefix: " ",
|
|
10003
|
+
body: sanitizeSingleLine(state.errorText),
|
|
10004
|
+
bodyStyle: "tool-status-fail"
|
|
10005
|
+
});
|
|
10006
|
+
}
|
|
10007
|
+
return lines;
|
|
9037
10008
|
}
|
|
9038
10009
|
function toolStatusIcon(status) {
|
|
9039
10010
|
switch (status) {
|
|
@@ -9132,7 +10103,8 @@ var highlightChalk, HIGHLIGHT_THEME;
|
|
|
9132
10103
|
var init_format = __esm({
|
|
9133
10104
|
"src/tui/format.ts"() {
|
|
9134
10105
|
"use strict";
|
|
9135
|
-
|
|
10106
|
+
init_render_update();
|
|
10107
|
+
highlightChalk = new chalk2.Instance({ level: 3 });
|
|
9136
10108
|
HIGHLIGHT_THEME = {
|
|
9137
10109
|
keyword: highlightChalk.blueBright,
|
|
9138
10110
|
built_in: highlightChalk.cyan,
|
|
@@ -9194,8 +10166,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9194
10166
|
term.grabInput(false);
|
|
9195
10167
|
process.exit(0);
|
|
9196
10168
|
}
|
|
9197
|
-
const
|
|
9198
|
-
term
|
|
10169
|
+
const launchLabelBase = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
10170
|
+
const installStatus = createInstallStatusLine(term, launchLabelBase);
|
|
10171
|
+
installStatus.write(launchLabelBase);
|
|
9199
10172
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
9200
10173
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
9201
10174
|
const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
|
|
@@ -9221,6 +10194,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9221
10194
|
});
|
|
9222
10195
|
const conn = new JsonRpcConnection(stream);
|
|
9223
10196
|
await stream.start();
|
|
10197
|
+
conn.onNotification(AGENT_INSTALL_PROGRESS_METHOD, (raw) => {
|
|
10198
|
+
const parsed = AgentInstallProgressParams.safeParse(raw);
|
|
10199
|
+
if (!parsed.success) {
|
|
10200
|
+
return;
|
|
10201
|
+
}
|
|
10202
|
+
installStatus.applyProgress(parsed.data);
|
|
10203
|
+
});
|
|
9224
10204
|
let bufferedEvents = [];
|
|
9225
10205
|
let applyRenderEvent = null;
|
|
9226
10206
|
let teardownStarted = false;
|
|
@@ -9235,36 +10215,49 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9235
10215
|
}
|
|
9236
10216
|
};
|
|
9237
10217
|
let pendingTurns = 0;
|
|
10218
|
+
let currentHeadMessageId;
|
|
9238
10219
|
let sessionBusySince = null;
|
|
9239
10220
|
let sessionElapsedTimer = null;
|
|
10221
|
+
let lastUpdateAt = null;
|
|
10222
|
+
let upstreamInterruptedSeen = false;
|
|
9240
10223
|
const adjustPendingTurns = (delta) => {
|
|
9241
10224
|
const before = pendingTurns;
|
|
9242
10225
|
pendingTurns = Math.max(0, pendingTurns + delta);
|
|
9243
10226
|
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
9244
10227
|
if (before === 0 && pendingTurns > 0) {
|
|
9245
10228
|
sessionBusySince = Date.now();
|
|
10229
|
+
lastUpdateAt = Date.now();
|
|
9246
10230
|
dispatcherRef?.setTurnRunning(true);
|
|
9247
10231
|
if (screenReady) {
|
|
9248
|
-
screenRef.setBanner({ status: "busy", elapsedMs: 0 });
|
|
10232
|
+
screenRef.setBanner({ status: "busy", elapsedMs: 0, stalled: false });
|
|
9249
10233
|
}
|
|
9250
10234
|
if (sessionElapsedTimer === null && screenReady) {
|
|
9251
10235
|
sessionElapsedTimer = setInterval(() => {
|
|
9252
10236
|
if (sessionBusySince === null || screenRef === null) {
|
|
9253
10237
|
return;
|
|
9254
10238
|
}
|
|
9255
|
-
|
|
10239
|
+
const idleMs = lastUpdateAt === null ? 0 : Date.now() - lastUpdateAt;
|
|
10240
|
+
screenRef.setBanner({
|
|
10241
|
+
elapsedMs: Date.now() - sessionBusySince,
|
|
10242
|
+
stalled: idleMs >= STALL_THRESHOLD_MS
|
|
10243
|
+
});
|
|
9256
10244
|
renderToolsBlock();
|
|
9257
10245
|
}, 1e3);
|
|
9258
10246
|
}
|
|
9259
10247
|
} else if (before > 0 && pendingTurns === 0) {
|
|
9260
10248
|
sessionBusySince = null;
|
|
10249
|
+
lastUpdateAt = null;
|
|
9261
10250
|
dispatcherRef?.setTurnRunning(false);
|
|
9262
10251
|
if (sessionElapsedTimer !== null) {
|
|
9263
10252
|
clearInterval(sessionElapsedTimer);
|
|
9264
10253
|
sessionElapsedTimer = null;
|
|
9265
10254
|
}
|
|
9266
10255
|
if (screenReady) {
|
|
9267
|
-
screenRef.setBanner({
|
|
10256
|
+
screenRef.setBanner({
|
|
10257
|
+
status: "ready",
|
|
10258
|
+
elapsedMs: void 0,
|
|
10259
|
+
stalled: false
|
|
10260
|
+
});
|
|
9268
10261
|
}
|
|
9269
10262
|
}
|
|
9270
10263
|
void delta;
|
|
@@ -9285,6 +10278,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9285
10278
|
const { update } = params ?? {};
|
|
9286
10279
|
const event = mapUpdate(update);
|
|
9287
10280
|
debugLogUpdate(update, event);
|
|
10281
|
+
lastUpdateAt = Date.now();
|
|
9288
10282
|
const rawTag = update?.sessionUpdate;
|
|
9289
10283
|
if (typeof rawTag === "string" && !STATE_UPDATE_KINDS2.has(rawTag)) {
|
|
9290
10284
|
const u = update ?? {};
|
|
@@ -9326,13 +10320,29 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9326
10320
|
screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
|
|
9327
10321
|
}
|
|
9328
10322
|
});
|
|
10323
|
+
const amendPendingPaintTimers = /* @__PURE__ */ new Map();
|
|
10324
|
+
const AMEND_CHIP_DISPLAY_DELAY_MS = 200;
|
|
9329
10325
|
conn.onNotification("hydra-acp/prompt_queue_added", (params) => {
|
|
9330
10326
|
if (teardownStarted) return;
|
|
9331
10327
|
const p = params ?? {};
|
|
9332
10328
|
if (typeof p.messageId !== "string") return;
|
|
9333
|
-
|
|
9334
|
-
if (
|
|
9335
|
-
|
|
10329
|
+
const isAmendPending = typeof p._meta?.["hydra-acp"]?.amending === "string";
|
|
10330
|
+
if (isAmendPending) {
|
|
10331
|
+
const mid = p.messageId;
|
|
10332
|
+
const prompt = p.prompt;
|
|
10333
|
+
const timer = setTimeout(() => {
|
|
10334
|
+
amendPendingPaintTimers.delete(mid);
|
|
10335
|
+
queueCache.set(mid, chipFromPrompt(mid, prompt));
|
|
10336
|
+
if (screenRef && dispatcherRef) {
|
|
10337
|
+
refreshQueueDisplay();
|
|
10338
|
+
}
|
|
10339
|
+
}, AMEND_CHIP_DISPLAY_DELAY_MS);
|
|
10340
|
+
amendPendingPaintTimers.set(mid, timer);
|
|
10341
|
+
} else {
|
|
10342
|
+
queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
|
|
10343
|
+
if (screenRef && dispatcherRef) {
|
|
10344
|
+
refreshQueueDisplay();
|
|
10345
|
+
}
|
|
9336
10346
|
}
|
|
9337
10347
|
if (ownClientId !== void 0 && p.originator?.clientId === ownClientId) {
|
|
9338
10348
|
const echo = pendingEchoes.shift();
|
|
@@ -9377,6 +10387,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9377
10387
|
if (teardownStarted) return;
|
|
9378
10388
|
const p = params ?? {};
|
|
9379
10389
|
if (typeof p.messageId !== "string") return;
|
|
10390
|
+
if (p.reason === "started") {
|
|
10391
|
+
currentHeadMessageId = p.messageId;
|
|
10392
|
+
}
|
|
10393
|
+
const pendingTimer = amendPendingPaintTimers.get(p.messageId);
|
|
10394
|
+
if (pendingTimer !== void 0) {
|
|
10395
|
+
clearTimeout(pendingTimer);
|
|
10396
|
+
amendPendingPaintTimers.delete(p.messageId);
|
|
10397
|
+
}
|
|
9380
10398
|
const hadChip = queueCache.delete(p.messageId);
|
|
9381
10399
|
if (hadChip && screenRef && dispatcherRef) {
|
|
9382
10400
|
refreshQueueDisplay();
|
|
@@ -9395,6 +10413,22 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9395
10413
|
}
|
|
9396
10414
|
}
|
|
9397
10415
|
});
|
|
10416
|
+
conn.onNotification("hydra-acp/prompt_amended", (params) => {
|
|
10417
|
+
if (teardownStarted) return;
|
|
10418
|
+
const p = params ?? {};
|
|
10419
|
+
if (typeof p.cancelledMessageId !== "string") return;
|
|
10420
|
+
const cancelledId = p.cancelledMessageId;
|
|
10421
|
+
amendedMessageIds.add(cancelledId);
|
|
10422
|
+
if (currentTurnEcho !== null && currentTurnEcho.messageId !== void 0 && currentTurnEcho.messageId === cancelledId) {
|
|
10423
|
+
appendRender({
|
|
10424
|
+
kind: "turn-complete",
|
|
10425
|
+
stopReason: "cancelled",
|
|
10426
|
+
amended: true
|
|
10427
|
+
});
|
|
10428
|
+
currentTurnEcho = null;
|
|
10429
|
+
amendedMessageIds.delete(cancelledId);
|
|
10430
|
+
}
|
|
10431
|
+
});
|
|
9398
10432
|
const handlePermissionResolved = (update) => {
|
|
9399
10433
|
const u = update ?? {};
|
|
9400
10434
|
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : void 0;
|
|
@@ -9502,6 +10536,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9502
10536
|
let upstreamSessionId;
|
|
9503
10537
|
let agentInfoName;
|
|
9504
10538
|
let agentAcceptsImages = true;
|
|
10539
|
+
let daemonSupportsAmend = false;
|
|
9505
10540
|
try {
|
|
9506
10541
|
const initResult = await conn.request("initialize", {
|
|
9507
10542
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
@@ -9516,6 +10551,8 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9516
10551
|
if (imageCap === false) {
|
|
9517
10552
|
agentAcceptsImages = false;
|
|
9518
10553
|
}
|
|
10554
|
+
const hydraMeta = extractHydraMeta(initResult?._meta ?? void 0);
|
|
10555
|
+
daemonSupportsAmend = hydraMeta.promptAmending === true;
|
|
9519
10556
|
} catch {
|
|
9520
10557
|
}
|
|
9521
10558
|
let resolvedSessionId = ctx.sessionId;
|
|
@@ -9811,6 +10848,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9811
10848
|
};
|
|
9812
10849
|
const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
|
|
9813
10850
|
const usage = { ...initialUsage ?? {} };
|
|
10851
|
+
installStatus.finalize();
|
|
9814
10852
|
screen.start();
|
|
9815
10853
|
screen.setSessionbar({
|
|
9816
10854
|
agent: sessionbarAgent,
|
|
@@ -9916,6 +10954,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9916
10954
|
}
|
|
9917
10955
|
return true;
|
|
9918
10956
|
};
|
|
10957
|
+
const buildHelpEntries = () => {
|
|
10958
|
+
const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
|
|
10959
|
+
const amendDesc = "amend the in-flight turn (cancel + replace)";
|
|
10960
|
+
const head = config.tui.defaultEnterAction === "amend" ? [
|
|
10961
|
+
["Enter", amendDesc],
|
|
10962
|
+
["Ctrl+Enter / Shift+Enter", enqueueDesc]
|
|
10963
|
+
] : [
|
|
10964
|
+
["Enter", enqueueDesc],
|
|
10965
|
+
["Ctrl+Enter / Shift+Enter", amendDesc]
|
|
10966
|
+
];
|
|
10967
|
+
return [...head, ...HELP_ENTRIES_TAIL];
|
|
10968
|
+
};
|
|
9919
10969
|
const toggleHelpModal = () => {
|
|
9920
10970
|
if (screen.isHelpPromptActive()) {
|
|
9921
10971
|
screen.setHelpPrompt(null);
|
|
@@ -9923,7 +10973,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9923
10973
|
}
|
|
9924
10974
|
screen.setHelpPrompt({
|
|
9925
10975
|
title: "Hotkeys",
|
|
9926
|
-
entries:
|
|
10976
|
+
entries: buildHelpEntries(),
|
|
9927
10977
|
hint: "any key dismisses \xB7 /help lists commands"
|
|
9928
10978
|
});
|
|
9929
10979
|
};
|
|
@@ -10026,7 +11076,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10026
11076
|
const handleEffect = (effect) => {
|
|
10027
11077
|
switch (effect.type) {
|
|
10028
11078
|
case "send":
|
|
10029
|
-
|
|
11079
|
+
if (config.tui.defaultEnterAction === "amend") {
|
|
11080
|
+
amendPrompt(effect.text, effect.attachments);
|
|
11081
|
+
} else {
|
|
11082
|
+
enqueuePrompt(effect.text, effect.attachments);
|
|
11083
|
+
}
|
|
11084
|
+
return;
|
|
11085
|
+
case "amend":
|
|
11086
|
+
if (config.tui.defaultEnterAction === "amend") {
|
|
11087
|
+
enqueuePrompt(effect.text, effect.attachments);
|
|
11088
|
+
} else {
|
|
11089
|
+
amendPrompt(effect.text, effect.attachments);
|
|
11090
|
+
}
|
|
10030
11091
|
return;
|
|
10031
11092
|
case "queue-edit": {
|
|
10032
11093
|
const mid = queueMessageIdAt(effect.index);
|
|
@@ -10234,6 +11295,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10234
11295
|
const queueCache = /* @__PURE__ */ new Map();
|
|
10235
11296
|
const pendingEchoes = [];
|
|
10236
11297
|
const ownPendingByMid = /* @__PURE__ */ new Map();
|
|
11298
|
+
const amendedMessageIds = /* @__PURE__ */ new Set();
|
|
10237
11299
|
let currentTurnEcho = null;
|
|
10238
11300
|
const refreshQueueDisplay = () => {
|
|
10239
11301
|
const entries = [...queueCache.values()];
|
|
@@ -10268,6 +11330,75 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10268
11330
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
10269
11331
|
void runPrompt(text, attachments);
|
|
10270
11332
|
};
|
|
11333
|
+
const amendPrompt = (text, attachments) => {
|
|
11334
|
+
screen.scrollToBottom();
|
|
11335
|
+
if (handleBuiltinCommand(text)) {
|
|
11336
|
+
return;
|
|
11337
|
+
}
|
|
11338
|
+
history = appendEntry(history, text);
|
|
11339
|
+
dispatcher.setHistory(history);
|
|
11340
|
+
saveHistory(historyFile, history).catch(() => void 0);
|
|
11341
|
+
if (!daemonSupportsAmend || currentHeadMessageId === void 0) {
|
|
11342
|
+
void runPrompt(text, attachments);
|
|
11343
|
+
return;
|
|
11344
|
+
}
|
|
11345
|
+
const target = currentHeadMessageId;
|
|
11346
|
+
const blocks = [];
|
|
11347
|
+
if (text.length > 0) {
|
|
11348
|
+
blocks.push({ type: "text", text });
|
|
11349
|
+
}
|
|
11350
|
+
for (const a of attachments) {
|
|
11351
|
+
blocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
|
|
11352
|
+
}
|
|
11353
|
+
const echo = { text, attachments, flushed: false };
|
|
11354
|
+
pendingEchoes.push(echo);
|
|
11355
|
+
const popEcho = () => {
|
|
11356
|
+
const idx = pendingEchoes.indexOf(echo);
|
|
11357
|
+
if (idx >= 0) {
|
|
11358
|
+
pendingEchoes.splice(idx, 1);
|
|
11359
|
+
}
|
|
11360
|
+
if (echo.messageId !== void 0) {
|
|
11361
|
+
ownPendingByMid.delete(echo.messageId);
|
|
11362
|
+
}
|
|
11363
|
+
};
|
|
11364
|
+
conn.request("hydra-acp/amend_prompt", {
|
|
11365
|
+
sessionId: resolvedSessionId,
|
|
11366
|
+
targetMessageId: target,
|
|
11367
|
+
prompt: blocks
|
|
11368
|
+
}).then((raw) => {
|
|
11369
|
+
const res = raw;
|
|
11370
|
+
if (res.amended && res.reason === "ok") {
|
|
11371
|
+
adjustPendingTurns(1);
|
|
11372
|
+
return;
|
|
11373
|
+
}
|
|
11374
|
+
popEcho();
|
|
11375
|
+
if (res.reason === "target_completed") {
|
|
11376
|
+
screen.notify(
|
|
11377
|
+
"previous response finished \u2014 press Enter to send as a new turn"
|
|
11378
|
+
);
|
|
11379
|
+
dispatcher.setBuffer(text, attachments);
|
|
11380
|
+
screen.refreshPrompt();
|
|
11381
|
+
return;
|
|
11382
|
+
}
|
|
11383
|
+
if (res.reason === "target_cancelled") {
|
|
11384
|
+
screen.notify("amend skipped \u2014 previous turn was cancelled");
|
|
11385
|
+
dispatcher.setBuffer(text, attachments);
|
|
11386
|
+
screen.refreshPrompt();
|
|
11387
|
+
return;
|
|
11388
|
+
}
|
|
11389
|
+
if (res.reason === "target_not_found") {
|
|
11390
|
+
screen.notify("amend skipped \u2014 no matching prompt");
|
|
11391
|
+
dispatcher.setBuffer(text, attachments);
|
|
11392
|
+
screen.refreshPrompt();
|
|
11393
|
+
return;
|
|
11394
|
+
}
|
|
11395
|
+
}).catch((err) => {
|
|
11396
|
+
popEcho();
|
|
11397
|
+
screen.notify(`amend failed: ${err.message}`);
|
|
11398
|
+
dispatcher.setBuffer(text, attachments);
|
|
11399
|
+
screen.refreshPrompt();
|
|
11400
|
+
});
|
|
11401
|
+
};
|
|
10271
11402
|
const handleModeToggle = async (_on) => {
|
|
10272
11403
|
if (agentModes.length === 0) {
|
|
10273
11404
|
screen.notify("no modes advertised by agent");
|
|
@@ -10492,9 +11623,18 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10492
11623
|
turnInFlight = null;
|
|
10493
11624
|
adjustPendingTurns(-1);
|
|
10494
11625
|
if (echo.flushed && currentTurnEcho === echo) {
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
11626
|
+
const wasAmended = echo.messageId !== void 0 && amendedMessageIds.has(echo.messageId);
|
|
11627
|
+
if (wasAmended && echo.messageId !== void 0) {
|
|
11628
|
+
amendedMessageIds.delete(echo.messageId);
|
|
11629
|
+
}
|
|
11630
|
+
const tc = { kind: "turn-complete" };
|
|
11631
|
+
if (stopReason !== void 0) {
|
|
11632
|
+
tc.stopReason = stopReason;
|
|
11633
|
+
}
|
|
11634
|
+
if (wasAmended) {
|
|
11635
|
+
tc.amended = true;
|
|
11636
|
+
}
|
|
11637
|
+
appendRender(tc);
|
|
10498
11638
|
currentTurnEcho = null;
|
|
10499
11639
|
}
|
|
10500
11640
|
if (pendingPrefill !== null) {
|
|
@@ -10591,7 +11731,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10591
11731
|
for (const id of visibleIds) {
|
|
10592
11732
|
const state = toolStates.get(id);
|
|
10593
11733
|
if (state) {
|
|
10594
|
-
lines.push(formatToolLine2(state));
|
|
11734
|
+
lines.push(...formatToolLine2(state));
|
|
10595
11735
|
}
|
|
10596
11736
|
}
|
|
10597
11737
|
screen.upsertLines("tools", lines);
|
|
@@ -10602,7 +11742,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10602
11742
|
toolsBlockStopReason = null;
|
|
10603
11743
|
renderToolsBlock();
|
|
10604
11744
|
};
|
|
10605
|
-
const recordToolCall = (id, title, status) => {
|
|
11745
|
+
const recordToolCall = (id, title, status, errorText) => {
|
|
10606
11746
|
const wasNew = !toolStates.has(id);
|
|
10607
11747
|
const existing = toolStates.get(id);
|
|
10608
11748
|
const state = existing ?? {
|
|
@@ -10619,6 +11759,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10619
11759
|
if (!existing) {
|
|
10620
11760
|
state.status = status ?? "pending";
|
|
10621
11761
|
}
|
|
11762
|
+
if (errorText !== void 0) {
|
|
11763
|
+
state.errorText = errorText;
|
|
11764
|
+
}
|
|
10622
11765
|
toolStates.set(id, state);
|
|
10623
11766
|
if (wasNew) {
|
|
10624
11767
|
if (toolsBlockStartedAt === null) {
|
|
@@ -10710,7 +11853,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10710
11853
|
}
|
|
10711
11854
|
if (event.kind === "tool-call") {
|
|
10712
11855
|
closeAgentText();
|
|
10713
|
-
recordToolCall(event.toolCallId, event.title, event.status);
|
|
11856
|
+
recordToolCall(event.toolCallId, event.title, event.status, void 0);
|
|
10714
11857
|
renderToolsBlock();
|
|
10715
11858
|
return;
|
|
10716
11859
|
}
|
|
@@ -10725,7 +11868,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10725
11868
|
}
|
|
10726
11869
|
if (event.kind === "tool-call-update") {
|
|
10727
11870
|
closeAgentText();
|
|
10728
|
-
recordToolCall(
|
|
11871
|
+
recordToolCall(
|
|
11872
|
+
event.toolCallId,
|
|
11873
|
+
event.title,
|
|
11874
|
+
event.status,
|
|
11875
|
+
event.errorText
|
|
11876
|
+
);
|
|
11877
|
+
if (event.upstreamInterrupted) {
|
|
11878
|
+
upstreamInterruptedSeen = true;
|
|
11879
|
+
}
|
|
10729
11880
|
renderToolsBlock();
|
|
10730
11881
|
return;
|
|
10731
11882
|
}
|
|
@@ -10737,8 +11888,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10737
11888
|
screen.appendLines(formatted);
|
|
10738
11889
|
}
|
|
10739
11890
|
if (event.kind === "turn-complete") {
|
|
11891
|
+
currentHeadMessageId = void 0;
|
|
10740
11892
|
closeAgentText();
|
|
10741
|
-
|
|
11893
|
+
let effectiveStopReason = event.amended ? "amended" : event.stopReason;
|
|
11894
|
+
if (!event.amended && upstreamInterruptedSeen && (effectiveStopReason === void 0 || effectiveStopReason === "end_turn")) {
|
|
11895
|
+
effectiveStopReason = "error";
|
|
11896
|
+
}
|
|
11897
|
+
if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
10742
11898
|
const lines = formatEvent({ ...lastPlanEvent, stopped: true });
|
|
10743
11899
|
if (lines.length > 0) {
|
|
10744
11900
|
screen.upsertLines("plan", lines);
|
|
@@ -10748,15 +11904,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10748
11904
|
screen.clearKey("plan");
|
|
10749
11905
|
if (toolsBlockStartedAt !== null) {
|
|
10750
11906
|
toolsBlockEndedAt = Date.now();
|
|
10751
|
-
toolsBlockStopReason =
|
|
11907
|
+
toolsBlockStopReason = effectiveStopReason ?? null;
|
|
10752
11908
|
renderToolsBlock();
|
|
10753
11909
|
screen.clearKey("tools");
|
|
10754
|
-
} else if (
|
|
11910
|
+
} else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
10755
11911
|
screen.appendLines([
|
|
10756
11912
|
{
|
|
10757
11913
|
prefix: "\u26A0 ",
|
|
10758
11914
|
prefixStyle: "tool-status-fail",
|
|
10759
|
-
body: `turn ended: ${
|
|
11915
|
+
body: `turn ended: ${effectiveStopReason}`,
|
|
10760
11916
|
bodyStyle: "tool-status-fail"
|
|
10761
11917
|
}
|
|
10762
11918
|
]);
|
|
@@ -10767,6 +11923,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10767
11923
|
toolsBlockEndedAt = null;
|
|
10768
11924
|
toolsBlockStopReason = null;
|
|
10769
11925
|
toolsExpanded = false;
|
|
11926
|
+
upstreamInterruptedSeen = false;
|
|
10770
11927
|
screen.ensureSeparator();
|
|
10771
11928
|
}
|
|
10772
11929
|
};
|
|
@@ -10876,9 +12033,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10876
12033
|
if (resp.error) {
|
|
10877
12034
|
throw new Error(resp.error.message);
|
|
10878
12035
|
}
|
|
10879
|
-
const
|
|
10880
|
-
|
|
10881
|
-
|
|
12036
|
+
const fields = parseReattachResponse(resp.result);
|
|
12037
|
+
appliedPolicy = fields.appliedPolicy;
|
|
12038
|
+
if (fields.clientId !== void 0) {
|
|
12039
|
+
ownClientId = fields.clientId;
|
|
10882
12040
|
}
|
|
10883
12041
|
} catch (err) {
|
|
10884
12042
|
attachErr = err;
|
|
@@ -11004,21 +12162,110 @@ function writeDebugLine(payload) {
|
|
|
11004
12162
|
} catch {
|
|
11005
12163
|
}
|
|
11006
12164
|
}
|
|
11007
|
-
function
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
12165
|
+
function createInstallStatusLine(term, baseLabel) {
|
|
12166
|
+
let finalized = false;
|
|
12167
|
+
let lastText = "";
|
|
12168
|
+
let osc94Active = false;
|
|
12169
|
+
const writeOsc94 = (state) => {
|
|
12170
|
+
if (finalized) {
|
|
11011
12171
|
return;
|
|
11012
12172
|
}
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
12173
|
+
if (state === 3 && osc94Active) {
|
|
12174
|
+
return;
|
|
12175
|
+
}
|
|
12176
|
+
if (state === 0 && !osc94Active) {
|
|
12177
|
+
return;
|
|
12178
|
+
}
|
|
12179
|
+
osc94Active = state === 3;
|
|
12180
|
+
process.stdout.write(`\x1B]9;4;${state}\x1B\\`);
|
|
12181
|
+
};
|
|
12182
|
+
const redraw = (text) => {
|
|
12183
|
+
if (finalized) {
|
|
12184
|
+
return;
|
|
12185
|
+
}
|
|
12186
|
+
process.stdout.write("\r");
|
|
12187
|
+
term.eraseLineAfter();
|
|
12188
|
+
term.brightYellow(text);
|
|
12189
|
+
lastText = text;
|
|
12190
|
+
};
|
|
12191
|
+
const formatProgressText = (event) => {
|
|
12192
|
+
const idVer = `${event.agentId}@${event.version}`;
|
|
12193
|
+
if (event.source === "npm") {
|
|
12194
|
+
if (event.phase === "install_start" || event.phase === "download_start") {
|
|
12195
|
+
return `${baseLabel} installing ${idVer} via npm\u2026`;
|
|
12196
|
+
}
|
|
12197
|
+
if (event.phase === "installed") {
|
|
12198
|
+
return `${baseLabel} ${idVer} installed`;
|
|
12199
|
+
}
|
|
12200
|
+
return `${baseLabel} installing ${idVer} via npm\u2026`;
|
|
12201
|
+
}
|
|
12202
|
+
if (event.phase === "download_start" || event.phase === "download_progress") {
|
|
12203
|
+
const received = event.receivedBytes ?? 0;
|
|
12204
|
+
const total = event.totalBytes ?? 0;
|
|
12205
|
+
const rxMb = (received / 1e6).toFixed(1);
|
|
12206
|
+
if (total > 0) {
|
|
12207
|
+
const totalMb = (total / 1e6).toFixed(1);
|
|
12208
|
+
const pct = Math.min(100, Math.floor(received / total * 100));
|
|
12209
|
+
return `${baseLabel} downloading ${idVer} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
12210
|
+
}
|
|
12211
|
+
return `${baseLabel} downloading ${idVer} ${rxMb} MB`;
|
|
12212
|
+
}
|
|
12213
|
+
if (event.phase === "download_done") {
|
|
12214
|
+
return `${baseLabel} downloaded ${idVer}, verifying\u2026`;
|
|
12215
|
+
}
|
|
12216
|
+
if (event.phase === "extract") {
|
|
12217
|
+
return `${baseLabel} extracting ${idVer}\u2026`;
|
|
12218
|
+
}
|
|
12219
|
+
if (event.phase === "installed") {
|
|
12220
|
+
return `${baseLabel} ${idVer} installed`;
|
|
12221
|
+
}
|
|
12222
|
+
return lastText || baseLabel;
|
|
12223
|
+
};
|
|
12224
|
+
return {
|
|
12225
|
+
write(text) {
|
|
12226
|
+
if (finalized) {
|
|
12227
|
+
return;
|
|
12228
|
+
}
|
|
12229
|
+
term.brightYellow(text);
|
|
12230
|
+
lastText = text;
|
|
12231
|
+
},
|
|
12232
|
+
applyProgress(event) {
|
|
12233
|
+
if (finalized) {
|
|
12234
|
+
return;
|
|
12235
|
+
}
|
|
12236
|
+
const isActive = event.phase === "download_start" || event.phase === "download_progress" || event.phase === "install_start" || event.phase === "extract" || event.phase === "download_done";
|
|
12237
|
+
if (isActive) {
|
|
12238
|
+
writeOsc94(3);
|
|
12239
|
+
} else if (event.phase === "installed") {
|
|
12240
|
+
writeOsc94(0);
|
|
12241
|
+
}
|
|
12242
|
+
redraw(formatProgressText(event));
|
|
12243
|
+
},
|
|
12244
|
+
finalize() {
|
|
12245
|
+
if (finalized) {
|
|
12246
|
+
return;
|
|
12247
|
+
}
|
|
12248
|
+
finalized = true;
|
|
12249
|
+
writeOsc94(0);
|
|
12250
|
+
process.stdout.write("\n");
|
|
12251
|
+
}
|
|
12252
|
+
};
|
|
12253
|
+
}
|
|
12254
|
+
function rotateIfBig(target) {
|
|
12255
|
+
try {
|
|
12256
|
+
const stat4 = statSync(target);
|
|
12257
|
+
if (stat4.size < logMaxBytes) {
|
|
12258
|
+
return;
|
|
12259
|
+
}
|
|
12260
|
+
renameSync(target, `${target}.0`);
|
|
12261
|
+
} catch {
|
|
12262
|
+
}
|
|
12263
|
+
}
|
|
12264
|
+
var STALL_THRESHOLD_MS, HELP_ENTRIES_TAIL, logMaxBytes;
|
|
12265
|
+
var init_app = __esm({
|
|
12266
|
+
"src/tui/app.ts"() {
|
|
12267
|
+
"use strict";
|
|
12268
|
+
init_connection();
|
|
11022
12269
|
init_types();
|
|
11023
12270
|
init_resilient_ws();
|
|
11024
12271
|
init_config();
|
|
@@ -11036,10 +12283,11 @@ var init_app = __esm({
|
|
|
11036
12283
|
init_attachments();
|
|
11037
12284
|
init_clipboard();
|
|
11038
12285
|
init_completion();
|
|
12286
|
+
init_reconnect_state();
|
|
11039
12287
|
init_render_update();
|
|
11040
12288
|
init_format();
|
|
11041
|
-
|
|
11042
|
-
|
|
12289
|
+
STALL_THRESHOLD_MS = 12e4;
|
|
12290
|
+
HELP_ENTRIES_TAIL = [
|
|
11043
12291
|
["Alt+Enter", "newline in prompt"],
|
|
11044
12292
|
["Shift+Tab", "cycle agent modes (plan / accept-edits / etc.)"],
|
|
11045
12293
|
["Tab", "indent \xB7 slash-command completion"],
|
|
@@ -11200,6 +12448,7 @@ init_config();
|
|
|
11200
12448
|
init_service_token();
|
|
11201
12449
|
import * as fsp7 from "fs/promises";
|
|
11202
12450
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
12451
|
+
import chalk from "chalk";
|
|
11203
12452
|
|
|
11204
12453
|
// src/daemon/server.ts
|
|
11205
12454
|
init_config();
|
|
@@ -11268,8 +12517,10 @@ async function ensureBinary(args) {
|
|
|
11268
12517
|
}
|
|
11269
12518
|
await downloadAndExtract({
|
|
11270
12519
|
agentId: args.agentId,
|
|
12520
|
+
version: args.version,
|
|
11271
12521
|
archiveUrl: args.target.archive,
|
|
11272
|
-
installDir
|
|
12522
|
+
installDir,
|
|
12523
|
+
onProgress: args.onProgress
|
|
11273
12524
|
});
|
|
11274
12525
|
if (!await fileExists(cmdPath)) {
|
|
11275
12526
|
throw new Error(
|
|
@@ -11289,9 +12540,16 @@ async function downloadAndExtract(args) {
|
|
|
11289
12540
|
const archivePath = await downloadTo({
|
|
11290
12541
|
url: args.archiveUrl,
|
|
11291
12542
|
dir: tempDir,
|
|
11292
|
-
agentId: args.agentId
|
|
12543
|
+
agentId: args.agentId,
|
|
12544
|
+
version: args.version,
|
|
12545
|
+
onProgress: args.onProgress
|
|
11293
12546
|
});
|
|
11294
12547
|
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
12548
|
+
safeEmit(args.onProgress, {
|
|
12549
|
+
phase: "extract",
|
|
12550
|
+
agentId: args.agentId,
|
|
12551
|
+
version: args.version
|
|
12552
|
+
});
|
|
11295
12553
|
await extract(archivePath, tempDir);
|
|
11296
12554
|
await fsp.unlink(archivePath).catch(() => void 0);
|
|
11297
12555
|
try {
|
|
@@ -11302,16 +12560,35 @@ async function downloadAndExtract(args) {
|
|
|
11302
12560
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
11303
12561
|
() => void 0
|
|
11304
12562
|
);
|
|
12563
|
+
safeEmit(args.onProgress, {
|
|
12564
|
+
phase: "installed",
|
|
12565
|
+
agentId: args.agentId,
|
|
12566
|
+
version: args.version
|
|
12567
|
+
});
|
|
11305
12568
|
return;
|
|
11306
12569
|
}
|
|
11307
12570
|
throw err;
|
|
11308
12571
|
}
|
|
11309
12572
|
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
12573
|
+
safeEmit(args.onProgress, {
|
|
12574
|
+
phase: "installed",
|
|
12575
|
+
agentId: args.agentId,
|
|
12576
|
+
version: args.version
|
|
12577
|
+
});
|
|
11310
12578
|
} catch (err) {
|
|
11311
12579
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
11312
12580
|
throw err;
|
|
11313
12581
|
}
|
|
11314
12582
|
}
|
|
12583
|
+
function safeEmit(cb, event) {
|
|
12584
|
+
if (!cb) {
|
|
12585
|
+
return;
|
|
12586
|
+
}
|
|
12587
|
+
try {
|
|
12588
|
+
cb(event);
|
|
12589
|
+
} catch {
|
|
12590
|
+
}
|
|
12591
|
+
}
|
|
11315
12592
|
async function downloadTo(args) {
|
|
11316
12593
|
const filename = inferArchiveName(args.url);
|
|
11317
12594
|
const dest = path2.join(args.dir, filename);
|
|
@@ -11324,17 +12601,34 @@ async function downloadTo(args) {
|
|
|
11324
12601
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
11325
12602
|
const out = fs4.createWriteStream(dest);
|
|
11326
12603
|
const nodeStream = Readable.fromWeb(response.body);
|
|
12604
|
+
safeEmit(args.onProgress, {
|
|
12605
|
+
phase: "download_start",
|
|
12606
|
+
agentId: args.agentId,
|
|
12607
|
+
version: args.version,
|
|
12608
|
+
totalBytes: total
|
|
12609
|
+
});
|
|
11327
12610
|
let received = 0;
|
|
11328
|
-
let
|
|
11329
|
-
|
|
12611
|
+
let lastLogEmit = Date.now();
|
|
12612
|
+
let lastCbEmit = 0;
|
|
12613
|
+
const LOG_INTERVAL_MS = 2e3;
|
|
12614
|
+
const CB_INTERVAL_MS = 150;
|
|
11330
12615
|
nodeStream.on("data", (chunk) => {
|
|
11331
12616
|
received += chunk.length;
|
|
11332
12617
|
const now = Date.now();
|
|
11333
|
-
if (now -
|
|
11334
|
-
|
|
12618
|
+
if (now - lastCbEmit >= CB_INTERVAL_MS) {
|
|
12619
|
+
lastCbEmit = now;
|
|
12620
|
+
safeEmit(args.onProgress, {
|
|
12621
|
+
phase: "download_progress",
|
|
12622
|
+
agentId: args.agentId,
|
|
12623
|
+
version: args.version,
|
|
12624
|
+
receivedBytes: received,
|
|
12625
|
+
totalBytes: total
|
|
12626
|
+
});
|
|
12627
|
+
}
|
|
12628
|
+
if (now - lastLogEmit >= LOG_INTERVAL_MS) {
|
|
12629
|
+
lastLogEmit = now;
|
|
12630
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
11335
12631
|
}
|
|
11336
|
-
lastEmit = now;
|
|
11337
|
-
logSink(formatProgress(args.agentId, received, total));
|
|
11338
12632
|
});
|
|
11339
12633
|
await new Promise((resolve5, reject) => {
|
|
11340
12634
|
nodeStream.on("error", reject);
|
|
@@ -11349,6 +12643,13 @@ async function downloadTo(args) {
|
|
|
11349
12643
|
/* done */
|
|
11350
12644
|
true
|
|
11351
12645
|
));
|
|
12646
|
+
safeEmit(args.onProgress, {
|
|
12647
|
+
phase: "download_done",
|
|
12648
|
+
agentId: args.agentId,
|
|
12649
|
+
version: args.version,
|
|
12650
|
+
receivedBytes: received,
|
|
12651
|
+
totalBytes: total
|
|
12652
|
+
});
|
|
11352
12653
|
return dest;
|
|
11353
12654
|
}
|
|
11354
12655
|
function formatProgress(agentId, received, total, done = false) {
|
|
@@ -11448,9 +12749,11 @@ async function ensureNpmPackage(args) {
|
|
|
11448
12749
|
}
|
|
11449
12750
|
await installInto({
|
|
11450
12751
|
agentId: args.agentId,
|
|
12752
|
+
version: args.version,
|
|
11451
12753
|
packageSpec: args.packageSpec,
|
|
11452
12754
|
installDir,
|
|
11453
|
-
registry: args.registry
|
|
12755
|
+
registry: args.registry,
|
|
12756
|
+
onProgress: args.onProgress
|
|
11454
12757
|
});
|
|
11455
12758
|
if (!await fileExists2(binPath)) {
|
|
11456
12759
|
throw new Error(
|
|
@@ -11466,6 +12769,12 @@ async function installInto(args) {
|
|
|
11466
12769
|
logSink2(
|
|
11467
12770
|
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
11468
12771
|
);
|
|
12772
|
+
safeEmit2(args.onProgress, {
|
|
12773
|
+
phase: "install_start",
|
|
12774
|
+
agentId: args.agentId,
|
|
12775
|
+
version: args.version,
|
|
12776
|
+
packageSpec: args.packageSpec
|
|
12777
|
+
});
|
|
11469
12778
|
await runNpmInstall({
|
|
11470
12779
|
packageSpec: args.packageSpec,
|
|
11471
12780
|
cwd: tempDir,
|
|
@@ -11479,11 +12788,21 @@ async function installInto(args) {
|
|
|
11479
12788
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
11480
12789
|
() => void 0
|
|
11481
12790
|
);
|
|
12791
|
+
safeEmit2(args.onProgress, {
|
|
12792
|
+
phase: "installed",
|
|
12793
|
+
agentId: args.agentId,
|
|
12794
|
+
version: args.version
|
|
12795
|
+
});
|
|
11482
12796
|
return;
|
|
11483
12797
|
}
|
|
11484
12798
|
throw err;
|
|
11485
12799
|
}
|
|
11486
12800
|
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
12801
|
+
safeEmit2(args.onProgress, {
|
|
12802
|
+
phase: "installed",
|
|
12803
|
+
agentId: args.agentId,
|
|
12804
|
+
version: args.version
|
|
12805
|
+
});
|
|
11487
12806
|
} catch (err) {
|
|
11488
12807
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
11489
12808
|
() => void 0
|
|
@@ -11491,44 +12810,87 @@ async function installInto(args) {
|
|
|
11491
12810
|
throw err;
|
|
11492
12811
|
}
|
|
11493
12812
|
}
|
|
12813
|
+
function safeEmit2(cb, event) {
|
|
12814
|
+
if (!cb) {
|
|
12815
|
+
return;
|
|
12816
|
+
}
|
|
12817
|
+
try {
|
|
12818
|
+
cb(event);
|
|
12819
|
+
} catch {
|
|
12820
|
+
}
|
|
12821
|
+
}
|
|
12822
|
+
var ETXTBSY_RETRIES = 5;
|
|
12823
|
+
var ETXTBSY_BACKOFF_MS = 25;
|
|
11494
12824
|
function runNpmInstall(args) {
|
|
11495
|
-
return
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
child.on("exit", (code, signal) => {
|
|
11518
|
-
if (code === 0) {
|
|
11519
|
-
resolve5();
|
|
12825
|
+
return runNpmInstallOnce(args, 0);
|
|
12826
|
+
}
|
|
12827
|
+
async function runNpmInstallOnce(args, attempt) {
|
|
12828
|
+
try {
|
|
12829
|
+
await new Promise((resolve5, reject) => {
|
|
12830
|
+
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
12831
|
+
let child;
|
|
12832
|
+
try {
|
|
12833
|
+
child = spawn2(
|
|
12834
|
+
"npm",
|
|
12835
|
+
[
|
|
12836
|
+
"install",
|
|
12837
|
+
"--no-audit",
|
|
12838
|
+
"--no-fund",
|
|
12839
|
+
"--silent",
|
|
12840
|
+
...registryArgs,
|
|
12841
|
+
args.packageSpec
|
|
12842
|
+
],
|
|
12843
|
+
{ cwd: args.cwd, stdio: ["ignore", "pipe", "pipe"] }
|
|
12844
|
+
);
|
|
12845
|
+
} catch (err) {
|
|
12846
|
+
reject(err);
|
|
11520
12847
|
return;
|
|
11521
12848
|
}
|
|
11522
|
-
|
|
11523
|
-
|
|
11524
|
-
|
|
11525
|
-
|
|
11526
|
-
|
|
12849
|
+
let stderrTail = "";
|
|
12850
|
+
child.stdout?.on("data", (chunk) => {
|
|
12851
|
+
void chunk;
|
|
12852
|
+
});
|
|
12853
|
+
child.stderr?.setEncoding("utf8");
|
|
12854
|
+
child.stderr?.on("data", (chunk) => {
|
|
12855
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
12856
|
+
});
|
|
12857
|
+
child.on("error", (err) => {
|
|
12858
|
+
const e = err;
|
|
12859
|
+
if (e.code === "ENOENT") {
|
|
12860
|
+
reject(
|
|
12861
|
+
new Error(
|
|
12862
|
+
`npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)`
|
|
12863
|
+
)
|
|
12864
|
+
);
|
|
12865
|
+
return;
|
|
12866
|
+
}
|
|
12867
|
+
reject(err);
|
|
12868
|
+
});
|
|
12869
|
+
child.on("exit", (code, signal) => {
|
|
12870
|
+
if (code === 0) {
|
|
12871
|
+
resolve5();
|
|
12872
|
+
return;
|
|
12873
|
+
}
|
|
12874
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
12875
|
+
const tail = stderrTail.trim();
|
|
12876
|
+
reject(
|
|
12877
|
+
new Error(
|
|
12878
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
11527
12879
|
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
11528
|
-
|
|
11529
|
-
|
|
12880
|
+
)
|
|
12881
|
+
);
|
|
12882
|
+
});
|
|
11530
12883
|
});
|
|
11531
|
-
})
|
|
12884
|
+
} catch (err) {
|
|
12885
|
+
const code = err.code;
|
|
12886
|
+
if (code === "ETXTBSY" && attempt < ETXTBSY_RETRIES) {
|
|
12887
|
+
await new Promise(
|
|
12888
|
+
(r) => setTimeout(r, ETXTBSY_BACKOFF_MS * (attempt + 1))
|
|
12889
|
+
);
|
|
12890
|
+
return runNpmInstallOnce(args, attempt + 1);
|
|
12891
|
+
}
|
|
12892
|
+
throw err;
|
|
12893
|
+
}
|
|
11532
12894
|
}
|
|
11533
12895
|
async function fileExists2(p) {
|
|
11534
12896
|
try {
|
|
@@ -11726,12 +13088,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
11726
13088
|
};
|
|
11727
13089
|
}
|
|
11728
13090
|
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
13091
|
+
const npmCb = options.onInstallProgress;
|
|
11729
13092
|
const binPath = await ensureNpmPackage({
|
|
11730
13093
|
agentId: agent.id,
|
|
11731
13094
|
version,
|
|
11732
13095
|
packageSpec: npx.package,
|
|
11733
13096
|
bin,
|
|
11734
|
-
registry: options.npmRegistry
|
|
13097
|
+
registry: options.npmRegistry,
|
|
13098
|
+
onProgress: npmCb ? (e) => npmCb({ source: "npm", ...e }) : void 0
|
|
11735
13099
|
});
|
|
11736
13100
|
return {
|
|
11737
13101
|
command: binPath,
|
|
@@ -11747,10 +13111,12 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
11747
13111
|
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
11748
13112
|
);
|
|
11749
13113
|
}
|
|
13114
|
+
const binCb = options.onInstallProgress;
|
|
11750
13115
|
const cmdPath = await ensureBinary({
|
|
11751
13116
|
agentId: agent.id,
|
|
11752
13117
|
version,
|
|
11753
|
-
target
|
|
13118
|
+
target,
|
|
13119
|
+
onProgress: binCb ? (e) => binCb({ source: "binary", ...e }) : void 0
|
|
11754
13120
|
});
|
|
11755
13121
|
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
11756
13122
|
return {
|
|
@@ -12168,7 +13534,8 @@ var SessionManager = class {
|
|
|
12168
13534
|
cwd: params.cwd,
|
|
12169
13535
|
agentArgs: params.agentArgs,
|
|
12170
13536
|
mcpServers: params.mcpServers,
|
|
12171
|
-
model: params.model
|
|
13537
|
+
model: params.model,
|
|
13538
|
+
onInstallProgress: params.onInstallProgress
|
|
12172
13539
|
});
|
|
12173
13540
|
const session = new Session({
|
|
12174
13541
|
cwd: params.cwd,
|
|
@@ -12185,7 +13552,8 @@ var SessionManager = class {
|
|
|
12185
13552
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
12186
13553
|
currentModel: fresh.initialModel,
|
|
12187
13554
|
currentMode: fresh.initialMode,
|
|
12188
|
-
agentModes: fresh.initialModes
|
|
13555
|
+
agentModes: fresh.initialModes,
|
|
13556
|
+
agentModels: fresh.initialModels
|
|
12189
13557
|
});
|
|
12190
13558
|
await this.attachManagerHooks(session);
|
|
12191
13559
|
return session;
|
|
@@ -12230,7 +13598,10 @@ var SessionManager = class {
|
|
|
12230
13598
|
if (params.upstreamSessionId === "") {
|
|
12231
13599
|
return this.doResurrectFromImport(params);
|
|
12232
13600
|
}
|
|
12233
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13601
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13602
|
+
npmRegistry: this.npmRegistry,
|
|
13603
|
+
onInstallProgress: params.onInstallProgress
|
|
13604
|
+
});
|
|
12234
13605
|
const agent = this.spawner({
|
|
12235
13606
|
agentId: params.agentId,
|
|
12236
13607
|
cwd: params.cwd,
|
|
@@ -12288,6 +13659,7 @@ var SessionManager = class {
|
|
|
12288
13659
|
currentUsage: params.currentUsage,
|
|
12289
13660
|
agentCommands: params.agentCommands,
|
|
12290
13661
|
agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
|
|
13662
|
+
agentModels: params.agentModels ?? nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})),
|
|
12291
13663
|
// Only gate the first-prompt title heuristic when we actually have
|
|
12292
13664
|
// a title to preserve. A title-less session (lost to a write race
|
|
12293
13665
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -12311,7 +13683,8 @@ var SessionManager = class {
|
|
|
12311
13683
|
agentId: params.agentId,
|
|
12312
13684
|
cwd,
|
|
12313
13685
|
agentArgs: params.agentArgs,
|
|
12314
|
-
mcpServers: []
|
|
13686
|
+
mcpServers: [],
|
|
13687
|
+
onInstallProgress: params.onInstallProgress
|
|
12315
13688
|
});
|
|
12316
13689
|
const session = new Session({
|
|
12317
13690
|
sessionId: params.hydraSessionId,
|
|
@@ -12334,6 +13707,7 @@ var SessionManager = class {
|
|
|
12334
13707
|
currentUsage: params.currentUsage,
|
|
12335
13708
|
agentCommands: params.agentCommands,
|
|
12336
13709
|
agentModes: params.agentModes ?? fresh.initialModes,
|
|
13710
|
+
agentModels: params.agentModels ?? fresh.initialModels,
|
|
12337
13711
|
firstPromptSeeded: !!params.title,
|
|
12338
13712
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
12339
13713
|
});
|
|
@@ -12363,7 +13737,10 @@ var SessionManager = class {
|
|
|
12363
13737
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
12364
13738
|
throw err;
|
|
12365
13739
|
}
|
|
12366
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13740
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13741
|
+
npmRegistry: this.npmRegistry,
|
|
13742
|
+
onInstallProgress: params.onInstallProgress
|
|
13743
|
+
});
|
|
12367
13744
|
const agent = this.spawner({
|
|
12368
13745
|
agentId: params.agentId,
|
|
12369
13746
|
cwd: params.cwd,
|
|
@@ -12389,15 +13766,25 @@ var SessionManager = class {
|
|
|
12389
13766
|
);
|
|
12390
13767
|
}
|
|
12391
13768
|
let initialModel = extractInitialModel(newResult);
|
|
13769
|
+
const initialModels = extractInitialModels(newResult);
|
|
12392
13770
|
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
12393
13771
|
if (desired && desired !== initialModel) {
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
13772
|
+
const validates = initialModels.length === 0 || initialModels.some((m) => m.modelId === desired);
|
|
13773
|
+
if (validates) {
|
|
13774
|
+
try {
|
|
13775
|
+
await agent.connection.request("session/set_model", {
|
|
13776
|
+
sessionId: sessionIdRaw,
|
|
13777
|
+
modelId: desired
|
|
13778
|
+
});
|
|
13779
|
+
initialModel = desired;
|
|
13780
|
+
} catch {
|
|
13781
|
+
}
|
|
13782
|
+
} else {
|
|
13783
|
+
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
13784
|
+
process.stderr.write(
|
|
13785
|
+
`hydra-acp: defaultModels[${params.agentId}]=${JSON.stringify(desired)} is not in the agent's availableModels (${known}); skipping session/set_model
|
|
13786
|
+
`
|
|
13787
|
+
);
|
|
12401
13788
|
}
|
|
12402
13789
|
}
|
|
12403
13790
|
const initialModes = extractInitialModes(newResult);
|
|
@@ -12407,6 +13794,7 @@ var SessionManager = class {
|
|
|
12407
13794
|
upstreamSessionId: sessionIdRaw,
|
|
12408
13795
|
agentMeta: newResult._meta,
|
|
12409
13796
|
initialModel,
|
|
13797
|
+
initialModels: initialModels.length > 0 ? initialModels : void 0,
|
|
12410
13798
|
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
12411
13799
|
initialMode
|
|
12412
13800
|
};
|
|
@@ -12469,6 +13857,15 @@ var SessionManager = class {
|
|
|
12469
13857
|
}))
|
|
12470
13858
|
}).catch(() => void 0);
|
|
12471
13859
|
});
|
|
13860
|
+
session.onAgentModelsChange((models) => {
|
|
13861
|
+
void this.persistSnapshot(session.sessionId, {
|
|
13862
|
+
agentModels: models.map((m) => ({
|
|
13863
|
+
modelId: m.modelId,
|
|
13864
|
+
...m.name !== void 0 ? { name: m.name } : {},
|
|
13865
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
13866
|
+
}))
|
|
13867
|
+
}).catch(() => void 0);
|
|
13868
|
+
});
|
|
12472
13869
|
this.sessions.set(session.sessionId, session);
|
|
12473
13870
|
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
12474
13871
|
const existing = await this.store.read(session.sessionId);
|
|
@@ -12512,6 +13909,7 @@ var SessionManager = class {
|
|
|
12512
13909
|
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
12513
13910
|
agentCommands: record.agentCommands,
|
|
12514
13911
|
agentModes: record.agentModes,
|
|
13912
|
+
agentModels: record.agentModels,
|
|
12515
13913
|
createdAt: record.createdAt
|
|
12516
13914
|
};
|
|
12517
13915
|
}
|
|
@@ -12755,6 +14153,26 @@ var SessionManager = class {
|
|
|
12755
14153
|
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
12756
14154
|
return record !== void 0;
|
|
12757
14155
|
}
|
|
14156
|
+
// Public retitle entry point that works on live AND cold sessions.
|
|
14157
|
+
// - Live: routes through Session.retitle so attached clients receive
|
|
14158
|
+
// a session_info_update broadcast (and persistTitle fires from the
|
|
14159
|
+
// onTitleChange handler, just like /hydra title).
|
|
14160
|
+
// - Cold: writes the new title straight into meta.json — there's
|
|
14161
|
+
// nothing in memory to broadcast to, but a later resurrect / list
|
|
14162
|
+
// will pick up the new title.
|
|
14163
|
+
// Returns false when no record exists at all (live or on disk).
|
|
14164
|
+
async setTitle(sessionId, title) {
|
|
14165
|
+
const live = this.get(sessionId);
|
|
14166
|
+
if (live) {
|
|
14167
|
+
await live.retitle(title);
|
|
14168
|
+
return true;
|
|
14169
|
+
}
|
|
14170
|
+
if (!await this.hasRecord(sessionId)) {
|
|
14171
|
+
return false;
|
|
14172
|
+
}
|
|
14173
|
+
await this.persistTitle(sessionId, title);
|
|
14174
|
+
return true;
|
|
14175
|
+
}
|
|
12758
14176
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
12759
14177
|
// was written at create time; updating it here keeps the session
|
|
12760
14178
|
// record's title in sync with what was broadcast to clients so a
|
|
@@ -12807,6 +14225,7 @@ var SessionManager = class {
|
|
|
12807
14225
|
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
12808
14226
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
12809
14227
|
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
14228
|
+
...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
|
|
12810
14229
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12811
14230
|
});
|
|
12812
14231
|
});
|
|
@@ -12908,6 +14327,18 @@ function mergeForPersistence(session, existing) {
|
|
|
12908
14327
|
return out;
|
|
12909
14328
|
}) : void 0;
|
|
12910
14329
|
const agentModes = persistedModes ?? existing?.agentModes;
|
|
14330
|
+
const sessionModels = session.availableModels();
|
|
14331
|
+
const persistedModels = sessionModels.length > 0 ? sessionModels.map((m) => {
|
|
14332
|
+
const out = { modelId: m.modelId };
|
|
14333
|
+
if (m.name !== void 0) {
|
|
14334
|
+
out.name = m.name;
|
|
14335
|
+
}
|
|
14336
|
+
if (m.description !== void 0) {
|
|
14337
|
+
out.description = m.description;
|
|
14338
|
+
}
|
|
14339
|
+
return out;
|
|
14340
|
+
}) : void 0;
|
|
14341
|
+
const agentModels = persistedModels ?? existing?.agentModels;
|
|
12911
14342
|
return recordFromMemorySession({
|
|
12912
14343
|
sessionId: session.sessionId,
|
|
12913
14344
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
@@ -12924,6 +14355,7 @@ function mergeForPersistence(session, existing) {
|
|
|
12924
14355
|
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
12925
14356
|
agentCommands,
|
|
12926
14357
|
agentModes,
|
|
14358
|
+
agentModels,
|
|
12927
14359
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
12928
14360
|
});
|
|
12929
14361
|
}
|
|
@@ -12989,6 +14421,40 @@ function asString(value) {
|
|
|
12989
14421
|
function nonEmptyOrUndefined(arr) {
|
|
12990
14422
|
return arr.length > 0 ? arr : void 0;
|
|
12991
14423
|
}
|
|
14424
|
+
function extractInitialModels(result) {
|
|
14425
|
+
const direct = parseModelsList(result.availableModels);
|
|
14426
|
+
if (direct.length > 0) {
|
|
14427
|
+
return direct;
|
|
14428
|
+
}
|
|
14429
|
+
const models = result.models;
|
|
14430
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
14431
|
+
const fromModelsObj = parseModelsList(
|
|
14432
|
+
models.availableModels
|
|
14433
|
+
);
|
|
14434
|
+
if (fromModelsObj.length > 0) {
|
|
14435
|
+
return fromModelsObj;
|
|
14436
|
+
}
|
|
14437
|
+
}
|
|
14438
|
+
const meta = result._meta;
|
|
14439
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
14440
|
+
for (const [key, value] of Object.entries(
|
|
14441
|
+
meta
|
|
14442
|
+
)) {
|
|
14443
|
+
if (key === "hydra-acp") {
|
|
14444
|
+
continue;
|
|
14445
|
+
}
|
|
14446
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
14447
|
+
const fromMeta = parseModelsList(
|
|
14448
|
+
value.availableModels
|
|
14449
|
+
);
|
|
14450
|
+
if (fromMeta.length > 0) {
|
|
14451
|
+
return fromMeta;
|
|
14452
|
+
}
|
|
14453
|
+
}
|
|
14454
|
+
}
|
|
14455
|
+
}
|
|
14456
|
+
return [];
|
|
14457
|
+
}
|
|
12992
14458
|
function extractInitialModes(result) {
|
|
12993
14459
|
const direct = parseModesList(result.availableModes);
|
|
12994
14460
|
if (direct.length > 0) {
|
|
@@ -13966,6 +15432,35 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
13966
15432
|
}
|
|
13967
15433
|
reply.code(204).send();
|
|
13968
15434
|
});
|
|
15435
|
+
app.patch("/v1/sessions/:id", async (request, reply) => {
|
|
15436
|
+
const raw = request.params.id;
|
|
15437
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
15438
|
+
const body = request.body ?? {};
|
|
15439
|
+
if (body.regen === true) {
|
|
15440
|
+
const session = manager.get(id);
|
|
15441
|
+
if (!session) {
|
|
15442
|
+
reply.code(409).send({ error: "regen requires a live session" });
|
|
15443
|
+
return;
|
|
15444
|
+
}
|
|
15445
|
+
void session.retitleFromAgent().catch((err) => {
|
|
15446
|
+
app.log.warn(
|
|
15447
|
+
`title regen failed for ${id}: ${err.message}`
|
|
15448
|
+
);
|
|
15449
|
+
});
|
|
15450
|
+
reply.code(202).send();
|
|
15451
|
+
return;
|
|
15452
|
+
}
|
|
15453
|
+
if (typeof body.title !== "string" || body.title.trim().length === 0) {
|
|
15454
|
+
reply.code(400).send({ error: "title must be a non-empty string" });
|
|
15455
|
+
return;
|
|
15456
|
+
}
|
|
15457
|
+
const ok = await manager.setTitle(id, body.title);
|
|
15458
|
+
if (!ok) {
|
|
15459
|
+
reply.code(404).send({ error: "session not found" });
|
|
15460
|
+
return;
|
|
15461
|
+
}
|
|
15462
|
+
reply.code(204).send();
|
|
15463
|
+
});
|
|
13969
15464
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
13970
15465
|
const raw = request.params.id;
|
|
13971
15466
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -14508,7 +16003,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14508
16003
|
mcpServers: params.mcpServers,
|
|
14509
16004
|
title: hydraMeta.name,
|
|
14510
16005
|
agentArgs: hydraMeta.agentArgs,
|
|
14511
|
-
model: hydraMeta.model
|
|
16006
|
+
model: hydraMeta.model,
|
|
16007
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
14512
16008
|
});
|
|
14513
16009
|
const client = bindClientToSession(connection, session, state);
|
|
14514
16010
|
const { entries: replay } = await session.attach(client, "full");
|
|
@@ -14524,6 +16020,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14524
16020
|
})();
|
|
14525
16021
|
});
|
|
14526
16022
|
const modesPayload = buildModesPayload(session);
|
|
16023
|
+
const modelsPayload = buildModelsPayload(session);
|
|
14527
16024
|
return {
|
|
14528
16025
|
sessionId: session.sessionId,
|
|
14529
16026
|
// session/new is implicitly an attach; mirror session/attach's
|
|
@@ -14532,6 +16029,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14532
16029
|
// events without an extra round-trip.
|
|
14533
16030
|
clientId: client.clientId,
|
|
14534
16031
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16032
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
14535
16033
|
_meta: buildResponseMeta(session)
|
|
14536
16034
|
};
|
|
14537
16035
|
});
|
|
@@ -14567,7 +16065,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14567
16065
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14568
16066
|
throw err;
|
|
14569
16067
|
}
|
|
14570
|
-
session = await deps.manager.resurrect(
|
|
16068
|
+
session = await deps.manager.resurrect({
|
|
16069
|
+
...resurrectParams,
|
|
16070
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
16071
|
+
});
|
|
14571
16072
|
}
|
|
14572
16073
|
const client = bindClientToSession(
|
|
14573
16074
|
connection,
|
|
@@ -14593,6 +16094,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14593
16094
|
}
|
|
14594
16095
|
session.replayPendingPermissions(client);
|
|
14595
16096
|
const modesPayload = buildModesPayload(session);
|
|
16097
|
+
const modelsPayload = buildModelsPayload(session);
|
|
14596
16098
|
return {
|
|
14597
16099
|
sessionId: session.sessionId,
|
|
14598
16100
|
clientId: client.clientId,
|
|
@@ -14604,6 +16106,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14604
16106
|
historyPolicy: appliedPolicy,
|
|
14605
16107
|
replayed: replay.length,
|
|
14606
16108
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16109
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
14607
16110
|
_meta: buildResponseMeta(session)
|
|
14608
16111
|
};
|
|
14609
16112
|
});
|
|
@@ -14711,6 +16214,22 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14711
16214
|
}
|
|
14712
16215
|
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
14713
16216
|
});
|
|
16217
|
+
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
16218
|
+
const params = AmendPromptParams.parse(raw);
|
|
16219
|
+
const att = state.attached.get(params.sessionId);
|
|
16220
|
+
if (!att) {
|
|
16221
|
+
const err = new Error("not attached to session");
|
|
16222
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
16223
|
+
throw err;
|
|
16224
|
+
}
|
|
16225
|
+
const session = deps.manager.get(params.sessionId);
|
|
16226
|
+
if (!session) {
|
|
16227
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
16228
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
16229
|
+
throw err;
|
|
16230
|
+
}
|
|
16231
|
+
return session.amendPrompt(att.clientId, params);
|
|
16232
|
+
});
|
|
14714
16233
|
connection.onRequest("session/load", async (raw) => {
|
|
14715
16234
|
const rawObj = raw ?? {};
|
|
14716
16235
|
const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
|
|
@@ -14743,15 +16262,39 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14743
16262
|
}
|
|
14744
16263
|
session.replayPendingPermissions(client);
|
|
14745
16264
|
const modesPayload = buildModesPayload(session);
|
|
16265
|
+
const modelsPayload = buildModelsPayload(session);
|
|
14746
16266
|
return {
|
|
14747
16267
|
sessionId: session.sessionId,
|
|
14748
16268
|
// Same as session/new: include clientId so the deferred-echo
|
|
14749
16269
|
// path in queue-aware clients can recognize own broadcasts.
|
|
14750
16270
|
clientId: client.clientId,
|
|
14751
16271
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16272
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
14752
16273
|
_meta: buildResponseMeta(session)
|
|
14753
16274
|
};
|
|
14754
16275
|
});
|
|
16276
|
+
connection.onRequest("session/set_model", async (rawParams) => {
|
|
16277
|
+
const decision = decideSetModel(rawParams, deps.manager);
|
|
16278
|
+
if (decision.kind === "error") {
|
|
16279
|
+
app.log.warn(decision.logMessage);
|
|
16280
|
+
const err = new Error(decision.message);
|
|
16281
|
+
err.code = decision.code;
|
|
16282
|
+
throw err;
|
|
16283
|
+
}
|
|
16284
|
+
if (decision.kind === "no_op") {
|
|
16285
|
+
app.log.warn(decision.logMessage);
|
|
16286
|
+
await connection.notify("session/update", {
|
|
16287
|
+
sessionId: decision.sessionId,
|
|
16288
|
+
update: {
|
|
16289
|
+
sessionUpdate: "current_model_update",
|
|
16290
|
+
currentModel: decision.currentModel
|
|
16291
|
+
}
|
|
16292
|
+
}).catch(() => void 0);
|
|
16293
|
+
return null;
|
|
16294
|
+
}
|
|
16295
|
+
app.log.info(decision.logMessage);
|
|
16296
|
+
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
16297
|
+
});
|
|
14755
16298
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
14756
16299
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
14757
16300
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -14774,6 +16317,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
14774
16317
|
});
|
|
14775
16318
|
});
|
|
14776
16319
|
}
|
|
16320
|
+
function makeInstallProgressForwarder(connection) {
|
|
16321
|
+
return (event) => {
|
|
16322
|
+
const payload = {
|
|
16323
|
+
agentId: event.agentId,
|
|
16324
|
+
version: event.version,
|
|
16325
|
+
source: event.source,
|
|
16326
|
+
phase: event.phase
|
|
16327
|
+
};
|
|
16328
|
+
if ("receivedBytes" in event) {
|
|
16329
|
+
payload.receivedBytes = event.receivedBytes;
|
|
16330
|
+
}
|
|
16331
|
+
if ("totalBytes" in event) {
|
|
16332
|
+
payload.totalBytes = event.totalBytes;
|
|
16333
|
+
}
|
|
16334
|
+
if ("packageSpec" in event) {
|
|
16335
|
+
payload.packageSpec = event.packageSpec;
|
|
16336
|
+
}
|
|
16337
|
+
void connection.notify(AGENT_INSTALL_PROGRESS_METHOD, payload).catch(() => void 0);
|
|
16338
|
+
};
|
|
16339
|
+
}
|
|
14777
16340
|
function buildModesPayload(session) {
|
|
14778
16341
|
const modes = session.availableModes();
|
|
14779
16342
|
if (modes.length === 0) {
|
|
@@ -14794,6 +16357,94 @@ function buildModesPayload(session) {
|
|
|
14794
16357
|
const currentModeId = session.currentMode ?? modes[0].id;
|
|
14795
16358
|
return { currentModeId, availableModes };
|
|
14796
16359
|
}
|
|
16360
|
+
function buildModelsPayload(session) {
|
|
16361
|
+
const models = session.availableModels();
|
|
16362
|
+
if (models.length === 0) {
|
|
16363
|
+
return void 0;
|
|
16364
|
+
}
|
|
16365
|
+
const availableModels = models.map((m) => {
|
|
16366
|
+
const out = {
|
|
16367
|
+
modelId: m.modelId
|
|
16368
|
+
};
|
|
16369
|
+
if (m.name !== void 0) {
|
|
16370
|
+
out.name = m.name;
|
|
16371
|
+
}
|
|
16372
|
+
if (m.description !== void 0) {
|
|
16373
|
+
out.description = m.description;
|
|
16374
|
+
}
|
|
16375
|
+
return out;
|
|
16376
|
+
});
|
|
16377
|
+
const currentModelId = session.currentModel ?? models[0].modelId;
|
|
16378
|
+
return { currentModelId, availableModels };
|
|
16379
|
+
}
|
|
16380
|
+
function decideSetModel(rawParams, manager) {
|
|
16381
|
+
if (!rawParams || typeof rawParams !== "object") {
|
|
16382
|
+
return {
|
|
16383
|
+
kind: "error",
|
|
16384
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16385
|
+
message: "session/set_model requires params",
|
|
16386
|
+
logMessage: "session/set_model rejected: params not an object"
|
|
16387
|
+
};
|
|
16388
|
+
}
|
|
16389
|
+
const params = rawParams;
|
|
16390
|
+
if (typeof params.sessionId !== "string") {
|
|
16391
|
+
return {
|
|
16392
|
+
kind: "error",
|
|
16393
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16394
|
+
message: "session/set_model requires string sessionId",
|
|
16395
|
+
logMessage: "session/set_model rejected: missing/non-string sessionId"
|
|
16396
|
+
};
|
|
16397
|
+
}
|
|
16398
|
+
if (typeof params.modelId !== "string") {
|
|
16399
|
+
return {
|
|
16400
|
+
kind: "error",
|
|
16401
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16402
|
+
message: "session/set_model requires string modelId",
|
|
16403
|
+
logMessage: `session/set_model rejected: missing/non-string modelId sessionId=${params.sessionId}`
|
|
16404
|
+
};
|
|
16405
|
+
}
|
|
16406
|
+
const session = manager.get(params.sessionId);
|
|
16407
|
+
if (!session) {
|
|
16408
|
+
return {
|
|
16409
|
+
kind: "error",
|
|
16410
|
+
code: JsonRpcErrorCodes.SessionNotFound,
|
|
16411
|
+
message: `session ${params.sessionId} not found`,
|
|
16412
|
+
logMessage: `session/set_model rejected: session not found sessionId=${params.sessionId}`
|
|
16413
|
+
};
|
|
16414
|
+
}
|
|
16415
|
+
const advertised = session.availableModels();
|
|
16416
|
+
if (advertised.length === 0) {
|
|
16417
|
+
return {
|
|
16418
|
+
kind: "ok",
|
|
16419
|
+
session,
|
|
16420
|
+
logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16421
|
+
};
|
|
16422
|
+
}
|
|
16423
|
+
const match = advertised.find((m) => m.modelId === params.modelId);
|
|
16424
|
+
if (!match) {
|
|
16425
|
+
const known = advertised.map((m) => m.modelId).join(", ");
|
|
16426
|
+
if (session.currentModel !== void 0 && session.currentModel.length > 0) {
|
|
16427
|
+
return {
|
|
16428
|
+
kind: "no_op",
|
|
16429
|
+
session,
|
|
16430
|
+
sessionId: params.sessionId,
|
|
16431
|
+
currentModel: session.currentModel,
|
|
16432
|
+
logMessage: `session/set_model no_op (resyncing client) sessionId=${params.sessionId} requested=${JSON.stringify(params.modelId)} actual=${JSON.stringify(session.currentModel)} agentId=${session.agentId} known=[${known}]`
|
|
16433
|
+
};
|
|
16434
|
+
}
|
|
16435
|
+
return {
|
|
16436
|
+
kind: "error",
|
|
16437
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16438
|
+
message: `model "${params.modelId}" is not in this session's availableModels (agent ${session.agentId}); known models: ${known}`,
|
|
16439
|
+
logMessage: `session/set_model rejected sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)} agentId=${session.agentId} known=[${known}] (no current model to fall back to)`
|
|
16440
|
+
};
|
|
16441
|
+
}
|
|
16442
|
+
return {
|
|
16443
|
+
kind: "ok",
|
|
16444
|
+
session,
|
|
16445
|
+
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16446
|
+
};
|
|
16447
|
+
}
|
|
14797
16448
|
function buildResponseMeta(session) {
|
|
14798
16449
|
const ours = {
|
|
14799
16450
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -14823,6 +16474,10 @@ function buildResponseMeta(session) {
|
|
|
14823
16474
|
if (modes.length > 0) {
|
|
14824
16475
|
ours.availableModes = modes;
|
|
14825
16476
|
}
|
|
16477
|
+
const models = session.availableModels();
|
|
16478
|
+
if (models.length > 0) {
|
|
16479
|
+
ours.availableModels = models;
|
|
16480
|
+
}
|
|
14826
16481
|
if (session.turnStartedAt !== void 0) {
|
|
14827
16482
|
ours.turnStartedAt = session.turnStartedAt;
|
|
14828
16483
|
}
|
|
@@ -14863,10 +16518,17 @@ function buildInitializeResult() {
|
|
|
14863
16518
|
],
|
|
14864
16519
|
// Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
|
|
14865
16520
|
// ACP clients ignore the field; capability-aware clients learn here
|
|
14866
|
-
//
|
|
14867
|
-
//
|
|
14868
|
-
//
|
|
14869
|
-
|
|
16521
|
+
// which hydra-acp extensions the daemon supports so they can gate
|
|
16522
|
+
// UI surface accordingly. promptPipelining is false until the
|
|
16523
|
+
// streaming-input probe lands (Option A in the steering brief);
|
|
16524
|
+
// the others are unconditional method-availability flags.
|
|
16525
|
+
_meta: mergeMeta(void 0, {
|
|
16526
|
+
promptQueueing: true,
|
|
16527
|
+
promptCancelling: true,
|
|
16528
|
+
promptUpdating: true,
|
|
16529
|
+
promptAmending: true,
|
|
16530
|
+
promptPipelining: false
|
|
16531
|
+
})
|
|
14870
16532
|
};
|
|
14871
16533
|
}
|
|
14872
16534
|
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
@@ -15051,6 +16713,7 @@ function ensureLoopbackOrTls(config) {
|
|
|
15051
16713
|
|
|
15052
16714
|
// src/cli/commands/daemon.ts
|
|
15053
16715
|
init_daemon_bootstrap();
|
|
16716
|
+
init_hydra_version();
|
|
15054
16717
|
|
|
15055
16718
|
// src/cli/commands/log-tail.ts
|
|
15056
16719
|
import * as fs16 from "fs";
|
|
@@ -15281,6 +16944,8 @@ async function runDaemonStatus() {
|
|
|
15281
16944
|
const info = await readPidFile();
|
|
15282
16945
|
if (!info) {
|
|
15283
16946
|
process.stdout.write("Daemon: not running\n");
|
|
16947
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
16948
|
+
`);
|
|
15284
16949
|
return;
|
|
15285
16950
|
}
|
|
15286
16951
|
const alive = isProcessAlive(info.pid);
|
|
@@ -15288,6 +16953,52 @@ async function runDaemonStatus() {
|
|
|
15288
16953
|
`Daemon: ${alive ? "running" : "stale pid file"} pid=${info.pid} host=${info.host} port=${info.port} started=${info.startedAt}
|
|
15289
16954
|
`
|
|
15290
16955
|
);
|
|
16956
|
+
let daemonVersion;
|
|
16957
|
+
if (alive) {
|
|
16958
|
+
try {
|
|
16959
|
+
const config = await loadConfig();
|
|
16960
|
+
daemonVersion = await fetchDaemonVersion(config);
|
|
16961
|
+
} catch {
|
|
16962
|
+
}
|
|
16963
|
+
}
|
|
16964
|
+
if (daemonVersion === void 0) {
|
|
16965
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
16966
|
+
`);
|
|
16967
|
+
if (alive) {
|
|
16968
|
+
process.stdout.write(
|
|
16969
|
+
"Daemon version: unknown (health endpoint unreachable)\n"
|
|
16970
|
+
);
|
|
16971
|
+
}
|
|
16972
|
+
return;
|
|
16973
|
+
}
|
|
16974
|
+
if (daemonVersion === HYDRA_VERSION) {
|
|
16975
|
+
process.stdout.write(`Version: ${HYDRA_VERSION}
|
|
16976
|
+
`);
|
|
16977
|
+
return;
|
|
16978
|
+
}
|
|
16979
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
16980
|
+
`);
|
|
16981
|
+
process.stdout.write(`Daemon version: ${daemonVersion}
|
|
16982
|
+
`);
|
|
16983
|
+
process.stdout.write(
|
|
16984
|
+
chalk.yellow(
|
|
16985
|
+
"Version mismatch \u2014 run `hydra-acp daemon restart` to upgrade the daemon.\n"
|
|
16986
|
+
)
|
|
16987
|
+
);
|
|
16988
|
+
}
|
|
16989
|
+
async function fetchDaemonVersion(config) {
|
|
16990
|
+
const protocol = config.daemon.tls ? "https" : "http";
|
|
16991
|
+
const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/v1/health`;
|
|
16992
|
+
try {
|
|
16993
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
|
|
16994
|
+
if (!response.ok) {
|
|
16995
|
+
return void 0;
|
|
16996
|
+
}
|
|
16997
|
+
const body = await response.json();
|
|
16998
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
16999
|
+
} catch {
|
|
17000
|
+
return void 0;
|
|
17001
|
+
}
|
|
15291
17002
|
}
|
|
15292
17003
|
async function readPidFile() {
|
|
15293
17004
|
try {
|