@hydra-acp/cli 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1397,7 +1397,10 @@ var init_connection = __esm({
1397
1397
  // every entry would be re-appended to history.jsonl, doubling the log
1398
1398
  // each time the session was woken up.
1399
1399
  drainBuffered(method) {
1400
+ const buf = this.bufferedNotifications.get(method);
1401
+ const count = buf?.length ?? 0;
1400
1402
  this.bufferedNotifications.delete(method);
1403
+ return count;
1401
1404
  }
1402
1405
  onClose(handler) {
1403
1406
  this.closeHandlers.push(handler);
@@ -2265,6 +2268,8 @@ var init_session = __esm({
2265
2268
  listSessions;
2266
2269
  logger;
2267
2270
  transformChain;
2271
+ extensionCommands;
2272
+ extensionCommandsUnsub;
2268
2273
  // Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
2269
2274
  pendingClaims = /* @__PURE__ */ new Map();
2270
2275
  agentChangeHandlers = [];
@@ -2360,6 +2365,14 @@ var init_session = __esm({
2360
2365
  this.listSessions = init.listSessions;
2361
2366
  this.logger = init.logger;
2362
2367
  this.transformChain = init.transformChain ?? [];
2368
+ this.extensionCommands = init.extensionCommands;
2369
+ if (this.extensionCommands) {
2370
+ this.extensionCommandsUnsub = this.extensionCommands.onChange(() => {
2371
+ if (!this.closed) {
2372
+ this.broadcastMergedCommands();
2373
+ }
2374
+ });
2375
+ }
2363
2376
  if (init.firstPromptSeeded) {
2364
2377
  this.firstPromptSeeded = true;
2365
2378
  }
@@ -2374,18 +2387,11 @@ var init_session = __esm({
2374
2387
  this.notifyChain("session.opened", {});
2375
2388
  }
2376
2389
  broadcastMergedCommands() {
2377
- const merged = [
2378
- ...hydraCommandsAsAdvertised(),
2379
- { name: "model <model-id>", description: "Switch model; omit arg to list available models" },
2380
- { name: "sessions", description: "List all sessions" },
2381
- { name: "help", description: "Show available commands" },
2382
- ...this.agentAdvertisedCommands
2383
- ];
2384
2390
  this.recordAndBroadcast("session/update", {
2385
2391
  sessionId: this.upstreamSessionId,
2386
2392
  update: {
2387
2393
  sessionUpdate: "available_commands_update",
2388
- availableCommands: merged
2394
+ availableCommands: this.mergedAvailableCommands()
2389
2395
  }
2390
2396
  });
2391
2397
  }
@@ -3548,6 +3554,9 @@ var init_session = __esm({
3548
3554
  if (!trimmed || trimmed === this.currentModel) {
3549
3555
  return true;
3550
3556
  }
3557
+ this.logger?.info(
3558
+ `live current_model_update: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
3559
+ );
3551
3560
  this.currentModel = trimmed;
3552
3561
  for (const handler of this.modelHandlers) {
3553
3562
  try {
@@ -3593,6 +3602,9 @@ var init_session = __esm({
3593
3602
  if (typeof cv === "string") {
3594
3603
  const trimmed = cv.trim();
3595
3604
  if (trimmed && trimmed !== this.currentModel) {
3605
+ this.logger?.info(
3606
+ `live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
3607
+ );
3596
3608
  this.currentModel = trimmed;
3597
3609
  for (const handler of this.modelHandlers) {
3598
3610
  try {
@@ -3761,6 +3773,9 @@ var init_session = __esm({
3761
3773
  this.broadcastAvailableModes();
3762
3774
  }
3763
3775
  setAgentAdvertisedModels(models) {
3776
+ this.logger?.info(
3777
+ `setAgentAdvertisedModels: sessionId=${this.sessionId} currentModel=${JSON.stringify(this.currentModel)} newList=[${models.map((m) => m.modelId).join(",")}]`
3778
+ );
3764
3779
  if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
3765
3780
  this.broadcastAvailableModels();
3766
3781
  return;
@@ -3792,6 +3807,38 @@ var init_session = __esm({
3792
3807
  onModeChange(handler) {
3793
3808
  this.modeHandlers.push(handler);
3794
3809
  }
3810
+ // Apply a model change initiated by a client request (session/set_model)
3811
+ // when the agent doesn't emit a current_model_update notification, or
3812
+ // emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
3813
+ // (persistence) and broadcasts a synthetic current_model_update so all
3814
+ // attached clients — including the originator — repaint immediately.
3815
+ applyModelChange(modelId) {
3816
+ const trimmed = modelId.trim();
3817
+ if (!trimmed || trimmed === this.currentModel) {
3818
+ return;
3819
+ }
3820
+ this.logger?.info(
3821
+ `applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
3822
+ );
3823
+ this.currentModel = trimmed;
3824
+ for (const handler of this.modelHandlers) {
3825
+ try {
3826
+ handler(trimmed);
3827
+ } catch {
3828
+ }
3829
+ }
3830
+ const update = {
3831
+ sessionUpdate: "current_model_update",
3832
+ currentModel: trimmed
3833
+ };
3834
+ if (this.agentAdvertisedModels.length > 0) {
3835
+ update.availableModels = [...this.agentAdvertisedModels];
3836
+ }
3837
+ this.recordAndBroadcast("session/update", {
3838
+ sessionId: this.upstreamSessionId,
3839
+ update
3840
+ });
3841
+ }
3795
3842
  // Apply a mode change initiated by a client request (session/set_mode)
3796
3843
  // when the agent doesn't emit a current_mode_update notification on its
3797
3844
  // own. Fires modeHandlers so the persistence hook and any other listeners
@@ -3815,11 +3862,31 @@ var init_session = __esm({
3815
3862
  onUsageChange(handler) {
3816
3863
  this.usageHandlers.push(handler);
3817
3864
  }
3818
- // Returns a freshly merged command list (hydra ∪ agent) for callers
3819
- // that need a snapshot — notably acp-ws.ts's buildResponseMeta when
3820
- // assembling the attach response.
3865
+ // Returns a freshly merged command list (hydra ∪ extension ∪ agent) for
3866
+ // callers that need a snapshot — notably acp-ws.ts's buildResponseMeta
3867
+ // when assembling the attach response. Order: built-in hydra verbs,
3868
+ // top-level daemon verbs (/model, /sessions, /help), extension-registered
3869
+ // entries, then whatever the agent advertised.
3821
3870
  mergedAvailableCommands() {
3822
- return [...hydraCommandsAsAdvertised(), ...this.agentAdvertisedCommands];
3871
+ const out = [
3872
+ ...hydraCommandsAsAdvertised(),
3873
+ { name: "model <model-id>", description: "Switch model; omit arg to list available models" },
3874
+ { name: "sessions", description: "List all sessions" },
3875
+ { name: "help", description: "Show available commands" }
3876
+ ];
3877
+ if (this.extensionCommands) {
3878
+ for (const { name, command } of this.extensionCommands.list()) {
3879
+ const head = `hydra ${name} ${command.verb}`;
3880
+ const display = command.argsHint ? `${head} ${command.argsHint}` : head;
3881
+ const entry = { name: display };
3882
+ if (command.description) {
3883
+ entry.description = command.description;
3884
+ }
3885
+ out.push(entry);
3886
+ }
3887
+ }
3888
+ out.push(...this.agentAdvertisedCommands);
3889
+ return out;
3823
3890
  }
3824
3891
  // The agent's own advertised commands (not merged with hydra verbs).
3825
3892
  // Used by SessionManager to persist into meta.json so cold resurrect
@@ -3871,39 +3938,118 @@ var init_session = __esm({
3871
3938
  // caller's promise resolves like a normal turn. To add a verb: append
3872
3939
  // an entry to HYDRA_COMMANDS (drives validation + client advertising)
3873
3940
  // and a dispatch case in the switch below.
3941
+ //
3942
+ // Extensions/transformers can also bind verbs via the
3943
+ // ExtensionCommandRegistry: "/hydra <process-name> <verb> [args]" routes
3944
+ // to that process's WS connection. Built-in hydra verbs win on name
3945
+ // collision so an extension can never shadow them.
3874
3946
  async handleSlashCommand(text) {
3875
3947
  const rest = text.slice("/hydra".length).trim();
3876
3948
  const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
3877
- const verb = match?.[1] ?? "";
3878
- const arg = (match?.[2] ?? "").trim();
3879
- if (verb === "") {
3949
+ const first = match?.[1] ?? "";
3950
+ const remainder = (match?.[2] ?? "").trim();
3951
+ if (first === "") {
3880
3952
  return { stopReason: "end_turn" };
3881
3953
  }
3882
- if (!HYDRA_COMMANDS.some((c) => c.verb === verb)) {
3883
- const known = HYDRA_COMMANDS.map((c) => c.verb).join(", ");
3884
- const err = new Error(
3885
- `unknown /hydra verb: ${verb} (known: ${known})`
3886
- );
3887
- err.code = JsonRpcErrorCodes.InvalidParams;
3888
- throw err;
3954
+ if (HYDRA_COMMANDS.some((c) => c.verb === first)) {
3955
+ switch (first) {
3956
+ case "title":
3957
+ return this.runTitleCommand(remainder);
3958
+ case "agent":
3959
+ return this.runAgentCommand(remainder);
3960
+ case "kill":
3961
+ return this.runKillCommand();
3962
+ case "restart":
3963
+ return this.runRestartCommand();
3964
+ default: {
3965
+ const err2 = new Error(
3966
+ `no dispatcher for /hydra verb ${first}`
3967
+ );
3968
+ err2.code = JsonRpcErrorCodes.InternalError;
3969
+ throw err2;
3970
+ }
3971
+ }
3889
3972
  }
3890
- switch (verb) {
3891
- case "title":
3892
- return this.runTitleCommand(arg);
3893
- case "agent":
3894
- return this.runAgentCommand(arg);
3895
- case "kill":
3896
- return this.runKillCommand();
3897
- case "restart":
3898
- return this.runRestartCommand();
3899
- default: {
3900
- const err = new Error(
3901
- `no dispatcher for /hydra verb ${verb}`
3902
- );
3903
- err.code = JsonRpcErrorCodes.InternalError;
3904
- throw err;
3973
+ if (this.extensionCommands?.has(first)) {
3974
+ return this.runExtensionCommand(first, remainder);
3975
+ }
3976
+ const known = HYDRA_COMMANDS.map((c) => c.verb);
3977
+ if (this.extensionCommands) {
3978
+ const seen = /* @__PURE__ */ new Set();
3979
+ for (const { name } of this.extensionCommands.list()) {
3980
+ if (!seen.has(name)) {
3981
+ known.push(name);
3982
+ seen.add(name);
3983
+ }
3905
3984
  }
3906
3985
  }
3986
+ const err = new Error(
3987
+ `unknown /hydra verb: ${first} (known: ${known.join(", ")})`
3988
+ );
3989
+ err.code = JsonRpcErrorCodes.InvalidParams;
3990
+ throw err;
3991
+ }
3992
+ // "/hydra <name> <verb> [args]" — name matches a registered extension
3993
+ // or transformer. We split the remainder into verb + args, validate the
3994
+ // verb against what the process advertised, and forward as a
3995
+ // hydra-acp/extension_command request on the process's WS connection.
3996
+ // The reply's text (if any) is broadcast as a synthetic
3997
+ // agent_message_chunk so it appears in the conversation alongside the
3998
+ // user's invocation.
3999
+ runExtensionCommand(name, remainder) {
4000
+ return this.enqueuePrompt(async () => {
4001
+ const entry = this.extensionCommands?.get(name);
4002
+ if (!entry) {
4003
+ return this.emitExtensionReply(
4004
+ `extension "${name}" is no longer connected`
4005
+ );
4006
+ }
4007
+ const m = remainder.match(/^(\S+)(?:\s+([\s\S]*))?$/);
4008
+ const verb = m?.[1] ?? "";
4009
+ const args = (m?.[2] ?? "").trim();
4010
+ if (verb === "") {
4011
+ const verbs = entry.commands.map((c) => c.verb).join(", ");
4012
+ return this.emitExtensionReply(
4013
+ `/hydra ${name} requires a verb (known: ${verbs || "(none)"})`
4014
+ );
4015
+ }
4016
+ if (!entry.commands.some((c) => c.verb === verb)) {
4017
+ const verbs = entry.commands.map((c) => c.verb).join(", ");
4018
+ return this.emitExtensionReply(
4019
+ `unknown verb "${verb}" for ${name} (known: ${verbs || "(none)"})`
4020
+ );
4021
+ }
4022
+ let reply;
4023
+ try {
4024
+ reply = await entry.connection.request("hydra-acp/extension_command", {
4025
+ sessionId: this.sessionId,
4026
+ verb,
4027
+ args
4028
+ });
4029
+ } catch (err) {
4030
+ return this.emitExtensionReply(
4031
+ `${name} ${verb}: ${err.message}`
4032
+ );
4033
+ }
4034
+ const text = reply && typeof reply === "object" && typeof reply.text === "string" ? reply.text : "";
4035
+ if (text.length > 0) {
4036
+ return this.emitExtensionReply(text);
4037
+ }
4038
+ return { stopReason: "end_turn" };
4039
+ });
4040
+ }
4041
+ emitExtensionReply(text) {
4042
+ this.recordAndBroadcast("session/update", {
4043
+ sessionId: this.upstreamSessionId,
4044
+ update: {
4045
+ sessionUpdate: "agent_message_chunk",
4046
+ content: { type: "text", text: `
4047
+ ${text}
4048
+ ` },
4049
+ _meta: { "hydra-acp": { synthetic: true } }
4050
+ }
4051
+ });
4052
+ return { stopReason: "end_turn" };
3907
4053
  }
3908
4054
  async handleSessionsCommand() {
3909
4055
  let text;
@@ -3966,11 +4112,15 @@ ${text}
3966
4112
  if (models.length === 0) {
3967
4113
  body = current ? `Current model: ${current}` : "_(no models advertised yet)_";
3968
4114
  } else {
4115
+ const inList = current ? models.some((m) => m.modelId === current) : true;
3969
4116
  const lines = models.map((m) => {
3970
4117
  const marker = m.modelId === current ? " \u25C0" : "";
3971
4118
  const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
3972
4119
  return `${m.modelId}${marker}${desc}`;
3973
4120
  });
4121
+ if (!inList && current) {
4122
+ lines.unshift(`${current} \u25C0`);
4123
+ }
3974
4124
  body = lines.join("\n");
3975
4125
  }
3976
4126
  this.recordAndBroadcast("session/update", {
@@ -4398,6 +4548,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
4398
4548
  }
4399
4549
  this.closed = true;
4400
4550
  this.cancelIdleTimer();
4551
+ if (this.extensionCommandsUnsub) {
4552
+ this.extensionCommandsUnsub();
4553
+ this.extensionCommandsUnsub = void 0;
4554
+ }
4401
4555
  if (this.currentEntry?.kind === "user") {
4402
4556
  this.broadcastTurnComplete(
4403
4557
  this.currentEntry.clientId,
@@ -5598,6 +5752,20 @@ async function regenSessionTitle(target, id, fetchImpl = fetch) {
5598
5752
  throw new Error(`daemon returned HTTP ${response.status}`);
5599
5753
  }
5600
5754
  }
5755
+ async function searchSessions(target, query, opts = {}, fetchImpl = fetch) {
5756
+ const url = new URL(`${target.baseUrl}/v1/sessions/search`);
5757
+ url.searchParams.set("q", query);
5758
+ if (opts.sessionIds && opts.sessionIds.length > 0) {
5759
+ url.searchParams.set("sessionIds", opts.sessionIds.join(","));
5760
+ }
5761
+ const response = await fetchImpl(url.toString(), {
5762
+ headers: { Authorization: `Bearer ${target.token}` }
5763
+ });
5764
+ if (!response.ok) {
5765
+ throw new Error(`daemon returned HTTP ${response.status}`);
5766
+ }
5767
+ return await response.json();
5768
+ }
5601
5769
  async function deleteSession(target, id, fetchImpl = fetch) {
5602
5770
  const response = await fetchImpl(`${target.baseUrl}/v1/sessions/${id}`, {
5603
5771
  method: "DELETE",
@@ -7277,7 +7445,7 @@ function writeStyled(term, text, style) {
7277
7445
  term(text);
7278
7446
  return;
7279
7447
  case "thought":
7280
- term.brightBlack.noFormat(text);
7448
+ term.brightBlack(text);
7281
7449
  return;
7282
7450
  case "tool":
7283
7451
  term.brightBlue.noFormat(text);
@@ -9982,8 +10150,8 @@ uncaught: ${err.stack ?? err.message}
9982
10150
  }
9983
10151
  });
9984
10152
 
9985
- // src/tui/picker.ts
9986
- async function pickSession(term, opts) {
10153
+ // src/tui/prompt-utils.ts
10154
+ function resetTerminalModes() {
9987
10155
  process.stdout.write("\x1B[<u");
9988
10156
  process.stdout.write("\x1B[?2004l");
9989
10157
  process.stdout.write("\x1B[>4;0m");
@@ -9993,55 +10161,503 @@ async function pickSession(term, opts) {
9993
10161
  process.stdout.write("\x1B[?1006l");
9994
10162
  process.stdout.write("\x1B[?1l");
9995
10163
  process.stdout.write("\x1B>");
9996
- const sortSessions = (sessions) => {
9997
- const score = (s) => {
9998
- if (s.status !== "live") {
9999
- return 0;
10000
- }
10001
- return s.cwd === opts.cwd ? 2 : 1;
10002
- };
10003
- return [...sessions].sort((a, b) => {
10004
- const tier = score(b) - score(a);
10005
- if (tier !== 0) {
10006
- return tier;
10007
- }
10008
- return b.updatedAt.slice(0, 16).localeCompare(a.updatedAt.slice(0, 16));
10009
- });
10164
+ }
10165
+ function readTermWidth(term) {
10166
+ return term.width ?? 80;
10167
+ }
10168
+ function readTermHeight(term) {
10169
+ return term.height ?? 24;
10170
+ }
10171
+ function drawBox(term, opts) {
10172
+ const termW = readTermWidth(term);
10173
+ const termH = readTermHeight(term);
10174
+ const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
10175
+ const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
10176
+ const contentW = Math.min(desiredContentW, maxContentW);
10177
+ const w = contentW + 2;
10178
+ const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
10179
+ const h = contentH + 2;
10180
+ const x = Math.max(1, Math.floor((termW - w) / 2) + 1);
10181
+ const y = Math.max(1, Math.floor((termH - h) / 2) + 1);
10182
+ term.moveTo(1, 1).eraseDisplayBelow();
10183
+ const topInner = HORIZ.repeat(w - 2);
10184
+ const top = renderTitleStrip(topInner, opts.title);
10185
+ term.moveTo(x, y);
10186
+ term.dim.noFormat(TL);
10187
+ paintTopStrip(term, top);
10188
+ term.dim.noFormat(TR);
10189
+ for (let row = 1; row <= contentH; row++) {
10190
+ term.moveTo(x, y + row);
10191
+ term.dim.noFormat(VERT);
10192
+ term.moveTo(x + w - 1, y + row);
10193
+ term.dim.noFormat(VERT);
10194
+ }
10195
+ term.moveTo(x, y + h - 1);
10196
+ term.dim.noFormat(BL + HORIZ.repeat(w - 2) + BR);
10197
+ return {
10198
+ x,
10199
+ y,
10200
+ w,
10201
+ h,
10202
+ contentX: x + 1,
10203
+ contentY: y + 1,
10204
+ contentW,
10205
+ contentH
10010
10206
  };
10011
- let cwdOnly = false;
10012
- let hostFilter = "__local";
10013
- if (opts.currentSessionId !== void 0) {
10014
- const current = opts.sessions.find(
10015
- (s) => s.sessionId === opts.currentSessionId
10016
- );
10017
- if (current?.importedFromMachine) {
10018
- hostFilter = "__all";
10207
+ }
10208
+ function renderTitleStrip(innerDashes, title) {
10209
+ if (!title) {
10210
+ return { dashes: innerDashes };
10211
+ }
10212
+ const chip = ` ${title} `;
10213
+ if (chip.length + 4 > innerDashes.length) {
10214
+ return { dashes: innerDashes };
10215
+ }
10216
+ const offset = 2;
10217
+ const dashes = innerDashes.slice(0, offset) + " ".repeat(chip.length) + innerDashes.slice(offset + chip.length);
10218
+ return { dashes, title: { offset, text: chip } };
10219
+ }
10220
+ function paintTopStrip(term, strip) {
10221
+ if (!strip.title) {
10222
+ term.dim.noFormat(strip.dashes);
10223
+ return;
10224
+ }
10225
+ term.dim.noFormat(strip.dashes.slice(0, strip.title.offset));
10226
+ term.brightCyan.noFormat(strip.title.text);
10227
+ term.dim.noFormat(strip.dashes.slice(strip.title.offset + strip.title.text.length));
10228
+ }
10229
+ var MAX_BOX_WIDTH, HORIZ, VERT, TL, TR, BL, BR;
10230
+ var init_prompt_utils = __esm({
10231
+ "src/tui/prompt-utils.ts"() {
10232
+ "use strict";
10233
+ MAX_BOX_WIDTH = 64;
10234
+ HORIZ = "\u2500";
10235
+ VERT = "\u2502";
10236
+ TL = "\u250C";
10237
+ TR = "\u2510";
10238
+ BL = "\u2514";
10239
+ BR = "\u2518";
10240
+ }
10241
+ });
10242
+
10243
+ // src/tui/import-action-prompt.ts
10244
+ function actionPromptStep(selected, key, choices = ACTION_CHOICES) {
10245
+ if (key.kind === "cancel") {
10246
+ return { kind: "cancel" };
10247
+ }
10248
+ if (key.kind === "back") {
10249
+ return { kind: "back" };
10250
+ }
10251
+ if (key.kind === "enter") {
10252
+ const choice = choices[selected];
10253
+ if (!choice) {
10254
+ return { kind: "back" };
10019
10255
  }
10256
+ return { kind: "resolve", action: choice.key };
10020
10257
  }
10021
- let allSessions = sortSessions(opts.sessions);
10022
- let visible = filterByHost(allSessions, hostFilter);
10023
- let rows = visible.map((s) => toRow(s, Date.now()));
10024
- let widths = computeWidths(rows);
10025
- let total = 1 + visible.length;
10026
- let selectedIdx = 0;
10027
- let scrollOffset = 0;
10028
- if (opts.currentSessionId !== void 0) {
10029
- const idx = visible.findIndex((s) => s.sessionId === opts.currentSessionId);
10258
+ if (key.kind === "up") {
10259
+ return {
10260
+ kind: "continue",
10261
+ selected: Math.max(0, selected - 1)
10262
+ };
10263
+ }
10264
+ if (key.kind === "down") {
10265
+ return {
10266
+ kind: "continue",
10267
+ selected: Math.min(choices.length - 1, selected + 1)
10268
+ };
10269
+ }
10270
+ if (key.kind === "char") {
10271
+ const lower = key.ch.toLowerCase();
10272
+ if (lower === "n") {
10273
+ return {
10274
+ kind: "continue",
10275
+ selected: Math.min(choices.length - 1, selected + 1)
10276
+ };
10277
+ }
10278
+ if (lower === "p") {
10279
+ return {
10280
+ kind: "continue",
10281
+ selected: Math.max(0, selected - 1)
10282
+ };
10283
+ }
10284
+ const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
10030
10285
  if (idx >= 0) {
10031
- selectedIdx = idx + 1;
10286
+ const choice = choices[idx];
10287
+ if (choice) {
10288
+ return { kind: "resolve", action: choice.key };
10289
+ }
10032
10290
  }
10033
10291
  }
10034
- let searchActive = false;
10035
- let searchTerm = "";
10036
- let mode = "normal";
10037
- let pendingAction = null;
10038
- let renameBuffer = "";
10039
- let transientStatus = null;
10040
- const composer = new InputDispatcher({ history: [] });
10041
- let termHeight = readTermHeight(term);
10042
- let termWidth = readTermWidth(term);
10043
- let viewportSize = 0;
10044
- let composerTitle = "";
10292
+ return { kind: "continue", selected };
10293
+ }
10294
+ async function promptForImportAction(term, session) {
10295
+ resetTerminalModes();
10296
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
10297
+ const fromMachine = session.importedFromMachine ?? "another machine";
10298
+ const originalCwd = shortenHomePath(session.cwd);
10299
+ let selected = ACTION_CHOICES.findIndex((c) => c.key === "view");
10300
+ if (selected < 0) {
10301
+ selected = 0;
10302
+ }
10303
+ const render = () => {
10304
+ const choiceRows = ACTION_CHOICES.length * 2;
10305
+ const contentHeight = 7 + choiceRows + 2;
10306
+ const layout = drawBox(term, {
10307
+ contentHeight,
10308
+ title: "Imported session"
10309
+ });
10310
+ const innerW = layout.contentW;
10311
+ const headerRows = [
10312
+ { label: "session: ", value: shortId2 },
10313
+ { label: "from: ", value: fromMachine },
10314
+ { label: "cwd: ", value: originalCwd }
10315
+ ];
10316
+ let row = 0;
10317
+ for (const hr of headerRows) {
10318
+ term.moveTo(layout.contentX, layout.contentY + row);
10319
+ term.dim.noFormat(` ${hr.label}`);
10320
+ term.noFormat(truncate2(hr.value, innerW - hr.label.length - 2));
10321
+ row++;
10322
+ }
10323
+ row++;
10324
+ term.moveTo(layout.contentX, layout.contentY + row);
10325
+ term.noFormat(" What do you want to do?");
10326
+ row += 2;
10327
+ for (let i = 0; i < ACTION_CHOICES.length; i++) {
10328
+ const choice = ACTION_CHOICES[i];
10329
+ if (!choice) {
10330
+ continue;
10331
+ }
10332
+ const pointer = i === selected ? "\u276F" : " ";
10333
+ const label = ` ${pointer} ${choice.label}`;
10334
+ term.moveTo(layout.contentX, layout.contentY + row);
10335
+ if (i === selected) {
10336
+ term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
10337
+ } else {
10338
+ term.noFormat(label);
10339
+ }
10340
+ row++;
10341
+ term.moveTo(layout.contentX, layout.contentY + row);
10342
+ term.dim.noFormat(` ${choice.description}`);
10343
+ row++;
10344
+ }
10345
+ row++;
10346
+ term.moveTo(layout.contentX, layout.contentY + row);
10347
+ term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 f/v jump \xB7 Esc back");
10348
+ return layout;
10349
+ };
10350
+ render();
10351
+ term.hideCursor();
10352
+ return await new Promise((resolve6) => {
10353
+ let resolved = false;
10354
+ const cleanup = () => {
10355
+ if (resolved) {
10356
+ return;
10357
+ }
10358
+ resolved = true;
10359
+ term.off("key", onKey);
10360
+ term.off("resize", onResize);
10361
+ term.grabInput(false);
10362
+ term.hideCursor(false);
10363
+ term.moveTo(1, 1).eraseDisplayBelow();
10364
+ };
10365
+ const finish = (value) => {
10366
+ cleanup();
10367
+ resolve6(value);
10368
+ };
10369
+ const onResize = () => {
10370
+ if (resolved) {
10371
+ return;
10372
+ }
10373
+ render();
10374
+ };
10375
+ const onKey = (name, _matches, data) => {
10376
+ const input = mapKey(name, data);
10377
+ if (!input) {
10378
+ return;
10379
+ }
10380
+ const step = actionPromptStep(selected, input);
10381
+ if (step.kind === "cancel") {
10382
+ finish("cancel");
10383
+ return;
10384
+ }
10385
+ if (step.kind === "back") {
10386
+ finish("back");
10387
+ return;
10388
+ }
10389
+ if (step.kind === "resolve") {
10390
+ finish(step.action);
10391
+ return;
10392
+ }
10393
+ if (step.selected !== selected) {
10394
+ selected = step.selected;
10395
+ render();
10396
+ }
10397
+ };
10398
+ term.grabInput({});
10399
+ term.on("key", onKey);
10400
+ term.on("resize", onResize);
10401
+ });
10402
+ }
10403
+ function mapKey(name, data) {
10404
+ if (name === "UP") {
10405
+ return { kind: "up" };
10406
+ }
10407
+ if (name === "DOWN") {
10408
+ return { kind: "down" };
10409
+ }
10410
+ if (name === "ENTER" || name === "KP_ENTER") {
10411
+ return { kind: "enter" };
10412
+ }
10413
+ if (name === "ESCAPE") {
10414
+ return { kind: "back" };
10415
+ }
10416
+ if (name === "CTRL_C" || name === "CTRL_D") {
10417
+ return { kind: "cancel" };
10418
+ }
10419
+ if (data?.isCharacter) {
10420
+ return { kind: "char", ch: name };
10421
+ }
10422
+ return null;
10423
+ }
10424
+ async function promptForLaunchOrView(term, session, focus) {
10425
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
10426
+ const titleOrCwd = session.title ?? shortenHomePath(session.cwd);
10427
+ let selected = 1;
10428
+ const CHOICES = [
10429
+ { label: "Launch", hotkey: "l", description: "start a new agent session" },
10430
+ { label: "View transcript", hotkey: "v", description: "open read-only, no agent spawn" }
10431
+ ];
10432
+ const render = () => {
10433
+ const layout = drawBox(term, { contentHeight: 11, title: "Open session" });
10434
+ const innerW = layout.contentW;
10435
+ let row = 0;
10436
+ term.moveTo(layout.contentX, layout.contentY + row);
10437
+ term.dim.noFormat(" session: ");
10438
+ term.noFormat(truncate2(shortId2, innerW - 10));
10439
+ row++;
10440
+ term.moveTo(layout.contentX, layout.contentY + row);
10441
+ term.noFormat(" " + truncate2(titleOrCwd, innerW - 2));
10442
+ row++;
10443
+ row++;
10444
+ term.moveTo(layout.contentX, layout.contentY + row);
10445
+ term.noFormat(" What do you want to do?");
10446
+ row += 2;
10447
+ for (let i = 0; i < CHOICES.length; i++) {
10448
+ const choice = CHOICES[i];
10449
+ if (!choice) {
10450
+ continue;
10451
+ }
10452
+ const pointer = i === selected ? "\u276F" : " ";
10453
+ const label = ` ${pointer} ${choice.label}`;
10454
+ term.moveTo(layout.contentX, layout.contentY + row);
10455
+ if (i === selected) {
10456
+ term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
10457
+ } else {
10458
+ term.noFormat(label);
10459
+ }
10460
+ row++;
10461
+ term.moveTo(layout.contentX, layout.contentY + row);
10462
+ term.dim.noFormat(` ${choice.description}`);
10463
+ row++;
10464
+ }
10465
+ row++;
10466
+ term.moveTo(layout.contentX, layout.contentY + row);
10467
+ term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 l/v jump \xB7 Esc back");
10468
+ };
10469
+ render();
10470
+ term.hideCursor();
10471
+ return await new Promise((resolve6) => {
10472
+ let resolved = false;
10473
+ const cleanup = () => {
10474
+ resolved = true;
10475
+ };
10476
+ const finish = (value) => {
10477
+ cleanup();
10478
+ focus.pop();
10479
+ resolve6(value);
10480
+ };
10481
+ const onKey = (name, _m, data) => {
10482
+ if (name === "CTRL_C" || name === "CTRL_D") {
10483
+ finish("cancel");
10484
+ return;
10485
+ }
10486
+ if (name === "ESCAPE") {
10487
+ finish("back");
10488
+ return;
10489
+ }
10490
+ if (name === "ENTER" || name === "KP_ENTER") {
10491
+ finish(selected === 0 ? "launch" : "view");
10492
+ return;
10493
+ }
10494
+ if (name === "UP" || name === "SHIFT_TAB") {
10495
+ if (selected > 0) {
10496
+ selected--;
10497
+ render();
10498
+ }
10499
+ return;
10500
+ }
10501
+ if (name === "DOWN" || name === "TAB") {
10502
+ if (selected < CHOICES.length - 1) {
10503
+ selected++;
10504
+ render();
10505
+ }
10506
+ return;
10507
+ }
10508
+ if (data?.isCharacter) {
10509
+ const lower = name.toLowerCase();
10510
+ if (lower === "l") {
10511
+ finish("launch");
10512
+ return;
10513
+ }
10514
+ if (lower === "v") {
10515
+ finish("view");
10516
+ return;
10517
+ }
10518
+ if (lower === "n") {
10519
+ if (selected < CHOICES.length - 1) {
10520
+ selected++;
10521
+ render();
10522
+ }
10523
+ return;
10524
+ }
10525
+ if (lower === "p") {
10526
+ if (selected > 0) {
10527
+ selected--;
10528
+ render();
10529
+ }
10530
+ return;
10531
+ }
10532
+ }
10533
+ };
10534
+ focus.push({
10535
+ onKey: (name, _m, data) => {
10536
+ if (!resolved) onKey(name, _m, data);
10537
+ },
10538
+ onResize: () => {
10539
+ if (!resolved) render();
10540
+ }
10541
+ });
10542
+ });
10543
+ }
10544
+ function truncate2(s, max) {
10545
+ if (max <= 1) {
10546
+ return "";
10547
+ }
10548
+ if (s.length <= max) {
10549
+ return s;
10550
+ }
10551
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
10552
+ }
10553
+ function padRight(s, w) {
10554
+ if (s.length >= w) {
10555
+ return s.slice(0, w);
10556
+ }
10557
+ return s + " ".repeat(w - s.length);
10558
+ }
10559
+ var ACTION_CHOICES;
10560
+ var init_import_action_prompt = __esm({
10561
+ "src/tui/import-action-prompt.ts"() {
10562
+ "use strict";
10563
+ init_paths();
10564
+ init_session();
10565
+ init_prompt_utils();
10566
+ ACTION_CHOICES = [
10567
+ {
10568
+ key: "fork-local",
10569
+ label: "Fork locally",
10570
+ hotkey: "f",
10571
+ description: "spawn a local fork \u2014 original imported copy stays as-is"
10572
+ },
10573
+ {
10574
+ key: "view",
10575
+ label: "View transcript",
10576
+ hotkey: "v",
10577
+ description: "open read-only, no agent spawn"
10578
+ }
10579
+ ];
10580
+ }
10581
+ });
10582
+
10583
+ // src/tui/picker.ts
10584
+ function createPickerPrefs() {
10585
+ return { filters: { cwdOnly: false, hostFilter: "__local" } };
10586
+ }
10587
+ async function pickSession(term, opts) {
10588
+ process.stdout.write("\x1B[<u");
10589
+ process.stdout.write("\x1B[?2004l");
10590
+ process.stdout.write("\x1B[>4;0m");
10591
+ process.stdout.write("\x1B[>5;0m");
10592
+ process.stdout.write("\x1B[?1000l");
10593
+ process.stdout.write("\x1B[?1002l");
10594
+ process.stdout.write("\x1B[?1006l");
10595
+ process.stdout.write("\x1B[?1l");
10596
+ process.stdout.write("\x1B>");
10597
+ const sortSessions = (sessions) => {
10598
+ const score = (s) => {
10599
+ if (s.status !== "live") {
10600
+ return 0;
10601
+ }
10602
+ return s.cwd === opts.cwd ? 2 : 1;
10603
+ };
10604
+ return [...sessions].sort((a, b) => {
10605
+ const tier = score(b) - score(a);
10606
+ if (tier !== 0) {
10607
+ return tier;
10608
+ }
10609
+ return b.updatedAt.slice(0, 16).localeCompare(a.updatedAt.slice(0, 16));
10610
+ });
10611
+ };
10612
+ const prefs = opts.prefs ?? createPickerPrefs();
10613
+ if (opts.prefs === void 0 && opts.currentSessionId !== void 0) {
10614
+ const current = opts.sessions.find(
10615
+ (s) => s.sessionId === opts.currentSessionId
10616
+ );
10617
+ if (current?.importedFromMachine) {
10618
+ prefs.filters.hostFilter = "__all";
10619
+ }
10620
+ }
10621
+ let allSessions = sortSessions(opts.sessions);
10622
+ const applyPrefsFilters = (sessions) => {
10623
+ let base = sessions;
10624
+ if (prefs.filters.cwdOnly) {
10625
+ base = base.filter((s) => s.cwd === opts.cwd);
10626
+ }
10627
+ base = filterByHost(base, prefs.filters.hostFilter);
10628
+ return base;
10629
+ };
10630
+ let visible = applyPrefsFilters(allSessions);
10631
+ let rows = visible.map((s) => toRow(s, Date.now()));
10632
+ let widths = computeWidths(rows);
10633
+ let total = 1 + visible.length;
10634
+ let selectedIdx = 0;
10635
+ let scrollOffset = 0;
10636
+ if (opts.currentSessionId !== void 0) {
10637
+ const idx = visible.findIndex((s) => s.sessionId === opts.currentSessionId);
10638
+ if (idx >= 0) {
10639
+ selectedIdx = idx + 1;
10640
+ }
10641
+ }
10642
+ let searchActive = false;
10643
+ let searchTerm = "";
10644
+ let mode = "normal";
10645
+ let pendingAction = null;
10646
+ let findSubMode = "input";
10647
+ let findComposer = new InputDispatcher({ history: [] });
10648
+ let findResults = [];
10649
+ let findTruncated = false;
10650
+ let findSelectedIdx = 0;
10651
+ let findSnippetIdx = 0;
10652
+ let findError = null;
10653
+ let findInFlight = false;
10654
+ let renameBuffer = "";
10655
+ let transientStatus = null;
10656
+ const composer = new InputDispatcher({ history: [] });
10657
+ let termHeight = readTermHeight2(term);
10658
+ let termWidth = readTermWidth2(term);
10659
+ let viewportSize = 0;
10660
+ let composerTitle = "";
10045
10661
  let composerRoom = 0;
10046
10662
  let composerVisualRows = [];
10047
10663
  let composerRows = 1;
@@ -10051,10 +10667,16 @@ async function pickSession(term, opts) {
10051
10667
  let headerLine = "";
10052
10668
  let sessionLines = [];
10053
10669
  let startRow = 1;
10670
+ let findRoom = 0;
10671
+ let findVisualRows = [];
10672
+ let findBoxRows = 1;
10673
+ let findBoxWindowStart = 0;
10674
+ let findBoxCursorVisualRow = 0;
10675
+ let findBoxCursorVisualCol = 0;
10054
10676
  const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
10055
10677
  const computeLayout = () => {
10056
- termHeight = readTermHeight(term);
10057
- termWidth = readTermWidth(term);
10678
+ termHeight = readTermHeight2(term);
10679
+ termWidth = readTermWidth2(term);
10058
10680
  const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
10059
10681
  composerRoom = Math.max(10, termWidth - BOX_HORIZONTAL_PAD);
10060
10682
  const titleBudget = Math.max(10, termWidth - 8);
@@ -10087,11 +10709,7 @@ async function pickSession(term, opts) {
10087
10709
  computeLayout();
10088
10710
  };
10089
10711
  const applyFilter = () => {
10090
- let base = allSessions;
10091
- if (cwdOnly) {
10092
- base = base.filter((s) => s.cwd === opts.cwd);
10093
- }
10094
- base = filterByHost(base, hostFilter);
10712
+ const base = applyPrefsFilters(allSessions);
10095
10713
  if (searchActive && searchTerm.length > 0) {
10096
10714
  visible = base.filter((s) => matchesSearch(s, searchTerm));
10097
10715
  } else {
@@ -10109,6 +10727,19 @@ async function pickSession(term, opts) {
10109
10727
  }
10110
10728
  adjustScroll();
10111
10729
  };
10730
+ const restoreCursorAfterFilter = (keepId) => {
10731
+ if (keepId !== void 0) {
10732
+ const idx = visible.findIndex((s) => s.sessionId === keepId);
10733
+ if (idx >= 0) {
10734
+ selectedIdx = idx + 1;
10735
+ adjustScroll();
10736
+ return;
10737
+ }
10738
+ }
10739
+ selectedIdx = visible.length > 0 ? 1 : 0;
10740
+ scrollOffset = 0;
10741
+ adjustScroll();
10742
+ };
10112
10743
  const adjustScroll = () => {
10113
10744
  if (selectedIdx === 0) {
10114
10745
  return;
@@ -10177,12 +10808,12 @@ async function pickSession(term, opts) {
10177
10808
  const above = scrollOffset;
10178
10809
  const below = Math.max(0, visible.length - scrollOffset - viewportSize);
10179
10810
  const parts = [];
10180
- if (cwdOnly) {
10811
+ if (prefs.filters.cwdOnly) {
10181
10812
  parts.push("cwd-only");
10182
10813
  }
10183
- if (hostFilter !== "__all") {
10814
+ if (prefs.filters.hostFilter !== "__all") {
10184
10815
  parts.push(
10185
- hostFilter === "__local" ? "host: local" : `host: ${hostFilter}`
10816
+ prefs.filters.hostFilter === "__local" ? "host: local" : `host: ${prefs.filters.hostFilter}`
10186
10817
  );
10187
10818
  }
10188
10819
  if (above > 0) {
@@ -10234,59 +10865,370 @@ async function pickSession(term, opts) {
10234
10865
  if (visualOffset < 0 || visualOffset >= composerRows) {
10235
10866
  return;
10236
10867
  }
10237
- const col = 3 + composerCursorCol;
10238
- term.moveTo(col, composerBodyRow(visualOffset));
10868
+ const col = 3 + composerCursorCol;
10869
+ term.moveTo(col, composerBodyRow(visualOffset));
10870
+ };
10871
+ const renderFromScratch = () => {
10872
+ withSync(() => {
10873
+ term.hideCursor();
10874
+ computeLayout();
10875
+ adjustScroll();
10876
+ startRow = 1;
10877
+ term.moveTo(1, 1).eraseDisplayBelow();
10878
+ paintComposerTopBorder();
10879
+ term("\n");
10880
+ for (let v = 0; v < composerRows; v++) {
10881
+ paintComposerBodyRow(composerWindowStart + v);
10882
+ term("\n");
10883
+ }
10884
+ paintComposerBottomBorder();
10885
+ term("\n\n");
10886
+ term.dim.noFormat(` ${headerLine}`)("\n");
10887
+ for (let v = 0; v < viewportSize; v++) {
10888
+ paintSessionRow(scrollOffset + v);
10889
+ term("\n");
10890
+ }
10891
+ paintIndicator();
10892
+ term("\n");
10893
+ if (selectedIdx === 0) {
10894
+ placeComposerCursor();
10895
+ term.hideCursor(false);
10896
+ }
10897
+ });
10898
+ };
10899
+ const renderHelp = () => {
10900
+ withSync(() => {
10901
+ term.hideCursor();
10902
+ term.moveTo(1, 1).eraseDisplayBelow();
10903
+ term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
10904
+ for (const entry of HELP_ENTRIES) {
10905
+ if (entry === null) {
10906
+ term("\n");
10907
+ continue;
10908
+ }
10909
+ const [keys, desc] = entry;
10910
+ term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
10911
+ term.noFormat(desc)("\n");
10912
+ }
10913
+ term("\n");
10914
+ term.dim.noFormat(" press any key to dismiss")("\n");
10915
+ });
10916
+ };
10917
+ const findResultsStartRow = () => findBoxRows + 4;
10918
+ const FIND_FOOTER_ROWS = 2;
10919
+ let findScrollOffset = 0;
10920
+ const findViewportSize = () => {
10921
+ termHeight = readTermHeight2(term);
10922
+ const avail = Math.max(2, termHeight - (findBoxRows + 3) - FIND_FOOTER_ROWS);
10923
+ return Math.max(1, Math.floor(avail / 2));
10924
+ };
10925
+ const adjustFindScroll = () => {
10926
+ const v = findViewportSize();
10927
+ if (findSelectedIdx < findScrollOffset) {
10928
+ findScrollOffset = findSelectedIdx;
10929
+ } else if (findSelectedIdx >= findScrollOffset + v) {
10930
+ findScrollOffset = findSelectedIdx - v + 1;
10931
+ }
10932
+ if (findScrollOffset + v > findResults.length) {
10933
+ findScrollOffset = Math.max(0, findResults.length - v);
10934
+ }
10935
+ if (findScrollOffset < 0) {
10936
+ findScrollOffset = 0;
10937
+ }
10938
+ };
10939
+ const paintFindBoxTopBorder = (focused) => {
10940
+ termWidth = readTermWidth2(term);
10941
+ const inner = Math.max(2, termWidth - 2);
10942
+ const title = "\u2500 Find sessions ";
10943
+ const dashes = "\u2500".repeat(Math.max(1, inner - title.length));
10944
+ if (focused) {
10945
+ term.brightBlue.noFormat(`\u256D${title}${dashes}\u256E`);
10946
+ } else {
10947
+ term.dim.noFormat(`\u256D${title}${dashes}\u256E`);
10948
+ }
10949
+ term.styleReset();
10950
+ };
10951
+ const computeFindBoxLayout = () => {
10952
+ termWidth = readTermWidth2(term);
10953
+ findRoom = Math.max(10, termWidth - BOX_HORIZONTAL_PAD);
10954
+ const state = findComposer.state();
10955
+ findVisualRows = computePromptVisualRows(state.buffer, findRoom);
10956
+ const layout = computePromptLayout(findVisualRows, state, FIND_BOX_MAX_ROWS);
10957
+ findBoxRows = layout.rendered;
10958
+ findBoxWindowStart = layout.windowStart;
10959
+ findBoxCursorVisualRow = layout.cursorVisualRow;
10960
+ findBoxCursorVisualCol = layout.cursorVisualCol;
10961
+ };
10962
+ const paintFindBoxBodyRow = (visualIdx, focused) => {
10963
+ termWidth = readTermWidth2(term);
10964
+ const inner = Math.max(2, termWidth - 2);
10965
+ const vr = findVisualRows[visualIdx];
10966
+ let slice = "";
10967
+ if (vr) {
10968
+ slice = (findComposer.state().buffer[vr.bufferIdx] ?? "").slice(
10969
+ vr.startCol,
10970
+ vr.endCol
10971
+ );
10972
+ }
10973
+ const padWidth = Math.max(0, inner - 1 - slice.length);
10974
+ const pad = " ".repeat(padWidth);
10975
+ if (focused) {
10976
+ term.brightBlue.noFormat("\u2502");
10977
+ term.noFormat(` ${slice}${pad}`);
10978
+ term.brightBlue.noFormat("\u2502");
10979
+ } else {
10980
+ term.dim.noFormat("\u2502");
10981
+ term.noFormat(` ${slice}${pad}`);
10982
+ term.dim.noFormat("\u2502");
10983
+ }
10984
+ term.styleReset();
10985
+ };
10986
+ const paintFindBoxBottomBorder = (focused) => {
10987
+ termWidth = readTermWidth2(term);
10988
+ const inner = Math.max(2, termWidth - 2);
10989
+ const dashes = "\u2500".repeat(inner);
10990
+ if (focused) {
10991
+ term.brightBlue.noFormat(`\u2570${dashes}\u256F`);
10992
+ } else {
10993
+ term.dim.noFormat(`\u2570${dashes}\u256F`);
10994
+ }
10995
+ term.styleReset();
10996
+ };
10997
+ const findBoxCursorCol = () => 3 + findBoxCursorVisualCol;
10998
+ const findBoxCursorScreenRow = () => 2 + (findBoxCursorVisualRow - findBoxWindowStart);
10999
+ const repaintFindBoxChrome = () => {
11000
+ const focused = findSubMode === "input";
11001
+ withSync(() => {
11002
+ if (focused) {
11003
+ term.hideCursor();
11004
+ }
11005
+ term.moveTo(1, 1);
11006
+ paintFindBoxTopBorder(focused);
11007
+ for (let v = 0; v < findBoxRows; v++) {
11008
+ term.moveTo(1, 2 + v);
11009
+ paintFindBoxBodyRow(findBoxWindowStart + v, focused);
11010
+ }
11011
+ term.moveTo(1, 2 + findBoxRows);
11012
+ paintFindBoxBottomBorder(focused);
11013
+ if (focused) {
11014
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11015
+ term.hideCursor(false);
11016
+ }
11017
+ });
11018
+ };
11019
+ const repaintFindBoxBodyRows = () => {
11020
+ withSync(() => {
11021
+ term.hideCursor();
11022
+ for (let v = 0; v < findBoxRows; v++) {
11023
+ term.moveTo(1, 2 + v);
11024
+ paintFindBoxBodyRow(findBoxWindowStart + v, true);
11025
+ }
11026
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11027
+ term.hideCursor(false);
11028
+ });
11029
+ };
11030
+ const SNIPPET_KIND_GLYPH = {
11031
+ user: "user",
11032
+ agent: "agent",
11033
+ thought: "thought",
11034
+ tool: "tool",
11035
+ "tool-input": "tool-input"
11036
+ };
11037
+ const findResultData = (idx, focused) => {
11038
+ const hit = findResults[idx];
11039
+ if (!hit) {
11040
+ return { rowBudget: 20, line1: "", line2: "", focusedRow: false };
11041
+ }
11042
+ const w = readTermWidth2(term);
11043
+ const rowBudget = Math.max(20, w - ROW_PREFIX_WIDTH);
11044
+ const shortId3 = stripHydraSessionPrefix(hit.sessionId);
11045
+ const title = hit.title ?? shortenHomePath(hit.cwd);
11046
+ const counterText = focused && hit.snippets.length > 1 ? ` [${findSnippetIdx + 1}/${hit.snippets.length}]` : focused && hit.totalMatches > hit.snippets.length ? ` [${hit.snippets.length} of ${hit.totalMatches}]` : "";
11047
+ const head = `${shortId3} ${hit.status === "live" ? "live" : "cold"}`;
11048
+ const titleBudget = Math.max(5, rowBudget - head.length - counterText.length - 2);
11049
+ const titleSlice = truncateMiddle(title, titleBudget);
11050
+ const line1 = `${head} ${titleSlice}${counterText}`.padEnd(rowBudget);
11051
+ const snippet = hit.snippets[focused ? findSnippetIdx : 0];
11052
+ const kind = snippet ? SNIPPET_KIND_GLYPH[snippet.kind] ?? snippet.kind : "";
11053
+ const prefix = snippet?.toolName ? `${kind} \xB7 ${snippet.toolName}` : kind;
11054
+ const snippetBudget = Math.max(10, rowBudget - prefix.length - 6);
11055
+ const text = snippet ? truncateMiddle(snippet.text, snippetBudget) : "";
11056
+ const line2 = snippet ? ` ${prefix} ${text}` : " (no snippet)";
11057
+ return { rowBudget, line1, line2: line2.padEnd(rowBudget + ROW_PREFIX_WIDTH), focusedRow: focused };
11058
+ };
11059
+ const paintFindResultA = (idx, focused) => {
11060
+ const { line1, focusedRow } = findResultData(idx, focused);
11061
+ if (focusedRow) {
11062
+ term.brightWhite.bgBlue.noFormat(`\u276F ${line1}`);
11063
+ } else {
11064
+ term.noFormat(` ${line1}`);
11065
+ }
11066
+ term.styleReset();
11067
+ };
11068
+ const paintFindResultB = (idx, focused) => {
11069
+ const { line2 } = findResultData(idx, focused);
11070
+ term.dim.noFormat(line2);
11071
+ term.styleReset();
11072
+ };
11073
+ const paintFindIndicator = () => {
11074
+ if (findInFlight) {
11075
+ term.dim.noFormat(" searching\u2026");
11076
+ term.styleReset();
11077
+ term.eraseLineAfter();
11078
+ } else if (findError !== null) {
11079
+ term.brightRed.noFormat(` ${findError}`);
11080
+ term.styleReset();
11081
+ term.eraseLineAfter();
11082
+ } else if (findSubMode === "input") {
11083
+ if (findResults.length > 0) {
11084
+ term.dim.noFormat(" Enter to search \xB7 \u2193 browse results \xB7 Esc cancel");
11085
+ } else {
11086
+ term.dim.noFormat(" Enter to search \xB7 Esc cancel");
11087
+ }
11088
+ term.styleReset();
11089
+ term.eraseLineAfter();
11090
+ } else {
11091
+ const sCount = findResults.length;
11092
+ const truncSuffix = findTruncated ? " \xB7 truncated" : "";
11093
+ const countPart = sCount > 0 ? ` ${sCount} ${sCount === 1 ? "session" : "sessions"} match${truncSuffix} \xB7 ` : " ";
11094
+ term.dim.noFormat(
11095
+ `${countPart}\u2191 edit query \xB7 Up/Down sessions \xB7 n/p snippets \xB7 Enter open \xB7 Esc back`
11096
+ );
11097
+ term.styleReset();
11098
+ term.eraseLineAfter();
11099
+ }
10239
11100
  };
10240
- const renderFromScratch = () => {
10241
- if (mode === "help") {
10242
- renderHelp();
10243
- return;
10244
- }
11101
+ const renderFind = () => {
11102
+ computeFindBoxLayout();
11103
+ const focused = findSubMode === "input";
11104
+ const queryText = findComposer.state().buffer.join("\n");
10245
11105
  withSync(() => {
10246
11106
  term.hideCursor();
10247
- computeLayout();
10248
- adjustScroll();
10249
- startRow = 1;
10250
11107
  term.moveTo(1, 1).eraseDisplayBelow();
10251
- paintComposerTopBorder();
10252
- term("\n");
10253
- for (let v = 0; v < composerRows; v++) {
10254
- paintComposerBodyRow(composerWindowStart + v);
10255
- term("\n");
10256
- }
10257
- paintComposerBottomBorder();
10258
- term("\n\n");
10259
- term.dim.noFormat(` ${headerLine}`)("\n");
10260
- for (let v = 0; v < viewportSize; v++) {
10261
- paintSessionRow(scrollOffset + v);
10262
- term("\n");
11108
+ paintFindBoxTopBorder(focused);
11109
+ for (let v = 0; v < findBoxRows; v++) {
11110
+ term.moveTo(1, 2 + v);
11111
+ paintFindBoxBodyRow(findBoxWindowStart + v, focused);
11112
+ }
11113
+ term.moveTo(1, 2 + findBoxRows);
11114
+ paintFindBoxBottomBorder(focused);
11115
+ const sCount = findResults.length;
11116
+ if (sCount === 0) {
11117
+ term.moveTo(1, findResultsStartRow());
11118
+ if (findInFlight) {
11119
+ } else if (findError === null && queryText.trim().length === 0) {
11120
+ term.dim.noFormat(" type a query in the box above, then press Enter");
11121
+ term.eraseLineAfter();
11122
+ } else if (findError === null) {
11123
+ term.dim.noFormat(" no matches");
11124
+ term.eraseLineAfter();
11125
+ }
11126
+ term.moveTo(1, findResultsStartRow() + 1);
11127
+ paintFindIndicator();
11128
+ } else {
11129
+ adjustFindScroll();
11130
+ const v = findViewportSize();
11131
+ const listFocused = findSubMode !== "input";
11132
+ for (let i = 0; i < v; i++) {
11133
+ const idx = findScrollOffset + i;
11134
+ term.moveTo(1, findResultsStartRow() + i * 2);
11135
+ if (idx < sCount) {
11136
+ paintFindResultA(idx, listFocused && idx === findSelectedIdx);
11137
+ } else {
11138
+ term.eraseLineAfter();
11139
+ }
11140
+ term.moveTo(1, findResultsStartRow() + i * 2 + 1);
11141
+ if (idx < sCount) {
11142
+ paintFindResultB(idx, listFocused && idx === findSelectedIdx);
11143
+ } else {
11144
+ term.eraseLineAfter();
11145
+ }
11146
+ }
11147
+ term.moveTo(1, findResultsStartRow() + v * 2);
11148
+ paintFindIndicator();
10263
11149
  }
10264
- paintIndicator();
10265
- term("\n");
10266
- if (selectedIdx === 0) {
10267
- placeComposerCursor();
11150
+ if (focused) {
11151
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
10268
11152
  term.hideCursor(false);
10269
11153
  }
10270
11154
  });
10271
11155
  };
10272
- const renderHelp = () => {
11156
+ const repaintFindResult = (idx, focused) => {
11157
+ const viewportIdx = idx - findScrollOffset;
11158
+ if (viewportIdx < 0 || viewportIdx >= findViewportSize()) {
11159
+ return;
11160
+ }
10273
11161
  withSync(() => {
10274
- term.hideCursor();
10275
- term.moveTo(1, 1).eraseDisplayBelow();
10276
- term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
10277
- for (const entry of HELP_ENTRIES) {
10278
- if (entry === null) {
10279
- term("\n");
10280
- continue;
11162
+ term.moveTo(1, findResultsStartRow() + viewportIdx * 2);
11163
+ paintFindResultA(idx, focused);
11164
+ term.moveTo(1, findResultsStartRow() + viewportIdx * 2 + 1);
11165
+ paintFindResultB(idx, focused);
11166
+ });
11167
+ };
11168
+ const repaintFindIndicatorRow = () => {
11169
+ withSync(() => {
11170
+ term.moveTo(1, findResultsStartRow() + findViewportSize() * 2);
11171
+ paintFindIndicator();
11172
+ });
11173
+ };
11174
+ const repaintFindViewport = () => {
11175
+ withSync(() => {
11176
+ const v = findViewportSize();
11177
+ const sCount = findResults.length;
11178
+ const listFocused = findSubMode !== "input";
11179
+ for (let i = 0; i < v; i++) {
11180
+ const idx = findScrollOffset + i;
11181
+ term.moveTo(1, findResultsStartRow() + i * 2);
11182
+ if (idx < sCount) {
11183
+ paintFindResultA(idx, listFocused && idx === findSelectedIdx);
11184
+ } else {
11185
+ term.eraseLineAfter();
11186
+ }
11187
+ term.moveTo(1, findResultsStartRow() + i * 2 + 1);
11188
+ if (idx < sCount) {
11189
+ paintFindResultB(idx, listFocused && idx === findSelectedIdx);
11190
+ } else {
11191
+ term.eraseLineAfter();
10281
11192
  }
10282
- const [keys, desc] = entry;
10283
- term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
10284
- term.noFormat(desc)("\n");
10285
11193
  }
10286
- term("\n");
10287
- term.dim.noFormat(" press any key to dismiss")("\n");
11194
+ term.moveTo(1, findResultsStartRow() + v * 2);
11195
+ paintFindIndicator();
10288
11196
  });
10289
11197
  };
11198
+ const findQueryText = () => findComposer.state().buffer.join("\n");
11199
+ const runFind = async () => {
11200
+ const query = findQueryText().trim();
11201
+ if (query.length === 0) {
11202
+ return;
11203
+ }
11204
+ if (visible.length === 0) {
11205
+ findError = "no sessions in view to search";
11206
+ renderFind();
11207
+ return;
11208
+ }
11209
+ findInFlight = true;
11210
+ findError = null;
11211
+ renderFind();
11212
+ try {
11213
+ const out = await searchSessions(opts.target, query, {
11214
+ sessionIds: visible.map((s) => s.sessionId)
11215
+ });
11216
+ findResults = out.results;
11217
+ findTruncated = out.truncated;
11218
+ findSelectedIdx = 0;
11219
+ findSnippetIdx = 0;
11220
+ findScrollOffset = 0;
11221
+ findSubMode = out.results.length > 0 ? "results" : "input";
11222
+ computeFindBoxLayout();
11223
+ } catch (err) {
11224
+ findError = `search failed: ${err.message}`;
11225
+ } finally {
11226
+ findInFlight = false;
11227
+ renderFind();
11228
+ }
11229
+ };
11230
+ let exitFind = () => {
11231
+ };
10290
11232
  const repaintComposerChrome = () => {
10291
11233
  withSync(() => {
10292
11234
  const showCursor = selectedIdx === 0;
@@ -10435,23 +11377,48 @@ async function pickSession(term, opts) {
10435
11377
  let resolved = false;
10436
11378
  let autoRefreshTimer = null;
10437
11379
  let autoRefreshInFlight = false;
10438
- const onResize = () => {
10439
- if (resolved) {
10440
- return;
11380
+ const focusStack = [];
11381
+ const pushLayer = (layer) => {
11382
+ focusStack.push(layer);
11383
+ };
11384
+ const popLayer = () => {
11385
+ focusStack.pop();
11386
+ if (!resolved) {
11387
+ focusStack[focusStack.length - 1]?.onResize();
10441
11388
  }
10442
- renderFromScratch();
11389
+ };
11390
+ const focus = { push: pushLayer, pop: popLayer };
11391
+ exitFind = () => {
11392
+ findComposer = new InputDispatcher({ history: [] });
11393
+ findResults = [];
11394
+ findTruncated = false;
11395
+ findSelectedIdx = 0;
11396
+ findSnippetIdx = 0;
11397
+ findScrollOffset = 0;
11398
+ findError = null;
11399
+ findInFlight = false;
11400
+ findSubMode = "input";
11401
+ popLayer();
11402
+ };
11403
+ const dispatch = (name, _matches, data) => {
11404
+ focusStack[focusStack.length - 1]?.onKey(name, _matches, data);
11405
+ };
11406
+ const dispatchResize = () => {
11407
+ if (resolved) return;
11408
+ focusStack[focusStack.length - 1]?.onResize();
10443
11409
  };
10444
11410
  const cleanup = () => {
10445
11411
  if (resolved) {
10446
11412
  return;
10447
11413
  }
10448
11414
  resolved = true;
11415
+ focusStack.length = 0;
10449
11416
  if (autoRefreshTimer) {
10450
11417
  clearInterval(autoRefreshTimer);
10451
11418
  autoRefreshTimer = null;
10452
11419
  }
10453
- term.off("key", onKey);
10454
- term.off("resize", onResize);
11420
+ term.off("key", dispatch);
11421
+ term.off("resize", dispatchResize);
10455
11422
  process.stdout.write("\x1B[?2004l");
10456
11423
  const tClean = term;
10457
11424
  if (tClean.stdin && tkStdinHandler) {
@@ -10607,18 +11574,231 @@ ${cells}`;
10607
11574
  paintIndicator();
10608
11575
  return true;
10609
11576
  };
10610
- const onKey = (name, _matches, data) => {
10611
- if (mode === "busy") {
11577
+ const openHelpLayer = () => {
11578
+ renderHelp();
11579
+ pushLayer({
11580
+ onKey: (name) => {
11581
+ if (name === "CTRL_C") {
11582
+ cleanup();
11583
+ resolve6({ kind: "abort" });
11584
+ return;
11585
+ }
11586
+ popLayer();
11587
+ },
11588
+ onResize: () => renderHelp()
11589
+ });
11590
+ };
11591
+ const openFindLayer = () => {
11592
+ if (visible.length === 0) {
11593
+ transientStatus = "no sessions to search";
11594
+ paintIndicator();
10612
11595
  return;
10613
11596
  }
10614
- if (mode === "help") {
10615
- if (name === "CTRL_C") {
10616
- cleanup();
10617
- resolve6({ kind: "abort" });
11597
+ findComposer = new InputDispatcher({ history: [] });
11598
+ findResults = [];
11599
+ findTruncated = false;
11600
+ findSelectedIdx = 0;
11601
+ findSnippetIdx = 0;
11602
+ findScrollOffset = 0;
11603
+ findError = null;
11604
+ findInFlight = false;
11605
+ findSubMode = "input";
11606
+ computeFindBoxLayout();
11607
+ renderFind();
11608
+ const findOnKey = (name, _matches, data) => {
11609
+ if (findSubMode === "input") {
11610
+ if (findInFlight) {
11611
+ return;
11612
+ }
11613
+ if (name === "ESCAPE" || name === "CTRL_C") {
11614
+ exitFind();
11615
+ return;
11616
+ }
11617
+ if (name === "ENTER" || name === "KP_ENTER") {
11618
+ if (findQueryText().trim().length === 0) {
11619
+ return;
11620
+ }
11621
+ void runFind();
11622
+ return;
11623
+ }
11624
+ if ((name === "DOWN" || name === "TAB" || name === "CTRL_N") && findResults.length > 0) {
11625
+ findSubMode = "results";
11626
+ findSelectedIdx = 0;
11627
+ findSnippetIdx = 0;
11628
+ withSync(() => {
11629
+ repaintFindBoxChrome();
11630
+ repaintFindResult(0, true);
11631
+ repaintFindIndicatorRow();
11632
+ term.hideCursor();
11633
+ });
11634
+ return;
11635
+ }
11636
+ const before = findComposer.state();
11637
+ let event = null;
11638
+ if (data?.isCharacter) {
11639
+ event = { type: "char", ch: name };
11640
+ } else {
11641
+ const mapped = mapKeyName(name);
11642
+ if (mapped !== null)
11643
+ event = { type: "key", name: mapped };
11644
+ }
11645
+ if (event === null) {
11646
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11647
+ return;
11648
+ }
11649
+ findComposer.feed(event);
11650
+ const after = findComposer.state();
11651
+ const unchanged = before.buffer.length === after.buffer.length && before.buffer.every((l, i) => l === after.buffer[i]) && before.row === after.row && before.col === after.col;
11652
+ if (unchanged) {
11653
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
11654
+ return;
11655
+ }
11656
+ const prevRows = findBoxRows;
11657
+ computeFindBoxLayout();
11658
+ if (findBoxRows !== prevRows) {
11659
+ renderFind();
11660
+ } else {
11661
+ repaintFindBoxBodyRows();
11662
+ }
10618
11663
  return;
10619
11664
  }
10620
- mode = "normal";
10621
- renderFromScratch();
11665
+ if (findSubMode === "results") {
11666
+ if (name === "ESCAPE" || name === "CTRL_C") {
11667
+ exitFind();
11668
+ return;
11669
+ }
11670
+ if (name === "CTRL_F") {
11671
+ findSubMode = "input";
11672
+ repaintFindViewport();
11673
+ repaintFindIndicatorRow();
11674
+ repaintFindBoxChrome();
11675
+ return;
11676
+ }
11677
+ if (name === "ENTER" || name === "KP_ENTER") {
11678
+ const hit = findResults[findSelectedIdx];
11679
+ if (!hit) {
11680
+ return;
11681
+ }
11682
+ const session = visible.find((s) => s.sessionId === hit.sessionId);
11683
+ const isImportedPassive = !!session?.importedFromMachine && !session.upstreamSessionId;
11684
+ if (isImportedPassive) {
11685
+ cleanup();
11686
+ const result = {
11687
+ kind: "attach",
11688
+ sessionId: hit.sessionId
11689
+ };
11690
+ if (session.agentId !== void 0) {
11691
+ result.agentId = session.agentId;
11692
+ }
11693
+ resolve6(result);
11694
+ return;
11695
+ }
11696
+ void (async () => {
11697
+ const action = await promptForLaunchOrView(term, {
11698
+ sessionId: hit.sessionId,
11699
+ title: hit.title,
11700
+ cwd: hit.cwd
11701
+ }, focus);
11702
+ if (action === "cancel") {
11703
+ cleanup();
11704
+ resolve6({ kind: "abort" });
11705
+ return;
11706
+ }
11707
+ if (action === "back") return;
11708
+ cleanup();
11709
+ const result = {
11710
+ kind: "attach",
11711
+ sessionId: hit.sessionId,
11712
+ readonly: action === "view"
11713
+ };
11714
+ if (session?.agentId !== void 0) {
11715
+ result.agentId = session.agentId;
11716
+ }
11717
+ resolve6(result);
11718
+ })();
11719
+ return;
11720
+ }
11721
+ if (data?.isCharacter && (name === "n" || name === "N")) {
11722
+ const hit = findResults[findSelectedIdx];
11723
+ if (!hit || hit.snippets.length <= 1) {
11724
+ return;
11725
+ }
11726
+ findSnippetIdx = (findSnippetIdx + 1) % hit.snippets.length;
11727
+ repaintFindResult(findSelectedIdx, true);
11728
+ return;
11729
+ }
11730
+ if (data?.isCharacter && (name === "p" || name === "P")) {
11731
+ const hit = findResults[findSelectedIdx];
11732
+ if (!hit || hit.snippets.length <= 1) {
11733
+ return;
11734
+ }
11735
+ findSnippetIdx = (findSnippetIdx - 1 + hit.snippets.length) % hit.snippets.length;
11736
+ repaintFindResult(findSelectedIdx, true);
11737
+ return;
11738
+ }
11739
+ const moveDeep = (delta) => {
11740
+ if (delta < 0 && findSelectedIdx === 0) {
11741
+ findSubMode = "input";
11742
+ withSync(() => {
11743
+ repaintFindResult(0, false);
11744
+ repaintFindIndicatorRow();
11745
+ repaintFindBoxChrome();
11746
+ });
11747
+ return;
11748
+ }
11749
+ const next = Math.min(
11750
+ findResults.length - 1,
11751
+ Math.max(0, findSelectedIdx + delta)
11752
+ );
11753
+ if (next === findSelectedIdx) {
11754
+ return;
11755
+ }
11756
+ const oldIdx = findSelectedIdx;
11757
+ const oldScroll = findScrollOffset;
11758
+ findSelectedIdx = next;
11759
+ findSnippetIdx = 0;
11760
+ adjustFindScroll();
11761
+ if (findScrollOffset !== oldScroll) {
11762
+ repaintFindViewport();
11763
+ } else {
11764
+ withSync(() => {
11765
+ repaintFindResult(oldIdx, false);
11766
+ repaintFindResult(findSelectedIdx, true);
11767
+ });
11768
+ repaintFindIndicatorRow();
11769
+ }
11770
+ };
11771
+ switch (name) {
11772
+ case "UP":
11773
+ case "SHIFT_TAB":
11774
+ case "CTRL_P":
11775
+ moveDeep(-1);
11776
+ return;
11777
+ case "DOWN":
11778
+ case "TAB":
11779
+ case "CTRL_N":
11780
+ moveDeep(1);
11781
+ return;
11782
+ case "PAGE_UP":
11783
+ moveDeep(-findViewportSize());
11784
+ return;
11785
+ case "PAGE_DOWN":
11786
+ moveDeep(findViewportSize());
11787
+ return;
11788
+ case "HOME":
11789
+ moveDeep(-findSelectedIdx);
11790
+ return;
11791
+ case "END":
11792
+ moveDeep(findResults.length);
11793
+ return;
11794
+ }
11795
+ return;
11796
+ }
11797
+ };
11798
+ pushLayer({ onKey: findOnKey, onResize: () => renderFind() });
11799
+ };
11800
+ const onKey = (name, _matches, data) => {
11801
+ if (mode === "busy") {
10622
11802
  return;
10623
11803
  }
10624
11804
  if (mode === "rename") {
@@ -10682,6 +11862,10 @@ ${cells}`;
10682
11862
  return;
10683
11863
  }
10684
11864
  clearTransient();
11865
+ if (name === "CTRL_F") {
11866
+ openFindLayer();
11867
+ return;
11868
+ }
10685
11869
  if (selectedIdx === 0 && !searchActive) {
10686
11870
  if (name === "ESCAPE" || name === "CTRL_C" || name === "CTRL_D") {
10687
11871
  cleanup();
@@ -10753,8 +11937,7 @@ ${cells}`;
10753
11937
  return;
10754
11938
  }
10755
11939
  if (!searchActive && data?.isCharacter && name === "?") {
10756
- mode = "help";
10757
- renderHelp();
11940
+ openHelpLayer();
10758
11941
  return;
10759
11942
  }
10760
11943
  if (searchActive) {
@@ -10812,29 +11995,20 @@ ${cells}`;
10812
11995
  }
10813
11996
  if (name === "o" || name === "O") {
10814
11997
  const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
10815
- cwdOnly = !cwdOnly;
11998
+ prefs.filters.cwdOnly = !prefs.filters.cwdOnly;
10816
11999
  applyFilter();
10817
- if (keepId !== void 0) {
10818
- const idx = visible.findIndex((s) => s.sessionId === keepId);
10819
- if (idx >= 0) {
10820
- selectedIdx = idx + 1;
10821
- adjustScroll();
10822
- }
10823
- }
12000
+ restoreCursorAfterFilter(keepId);
10824
12001
  renderFromScratch();
10825
12002
  return;
10826
12003
  }
10827
12004
  if (name === "h" || name === "H") {
10828
12005
  const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
10829
- hostFilter = nextHostFilter(hostFilter, allSessions);
12006
+ prefs.filters.hostFilter = nextHostFilter(
12007
+ prefs.filters.hostFilter,
12008
+ allSessions
12009
+ );
10830
12010
  applyFilter();
10831
- if (keepId !== void 0) {
10832
- const idx = visible.findIndex((s) => s.sessionId === keepId);
10833
- if (idx >= 0) {
10834
- selectedIdx = idx + 1;
10835
- adjustScroll();
10836
- }
10837
- }
12011
+ restoreCursorAfterFilter(keepId);
10838
12012
  renderFromScratch();
10839
12013
  return;
10840
12014
  }
@@ -10971,6 +12145,12 @@ ${cells}`;
10971
12145
  return;
10972
12146
  }
10973
12147
  };
12148
+ pushLayer({
12149
+ onKey: (name, _matches, data) => onKey(name, _matches, data),
12150
+ onResize: () => {
12151
+ if (!resolved) renderFromScratch();
12152
+ }
12153
+ });
10974
12154
  term.grabInput({});
10975
12155
  const tSetup = term;
10976
12156
  if (tSetup.stdin && typeof tSetup.onStdin === "function") {
@@ -10979,10 +12159,10 @@ ${cells}`;
10979
12159
  tSetup.stdin.on("data", rawStdinHandler);
10980
12160
  process.stdout.write("\x1B[?2004h");
10981
12161
  }
10982
- term.on("key", onKey);
10983
- term.on("resize", onResize);
12162
+ term.on("key", dispatch);
12163
+ term.on("resize", dispatchResize);
10984
12164
  autoRefreshTimer = setInterval(() => {
10985
- if (resolved || mode !== "normal" || searchActive || autoRefreshInFlight) {
12165
+ if (resolved || focusStack.length > 1 || mode !== "normal" || searchActive || autoRefreshInFlight) {
10986
12166
  return;
10987
12167
  }
10988
12168
  const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
@@ -10993,10 +12173,10 @@ ${cells}`;
10993
12173
  }, 3e3);
10994
12174
  });
10995
12175
  }
10996
- function readTermHeight(term) {
12176
+ function readTermHeight2(term) {
10997
12177
  return term.height ?? 24;
10998
12178
  }
10999
- function readTermWidth(term) {
12179
+ function readTermWidth2(term) {
11000
12180
  return term.width ?? 80;
11001
12181
  }
11002
12182
  function formatComposerTitle(cwd, maxWidth) {
@@ -11051,7 +12231,7 @@ function matchesSearch(s, term) {
11051
12231
  }
11052
12232
  return false;
11053
12233
  }
11054
- var ROW_PREFIX_WIDTH, PICKER_COMPOSER_MAX_ROWS, BOX_HORIZONTAL_PAD, HELP_KEYS_WIDTH, HELP_ENTRIES;
12234
+ var ROW_PREFIX_WIDTH, PICKER_COMPOSER_MAX_ROWS, FIND_BOX_MAX_ROWS, BOX_HORIZONTAL_PAD, HELP_KEYS_WIDTH, HELP_ENTRIES;
11055
12235
  var init_picker = __esm({
11056
12236
  "src/tui/picker.ts"() {
11057
12237
  "use strict";
@@ -11061,9 +12241,11 @@ var init_picker = __esm({
11061
12241
  init_discovery();
11062
12242
  init_input();
11063
12243
  init_screen();
12244
+ init_import_action_prompt();
11064
12245
  init_sync();
11065
12246
  ROW_PREFIX_WIDTH = 2;
11066
12247
  PICKER_COMPOSER_MAX_ROWS = 4;
12248
+ FIND_BOX_MAX_ROWS = 4;
11067
12249
  BOX_HORIZONTAL_PAD = 4;
11068
12250
  HELP_KEYS_WIDTH = 20;
11069
12251
  HELP_ENTRIES = [
@@ -11076,7 +12258,8 @@ var init_picker = __esm({
11076
12258
  ["Enter", "open selected session"],
11077
12259
  ["v", "view-only (open transcript without spawning the agent)"],
11078
12260
  null,
11079
- ["/", "search sessions"],
12261
+ ["/", "search sessions (metadata)"],
12262
+ ["^f", "find in session history (content + tool inputs)"],
11080
12263
  ["o", "toggle cwd-only filter"],
11081
12264
  ["h", "cycle host filter (local / <peer> / all)"],
11082
12265
  ["r", "refresh from daemon"],
@@ -11109,103 +12292,13 @@ async function validateLocalCwd(input) {
11109
12292
  }
11110
12293
  if (!stat5.isDirectory()) {
11111
12294
  return { ok: false, reason: `${resolved} is not a directory` };
11112
- }
11113
- return { ok: true, path: resolved };
11114
- }
11115
- var init_cwd = __esm({
11116
- "src/core/cwd.ts"() {
11117
- "use strict";
11118
- init_config();
11119
- }
11120
- });
11121
-
11122
- // src/tui/prompt-utils.ts
11123
- function resetTerminalModes() {
11124
- process.stdout.write("\x1B[<u");
11125
- process.stdout.write("\x1B[?2004l");
11126
- process.stdout.write("\x1B[>4;0m");
11127
- process.stdout.write("\x1B[>5;0m");
11128
- process.stdout.write("\x1B[?1000l");
11129
- process.stdout.write("\x1B[?1002l");
11130
- process.stdout.write("\x1B[?1006l");
11131
- process.stdout.write("\x1B[?1l");
11132
- process.stdout.write("\x1B>");
11133
- }
11134
- function readTermWidth2(term) {
11135
- return term.width ?? 80;
11136
- }
11137
- function readTermHeight2(term) {
11138
- return term.height ?? 24;
11139
- }
11140
- function drawBox(term, opts) {
11141
- const termW = readTermWidth2(term);
11142
- const termH = readTermHeight2(term);
11143
- const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
11144
- const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
11145
- const contentW = Math.min(desiredContentW, maxContentW);
11146
- const w = contentW + 2;
11147
- const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
11148
- const h = contentH + 2;
11149
- const x = Math.max(1, Math.floor((termW - w) / 2) + 1);
11150
- const y = Math.max(1, Math.floor((termH - h) / 2) + 1);
11151
- term.moveTo(1, 1).eraseDisplayBelow();
11152
- const topInner = HORIZ.repeat(w - 2);
11153
- const top = renderTitleStrip(topInner, opts.title);
11154
- term.moveTo(x, y);
11155
- term.dim.noFormat(TL);
11156
- paintTopStrip(term, top);
11157
- term.dim.noFormat(TR);
11158
- for (let row = 1; row <= contentH; row++) {
11159
- term.moveTo(x, y + row);
11160
- term.dim.noFormat(VERT);
11161
- term.moveTo(x + w - 1, y + row);
11162
- term.dim.noFormat(VERT);
11163
- }
11164
- term.moveTo(x, y + h - 1);
11165
- term.dim.noFormat(BL + HORIZ.repeat(w - 2) + BR);
11166
- return {
11167
- x,
11168
- y,
11169
- w,
11170
- h,
11171
- contentX: x + 1,
11172
- contentY: y + 1,
11173
- contentW,
11174
- contentH
11175
- };
11176
- }
11177
- function renderTitleStrip(innerDashes, title) {
11178
- if (!title) {
11179
- return { dashes: innerDashes };
11180
- }
11181
- const chip = ` ${title} `;
11182
- if (chip.length + 4 > innerDashes.length) {
11183
- return { dashes: innerDashes };
11184
- }
11185
- const offset = 2;
11186
- const dashes = innerDashes.slice(0, offset) + " ".repeat(chip.length) + innerDashes.slice(offset + chip.length);
11187
- return { dashes, title: { offset, text: chip } };
11188
- }
11189
- function paintTopStrip(term, strip) {
11190
- if (!strip.title) {
11191
- term.dim.noFormat(strip.dashes);
11192
- return;
11193
- }
11194
- term.dim.noFormat(strip.dashes.slice(0, strip.title.offset));
11195
- term.brightCyan.noFormat(strip.title.text);
11196
- term.dim.noFormat(strip.dashes.slice(strip.title.offset + strip.title.text.length));
12295
+ }
12296
+ return { ok: true, path: resolved };
11197
12297
  }
11198
- var MAX_BOX_WIDTH, HORIZ, VERT, TL, TR, BL, BR;
11199
- var init_prompt_utils = __esm({
11200
- "src/tui/prompt-utils.ts"() {
12298
+ var init_cwd = __esm({
12299
+ "src/core/cwd.ts"() {
11201
12300
  "use strict";
11202
- MAX_BOX_WIDTH = 64;
11203
- HORIZ = "\u2500";
11204
- VERT = "\u2502";
11205
- TL = "\u250C";
11206
- TR = "\u2510";
11207
- BL = "\u2514";
11208
- BR = "\u2518";
12301
+ init_config();
11209
12302
  }
11210
12303
  });
11211
12304
 
@@ -11225,7 +12318,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11225
12318
  const contentHeight = 9;
11226
12319
  layout = drawBox(term, {
11227
12320
  contentHeight,
11228
- title: "Run locally \u2014 choose cwd"
12321
+ title: "Fork locally \u2014 choose cwd"
11229
12322
  });
11230
12323
  const innerW = layout.contentW;
11231
12324
  const headerRows = [
@@ -11237,7 +12330,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11237
12330
  for (const hr of headerRows) {
11238
12331
  term.moveTo(layout.contentX, layout.contentY + row);
11239
12332
  term.dim.noFormat(` ${hr.label}`);
11240
- term.noFormat(truncate2(hr.value, innerW - hr.label.length - 2));
12333
+ term.noFormat(truncate3(hr.value, innerW - hr.label.length - 2));
11241
12334
  row++;
11242
12335
  }
11243
12336
  row++;
@@ -11248,7 +12341,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11248
12341
  row += 2;
11249
12342
  if (errorLine !== null) {
11250
12343
  term.moveTo(layout.contentX, layout.contentY + row);
11251
- term.red.noFormat(` ${truncate2(errorLine, innerW - 2)}`);
12344
+ term.red.noFormat(` ${truncate3(errorLine, innerW - 2)}`);
11252
12345
  } else {
11253
12346
  term.moveTo(layout.contentX, layout.contentY + row);
11254
12347
  term.dim.noFormat(
@@ -11284,7 +12377,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11284
12377
  term.dim.noFormat("\u2502");
11285
12378
  term.moveTo(layout.contentX, layout.contentY + errRow);
11286
12379
  if (errorLine !== null) {
11287
- term.red.noFormat(` ${truncate2(errorLine, layout.contentW - 2)}`);
12380
+ term.red.noFormat(` ${truncate3(errorLine, layout.contentW - 2)}`);
11288
12381
  } else {
11289
12382
  term.dim.noFormat(
11290
12383
  " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
@@ -11380,7 +12473,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11380
12473
  term.on("resize", onResize);
11381
12474
  });
11382
12475
  }
11383
- function truncate2(s, max) {
12476
+ function truncate3(s, max) {
11384
12477
  if (max <= 1) {
11385
12478
  return "";
11386
12479
  }
@@ -11408,223 +12501,6 @@ var init_import_cwd_prompt = __esm({
11408
12501
  }
11409
12502
  });
11410
12503
 
11411
- // src/tui/import-action-prompt.ts
11412
- function actionPromptStep(selected, key, choices = ACTION_CHOICES) {
11413
- if (key.kind === "cancel") {
11414
- return { kind: "cancel" };
11415
- }
11416
- if (key.kind === "back") {
11417
- return { kind: "back" };
11418
- }
11419
- if (key.kind === "enter") {
11420
- const choice = choices[selected];
11421
- if (!choice) {
11422
- return { kind: "back" };
11423
- }
11424
- return { kind: "resolve", action: choice.key };
11425
- }
11426
- if (key.kind === "up") {
11427
- return {
11428
- kind: "continue",
11429
- selected: Math.max(0, selected - 1)
11430
- };
11431
- }
11432
- if (key.kind === "down") {
11433
- return {
11434
- kind: "continue",
11435
- selected: Math.min(choices.length - 1, selected + 1)
11436
- };
11437
- }
11438
- if (key.kind === "char") {
11439
- const lower = key.ch.toLowerCase();
11440
- if (lower === "n") {
11441
- return {
11442
- kind: "continue",
11443
- selected: Math.min(choices.length - 1, selected + 1)
11444
- };
11445
- }
11446
- if (lower === "p") {
11447
- return {
11448
- kind: "continue",
11449
- selected: Math.max(0, selected - 1)
11450
- };
11451
- }
11452
- const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
11453
- if (idx >= 0) {
11454
- const choice = choices[idx];
11455
- if (choice) {
11456
- return { kind: "resolve", action: choice.key };
11457
- }
11458
- }
11459
- }
11460
- return { kind: "continue", selected };
11461
- }
11462
- async function promptForImportAction(term, session) {
11463
- resetTerminalModes();
11464
- const shortId2 = stripHydraSessionPrefix(session.sessionId);
11465
- const fromMachine = session.importedFromMachine ?? "another machine";
11466
- const originalCwd = shortenHomePath(session.cwd);
11467
- let selected = 0;
11468
- const render = () => {
11469
- const choiceRows = ACTION_CHOICES.length * 2;
11470
- const contentHeight = 7 + choiceRows + 2;
11471
- const layout = drawBox(term, {
11472
- contentHeight,
11473
- title: "Imported session"
11474
- });
11475
- const innerW = layout.contentW;
11476
- const headerRows = [
11477
- { label: "session: ", value: shortId2 },
11478
- { label: "from: ", value: fromMachine },
11479
- { label: "cwd: ", value: originalCwd }
11480
- ];
11481
- let row = 0;
11482
- for (const hr of headerRows) {
11483
- term.moveTo(layout.contentX, layout.contentY + row);
11484
- term.dim.noFormat(` ${hr.label}`);
11485
- term.noFormat(truncate3(hr.value, innerW - hr.label.length - 2));
11486
- row++;
11487
- }
11488
- row++;
11489
- term.moveTo(layout.contentX, layout.contentY + row);
11490
- term.noFormat(" What do you want to do?");
11491
- row += 2;
11492
- for (let i = 0; i < ACTION_CHOICES.length; i++) {
11493
- const choice = ACTION_CHOICES[i];
11494
- if (!choice) {
11495
- continue;
11496
- }
11497
- const pointer = i === selected ? "\u276F" : " ";
11498
- const label = ` ${pointer} ${choice.label}`;
11499
- term.moveTo(layout.contentX, layout.contentY + row);
11500
- if (i === selected) {
11501
- term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
11502
- } else {
11503
- term.noFormat(label);
11504
- }
11505
- row++;
11506
- term.moveTo(layout.contentX, layout.contentY + row);
11507
- term.dim.noFormat(` ${choice.description}`);
11508
- row++;
11509
- }
11510
- row++;
11511
- term.moveTo(layout.contentX, layout.contentY + row);
11512
- term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 r/v jump \xB7 Esc back");
11513
- return layout;
11514
- };
11515
- render();
11516
- term.hideCursor();
11517
- return await new Promise((resolve6) => {
11518
- let resolved = false;
11519
- const cleanup = () => {
11520
- if (resolved) {
11521
- return;
11522
- }
11523
- resolved = true;
11524
- term.off("key", onKey);
11525
- term.off("resize", onResize);
11526
- term.grabInput(false);
11527
- term.hideCursor(false);
11528
- term.moveTo(1, 1).eraseDisplayBelow();
11529
- };
11530
- const finish = (value) => {
11531
- cleanup();
11532
- resolve6(value);
11533
- };
11534
- const onResize = () => {
11535
- if (resolved) {
11536
- return;
11537
- }
11538
- render();
11539
- };
11540
- const onKey = (name, _matches, data) => {
11541
- const input = mapKey(name, data);
11542
- if (!input) {
11543
- return;
11544
- }
11545
- const step = actionPromptStep(selected, input);
11546
- if (step.kind === "cancel") {
11547
- finish("cancel");
11548
- return;
11549
- }
11550
- if (step.kind === "back") {
11551
- finish("back");
11552
- return;
11553
- }
11554
- if (step.kind === "resolve") {
11555
- finish(step.action);
11556
- return;
11557
- }
11558
- if (step.selected !== selected) {
11559
- selected = step.selected;
11560
- render();
11561
- }
11562
- };
11563
- term.grabInput({});
11564
- term.on("key", onKey);
11565
- term.on("resize", onResize);
11566
- });
11567
- }
11568
- function mapKey(name, data) {
11569
- if (name === "UP") {
11570
- return { kind: "up" };
11571
- }
11572
- if (name === "DOWN") {
11573
- return { kind: "down" };
11574
- }
11575
- if (name === "ENTER" || name === "KP_ENTER") {
11576
- return { kind: "enter" };
11577
- }
11578
- if (name === "ESCAPE") {
11579
- return { kind: "back" };
11580
- }
11581
- if (name === "CTRL_C" || name === "CTRL_D") {
11582
- return { kind: "cancel" };
11583
- }
11584
- if (data?.isCharacter) {
11585
- return { kind: "char", ch: name };
11586
- }
11587
- return null;
11588
- }
11589
- function truncate3(s, max) {
11590
- if (max <= 1) {
11591
- return "";
11592
- }
11593
- if (s.length <= max) {
11594
- return s;
11595
- }
11596
- return s.slice(0, Math.max(0, max - 1)) + "\u2026";
11597
- }
11598
- function padRight(s, w) {
11599
- if (s.length >= w) {
11600
- return s.slice(0, w);
11601
- }
11602
- return s + " ".repeat(w - s.length);
11603
- }
11604
- var ACTION_CHOICES;
11605
- var init_import_action_prompt = __esm({
11606
- "src/tui/import-action-prompt.ts"() {
11607
- "use strict";
11608
- init_paths();
11609
- init_session();
11610
- init_prompt_utils();
11611
- ACTION_CHOICES = [
11612
- {
11613
- key: "run-local",
11614
- label: "Run locally",
11615
- hotkey: "r",
11616
- description: "spawn the agent on this machine with a local cwd"
11617
- },
11618
- {
11619
- key: "view",
11620
- label: "View transcript",
11621
- hotkey: "v",
11622
- description: "open read-only, no agent spawn"
11623
- }
11624
- ];
11625
- }
11626
- });
11627
-
11628
12504
  // src/tui/clipboard.ts
11629
12505
  import { spawn as nodeSpawn } from "child_process";
11630
12506
  import fs21 from "fs/promises";
@@ -12023,41 +12899,69 @@ function formatEvent(event) {
12023
12899
  return [];
12024
12900
  }
12025
12901
  }
12026
- function applyInlineMarkup(text) {
12902
+ function applyInlineMarkup(text, opts) {
12903
+ const codeOpen = opts?.codeOpen ?? "^C";
12904
+ const boldReset = opts?.boldReset ?? "^:";
12905
+ const codeReset = opts?.codeReset ?? "^:";
12027
12906
  let s = text.replace(/\^/g, "^^");
12028
- s = s.replace(/\*\*(.+?)\*\*/g, "^+$1^:");
12029
- s = s.replace(/`([^`]+)`/g, "^C$1^:");
12907
+ s = s.replace(/\*\*(.+?)\*\*/g, `^+$1${boldReset}`);
12908
+ s = s.replace(/`([^`]+)`/g, `${codeOpen}$1${codeReset}`);
12030
12909
  return s;
12031
12910
  }
12032
- function parseAgentMarkdown(text) {
12911
+ function parseMarkdown(text, opts) {
12912
+ const {
12913
+ proseStyle,
12914
+ highlightCode,
12915
+ prefixStyle,
12916
+ firstPrefix = " ",
12917
+ inlineOpts
12918
+ } = opts;
12033
12919
  const out = [];
12034
12920
  const lines = text.split("\n");
12035
12921
  let inCode = false;
12036
12922
  let codeLang = "";
12037
12923
  let codeBuffer = [];
12924
+ let firstNonBlank = firstPrefix !== " ";
12925
+ const line = (body, bodyStyle, prefix = " ") => {
12926
+ const entry = { prefix, body, bodyStyle };
12927
+ if (prefixStyle !== void 0)
12928
+ entry.prefixStyle = prefixStyle;
12929
+ out.push(entry);
12930
+ };
12931
+ const nextPrefix = () => {
12932
+ if (!firstNonBlank)
12933
+ return " ";
12934
+ firstNonBlank = false;
12935
+ return firstPrefix;
12936
+ };
12038
12937
  const flushCode = () => {
12039
- if (codeBuffer.length === 0) {
12938
+ if (codeBuffer.length === 0)
12040
12939
  return;
12041
- }
12042
- const highlighted = highlightFencedBlock(codeLang, codeBuffer);
12043
- for (const piece of highlighted) {
12044
- const entry = {
12045
- prefix: " ",
12046
- body: piece.body,
12047
- bodyStyle: "code",
12048
- fillRow: true
12049
- };
12050
- if (piece.ansi) {
12051
- entry.ansi = true;
12940
+ if (highlightCode) {
12941
+ const highlighted = highlightFencedBlock(codeLang, codeBuffer);
12942
+ for (const piece of highlighted) {
12943
+ const entry = {
12944
+ prefix: " ",
12945
+ body: piece.body,
12946
+ bodyStyle: "code",
12947
+ fillRow: true
12948
+ };
12949
+ if (prefixStyle !== void 0)
12950
+ entry.prefixStyle = prefixStyle;
12951
+ if (piece.ansi)
12952
+ entry.ansi = true;
12953
+ out.push(entry);
12052
12954
  }
12053
- out.push(entry);
12955
+ } else {
12956
+ for (const cl of codeBuffer)
12957
+ line(cl.replace(/\^/g, "^^"), proseStyle);
12054
12958
  }
12055
12959
  codeBuffer = [];
12056
12960
  codeLang = "";
12057
12961
  };
12058
12962
  for (let i = 0; i < lines.length; i++) {
12059
- const line = lines[i];
12060
- const fence = line.match(/^\s*```\s*(\w*)\s*$/);
12963
+ const l = lines[i];
12964
+ const fence = l.match(/^\s*```\s*(\w*)\s*$/);
12061
12965
  if (fence) {
12062
12966
  if (!inCode) {
12063
12967
  inCode = true;
@@ -12069,68 +12973,81 @@ function parseAgentMarkdown(text) {
12069
12973
  continue;
12070
12974
  }
12071
12975
  if (inCode) {
12072
- codeBuffer.push(line);
12976
+ codeBuffer.push(l);
12073
12977
  continue;
12074
12978
  }
12075
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
12979
+ const heading = l.match(/^(#{1,6})\s+(.*)$/);
12076
12980
  if (heading) {
12077
12981
  const level = heading[1].length;
12078
- const text2 = heading[2] ?? "";
12079
- const style = level === 1 ? "heading-1" : level === 2 ? "heading-2" : "heading-3";
12080
- out.push({
12081
- prefix: " ",
12082
- body: text2,
12083
- bodyStyle: style
12084
- });
12982
+ const headingText = heading[2] ?? "";
12983
+ const headingStyle = highlightCode ? level === 1 ? "heading-1" : level === 2 ? "heading-2" : "heading-3" : proseStyle;
12984
+ line(headingText, headingStyle, nextPrefix());
12085
12985
  continue;
12086
12986
  }
12087
12987
  const next = lines[i + 1];
12088
- if (line.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(line).length === parseTableRow(next).length) {
12089
- const header = parseTableRow(line);
12988
+ if (l.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(l).length === parseTableRow(next).length) {
12989
+ const header = parseTableRow(l);
12090
12990
  const body = [];
12091
12991
  let j = i + 2;
12092
12992
  while (j < lines.length && lines[j].includes("|")) {
12093
12993
  body.push(parseTableRow(lines[j]));
12094
12994
  j++;
12095
12995
  }
12096
- out.push(...formatTable(header, body));
12996
+ const tableLines = formatTable(header, body);
12997
+ for (const tl of tableLines) {
12998
+ if (prefixStyle !== void 0)
12999
+ tl.prefixStyle = prefixStyle;
13000
+ out.push(tl);
13001
+ }
12097
13002
  i = j - 1;
12098
13003
  continue;
12099
13004
  }
12100
- const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
13005
+ const bullet = l.match(/^(\s*)[-*+]\s+(.*)$/);
12101
13006
  if (bullet) {
12102
13007
  const indent = bullet[1] ?? "";
12103
13008
  const item = bullet[2] ?? "";
12104
- out.push({
12105
- prefix: " ",
12106
- body: `${indent}\u2022 ${applyInlineMarkup(item)}`,
12107
- bodyStyle: "agent"
12108
- });
13009
+ line(
13010
+ `${indent}\u2022 ${applyInlineMarkup(item, inlineOpts)}`,
13011
+ proseStyle,
13012
+ nextPrefix()
13013
+ );
12109
13014
  continue;
12110
13015
  }
12111
- const ordered = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
13016
+ const ordered = l.match(/^(\s*)(\d+)\.\s+(.*)$/);
12112
13017
  if (ordered) {
12113
13018
  const indent = ordered[1] ?? "";
12114
13019
  const num = ordered[2] ?? "";
12115
- const item = ordered[3] ?? "";
12116
- out.push({
12117
- prefix: " ",
12118
- body: `${indent}${num}. ${applyInlineMarkup(item)}`,
12119
- bodyStyle: "agent"
12120
- });
13020
+ const item = ordered[3] ?? "";
13021
+ line(
13022
+ `${indent}${num}. ${applyInlineMarkup(item, inlineOpts)}`,
13023
+ proseStyle,
13024
+ nextPrefix()
13025
+ );
12121
13026
  continue;
12122
13027
  }
12123
- out.push({
12124
- prefix: " ",
12125
- body: applyInlineMarkup(line),
12126
- bodyStyle: "agent"
12127
- });
13028
+ const isBlank = l.trim() === "";
13029
+ line(
13030
+ applyInlineMarkup(l, inlineOpts),
13031
+ proseStyle,
13032
+ isBlank ? " " : nextPrefix()
13033
+ );
12128
13034
  }
12129
- if (inCode) {
13035
+ if (inCode)
12130
13036
  flushCode();
12131
- }
12132
13037
  return out;
12133
13038
  }
13039
+ function parseAgentMarkdown(text) {
13040
+ return parseMarkdown(text, { proseStyle: "agent", highlightCode: true });
13041
+ }
13042
+ function parseThoughtMarkdown(text) {
13043
+ return parseMarkdown(text, {
13044
+ proseStyle: "thought",
13045
+ highlightCode: false,
13046
+ prefixStyle: "thought",
13047
+ firstPrefix: "\xB7 ",
13048
+ inlineOpts: { codeOpen: "^c", boldReset: "^-", codeReset: "^K" }
13049
+ });
13050
+ }
12134
13051
  function parseTableRow(line) {
12135
13052
  let s = line.trim();
12136
13053
  if (s.startsWith("|")) {
@@ -12487,6 +13404,7 @@ async function runTuiApp(opts) {
12487
13404
  const viewPrefs = {
12488
13405
  showThoughts: config.tui.showThoughts
12489
13406
  };
13407
+ const pickerPrefs = createPickerPrefs();
12490
13408
  let altScreenEngaged = false;
12491
13409
  const enterAltScreen = () => {
12492
13410
  if (altScreenEngaged) {
@@ -12514,7 +13432,15 @@ async function runTuiApp(opts) {
12514
13432
  let nextOpts = opts;
12515
13433
  try {
12516
13434
  while (nextOpts !== null) {
12517
- nextOpts = await runSession(term, config, target, nextOpts, exitHint, viewPrefs);
13435
+ nextOpts = await runSession(
13436
+ term,
13437
+ config,
13438
+ target,
13439
+ nextOpts,
13440
+ exitHint,
13441
+ viewPrefs,
13442
+ pickerPrefs
13443
+ );
12518
13444
  }
12519
13445
  } finally {
12520
13446
  leaveAltScreen();
@@ -12534,8 +13460,8 @@ async function runTuiApp(opts) {
12534
13460
  );
12535
13461
  }
12536
13462
  }
12537
- async function runSession(term, config, target, opts, exitHint, viewPrefs) {
12538
- const ctx = await resolveSession(term, config, target, opts);
13463
+ async function runSession(term, config, target, opts, exitHint, viewPrefs, pickerPrefs) {
13464
+ const ctx = await resolveSession(term, config, target, opts, pickerPrefs);
12539
13465
  if (!ctx) {
12540
13466
  term.grabInput(false);
12541
13467
  return null;
@@ -13454,7 +14380,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
13454
14380
  sessions,
13455
14381
  config,
13456
14382
  target,
13457
- currentSessionId: resolvedSessionId
14383
+ currentSessionId: resolvedSessionId,
14384
+ prefs: pickerPrefs
13458
14385
  });
13459
14386
  if (choice2.kind === "abort") {
13460
14387
  screen.start({ skipFullscreen: true });
@@ -14147,6 +15074,33 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
14147
15074
  agentKey = null;
14148
15075
  agentBuffer = "";
14149
15076
  };
15077
+ let thoughtBuffer = "";
15078
+ let thoughtKey = null;
15079
+ let thoughtSeq = 0;
15080
+ const renderThoughtBlock = () => {
15081
+ if (thoughtKey === null)
15082
+ return;
15083
+ const lines = parseThoughtMarkdown(thoughtBuffer);
15084
+ if (lines.length === 0)
15085
+ return;
15086
+ screen.upsertLines(thoughtKey, lines);
15087
+ };
15088
+ const appendThought = (text) => {
15089
+ if (text.length === 0)
15090
+ return;
15091
+ if (thoughtKey === null) {
15092
+ screen.ensureSeparator();
15093
+ thoughtKey = `thought:${thoughtSeq}`;
15094
+ thoughtSeq += 1;
15095
+ thoughtBuffer = "";
15096
+ }
15097
+ thoughtBuffer += text;
15098
+ renderThoughtBlock();
15099
+ };
15100
+ const closeThought = () => {
15101
+ thoughtKey = null;
15102
+ thoughtBuffer = "";
15103
+ };
14150
15104
  const renderToolsBlock = () => {
14151
15105
  if (toolsBlockStartedAt === null) {
14152
15106
  return;
@@ -14288,6 +15242,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
14288
15242
  recordHistoryEntry(event.text);
14289
15243
  }
14290
15244
  closeAgentText();
15245
+ closeThought();
14291
15246
  if (toolsBlockStartedAt !== null) {
14292
15247
  toolsBlockEndedAt = Date.now();
14293
15248
  renderToolsBlock();
@@ -14311,16 +15266,18 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
14311
15266
  return;
14312
15267
  }
14313
15268
  if (event.kind === "agent-text") {
15269
+ closeThought();
14314
15270
  appendAgentText(event.text);
14315
15271
  return;
14316
15272
  }
14317
15273
  if (event.kind === "agent-thought") {
14318
15274
  closeAgentText();
14319
- screen.appendStreaming(event.text, "\xB7 ", "thought", "thought");
15275
+ appendThought(event.text);
14320
15276
  return;
14321
15277
  }
14322
15278
  if (event.kind === "exit-plan-mode") {
14323
15279
  closeAgentText();
15280
+ closeThought();
14324
15281
  const existing = exitPlanStates.get(event.toolCallId);
14325
15282
  const merged = {
14326
15283
  plan: event.plan ?? existing?.plan ?? "",
@@ -14338,12 +15295,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
14338
15295
  }
14339
15296
  if (event.kind === "tool-call") {
14340
15297
  closeAgentText();
15298
+ closeThought();
14341
15299
  recordToolCall(event.toolCallId, event.title, event.status, void 0);
14342
15300
  renderToolsBlock();
14343
15301
  return;
14344
15302
  }
14345
15303
  if (event.kind === "plan") {
14346
15304
  closeAgentText();
15305
+ closeThought();
14347
15306
  lastPlanEvent = event;
14348
15307
  const lines = formatEvent(event);
14349
15308
  if (lines.length > 0) {
@@ -14353,6 +15312,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
14353
15312
  }
14354
15313
  if (event.kind === "tool-call-update") {
14355
15314
  closeAgentText();
15315
+ closeThought();
14356
15316
  recordToolCall(
14357
15317
  event.toolCallId,
14358
15318
  event.title,
@@ -14375,6 +15335,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
14375
15335
  if (event.kind === "turn-complete") {
14376
15336
  currentHeadMessageId = void 0;
14377
15337
  closeAgentText();
15338
+ closeThought();
14378
15339
  let effectiveStopReason = event.amended ? "amended" : event.stopReason;
14379
15340
  if (!event.amended && upstreamInterruptedSeen && (effectiveStopReason === void 0 || effectiveStopReason === "end_turn")) {
14380
15341
  effectiveStopReason = "error";
@@ -14469,6 +15430,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
14469
15430
  resolve6({ outcome: { outcome: "cancelled" } });
14470
15431
  }
14471
15432
  closeAgentText();
15433
+ closeThought();
14472
15434
  };
14473
15435
  const markToolsBlockRecoveryFailed = () => {
14474
15436
  if (toolsBlockStartedAt === null) {
@@ -14596,7 +15558,7 @@ connection lost: ${err.message}
14596
15558
  }
14597
15559
  return await sessionDone;
14598
15560
  }
14599
- async function resolveSession(term, config, target, opts) {
15561
+ async function resolveSession(term, config, target, opts, pickerPrefs) {
14600
15562
  const cwd = opts.cwd ?? process.cwd();
14601
15563
  if (opts.sessionId) {
14602
15564
  const ctx = {
@@ -14632,7 +15594,8 @@ async function resolveSession(term, config, target, opts) {
14632
15594
  cwd,
14633
15595
  sessions,
14634
15596
  config,
14635
- target
15597
+ target,
15598
+ prefs: pickerPrefs
14636
15599
  });
14637
15600
  if (choice.kind === "abort") {
14638
15601
  return null;
@@ -14916,6 +15879,9 @@ import { dirname as dirname6, resolve as resolve5 } from "path";
14916
15879
  var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
14917
15880
  "all",
14918
15881
  "detach",
15882
+ "disabled",
15883
+ "follow",
15884
+ "force",
14919
15885
  "foreground",
14920
15886
  "help",
14921
15887
  "info",
@@ -14928,6 +15894,31 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
14928
15894
  "stream",
14929
15895
  "version"
14930
15896
  ]);
15897
+ var KNOWN_VALUE_FLAGS = /* @__PURE__ */ new Set([
15898
+ "agent",
15899
+ "args",
15900
+ "command",
15901
+ "cwd",
15902
+ "env",
15903
+ "host",
15904
+ "model",
15905
+ "name",
15906
+ "out",
15907
+ "prompt",
15908
+ "session",
15909
+ "stream-bytes",
15910
+ "stream-file-cap",
15911
+ "stream-threshold",
15912
+ "tail"
15913
+ ]);
15914
+ function validateKnownFlags(flags) {
15915
+ for (const key of Object.keys(flags)) {
15916
+ if (!KNOWN_BOOLEAN_FLAGS.has(key) && !KNOWN_VALUE_FLAGS.has(key)) {
15917
+ return key;
15918
+ }
15919
+ }
15920
+ return void 0;
15921
+ }
14931
15922
  function parseArgs(argv) {
14932
15923
  const positional = [];
14933
15924
  const flags = {};
@@ -16430,6 +17421,7 @@ var SessionManager = class {
16430
17421
  this.defaultTransformers = options.defaultTransformers ?? [];
16431
17422
  this.logger = options.logger;
16432
17423
  this.npmRegistry = options.npmRegistry;
17424
+ this.extensionCommands = options.extensionCommands;
16433
17425
  }
16434
17426
  registry;
16435
17427
  sessions = /* @__PURE__ */ new Map();
@@ -16448,6 +17440,7 @@ var SessionManager = class {
16448
17440
  metaWriteQueues = /* @__PURE__ */ new Map();
16449
17441
  logger;
16450
17442
  npmRegistry;
17443
+ extensionCommands;
16451
17444
  async create(params) {
16452
17445
  const fresh = await this.bootstrapAgent({
16453
17446
  agentId: params.agentId,
@@ -16501,7 +17494,8 @@ var SessionManager = class {
16501
17494
  agentModes: fresh.initialModes,
16502
17495
  agentModels: fresh.initialModels,
16503
17496
  transformChain: params.transformChain,
16504
- parentSessionId: params.parentSessionId
17497
+ parentSessionId: params.parentSessionId,
17498
+ extensionCommands: this.extensionCommands
16505
17499
  });
16506
17500
  await this.attachManagerHooks(session);
16507
17501
  return session;
@@ -16572,12 +17566,14 @@ var SessionManager = class {
16572
17566
  }
16573
17567
  let loadResult;
16574
17568
  try {
17569
+ const loadMeta = buildSessionLoadMeta(params.agentId, params.currentModel);
16575
17570
  loadResult = await agent.connection.request(
16576
17571
  "session/load",
16577
17572
  {
16578
17573
  sessionId: params.upstreamSessionId,
16579
17574
  cwd: params.cwd,
16580
- mcpServers: []
17575
+ mcpServers: [],
17576
+ ...loadMeta && { _meta: loadMeta }
16581
17577
  }
16582
17578
  );
16583
17579
  } catch (err) {
@@ -16593,7 +17589,10 @@ var SessionManager = class {
16593
17589
  () => void 0
16594
17590
  );
16595
17591
  } else {
16596
- agent.connection.drainBuffered("session/update");
17592
+ const drain1Count = agent.connection.drainBuffered("session/update");
17593
+ this.logger?.info(
17594
+ `resurrect: drain1 dropped ${drain1Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
17595
+ );
16597
17596
  }
16598
17597
  const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
16599
17598
  const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
@@ -16611,6 +17610,30 @@ var SessionManager = class {
16611
17610
  this.logger?.info(
16612
17611
  `resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
16613
17612
  );
17613
+ const agentReportedModel = extractInitialModel(loadResult ?? {});
17614
+ const advertisedModels = nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels;
17615
+ this.logger?.info(
17616
+ `resurrect: sessionId=${params.hydraSessionId} persistedModel=${JSON.stringify(params.currentModel)} agentReportedModel=${JSON.stringify(agentReportedModel)} advertisedModels=${JSON.stringify(advertisedModels?.map((m) => m.modelId))}`
17617
+ );
17618
+ if (params.pendingHistorySync !== true) {
17619
+ const drain2Count = agent.connection.drainBuffered("session/update");
17620
+ this.logger?.info(
17621
+ `resurrect: drain2 (post-mode-restore) dropped ${drain2Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
17622
+ );
17623
+ }
17624
+ const effectiveModel = await restoreCurrentModel({
17625
+ agent,
17626
+ upstreamSessionId: params.upstreamSessionId,
17627
+ persistedModel: params.currentModel,
17628
+ agentReportedModel,
17629
+ logger: this.logger
17630
+ });
17631
+ if (params.pendingHistorySync !== true) {
17632
+ const drain3Count = agent.connection.drainBuffered("session/update");
17633
+ this.logger?.info(
17634
+ `resurrect: drain3 (post-model-restore) dropped ${drain3Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
17635
+ );
17636
+ }
16614
17637
  const session = new Session({
16615
17638
  sessionId: params.hydraSessionId,
16616
17639
  cwd: params.cwd,
@@ -16627,11 +17650,7 @@ var SessionManager = class {
16627
17650
  listSessions: () => this.list(),
16628
17651
  historyStore: this.histories,
16629
17652
  historyMaxEntries: this.sessionHistoryMaxEntries,
16630
- // Prefer what we previously stored from a current_model_update; if
16631
- // we never captured one (e.g. old opencode sessions on disk before
16632
- // this fix), fall back to the model the agent ships in its
16633
- // session/load response body.
16634
- currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
17653
+ currentModel: effectiveModel,
16635
17654
  currentMode: effectiveMode,
16636
17655
  currentUsage: params.currentUsage,
16637
17656
  agentCommands: params.agentCommands,
@@ -16640,13 +17659,14 @@ var SessionManager = class {
16640
17659
  // snapshot — the proxy's available models can change between daemon
16641
17660
  // restarts (quota resets, rollouts), so meta.json is intentionally
16642
17661
  // treated as a cold fallback here, not the authoritative source.
16643
- agentModels: nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels,
17662
+ agentModels: advertisedModels,
16644
17663
  // Only gate the first-prompt title heuristic when we actually have
16645
17664
  // a title to preserve. A title-less session (lost to a write race
16646
17665
  // or never seeded) should re-derive from the next prompt rather
16647
17666
  // than stay stuck.
16648
17667
  firstPromptSeeded: !!params.title,
16649
- createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
17668
+ createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
17669
+ extensionCommands: this.extensionCommands
16650
17670
  });
16651
17671
  await this.attachManagerHooks(session);
16652
17672
  return session;
@@ -16665,7 +17685,11 @@ var SessionManager = class {
16665
17685
  cwd,
16666
17686
  agentArgs: params.agentArgs,
16667
17687
  mcpServers: [],
16668
- onInstallProgress: params.onInstallProgress
17688
+ onInstallProgress: params.onInstallProgress,
17689
+ // Pass the persisted model so bootstrapAgent calls session/set_model
17690
+ // during session/new — the only context where the agent reliably
17691
+ // honours the switch.
17692
+ model: params.currentModel
16669
17693
  });
16670
17694
  const advertisedModes = params.agentModes ?? fresh.initialModes;
16671
17695
  const effectiveMode = await restoreCurrentMode({
@@ -16676,6 +17700,15 @@ var SessionManager = class {
16676
17700
  advertisedModes,
16677
17701
  logger: this.logger
16678
17702
  });
17703
+ const advertisedModels = params.agentModels ?? fresh.initialModels;
17704
+ const effectiveModel = await restoreCurrentModel({
17705
+ agent: fresh.agent,
17706
+ upstreamSessionId: fresh.upstreamSessionId,
17707
+ persistedModel: params.currentModel,
17708
+ agentReportedModel: fresh.initialModel,
17709
+ logger: this.logger
17710
+ });
17711
+ fresh.agent.connection.drainBuffered("session/update");
16679
17712
  const session = new Session({
16680
17713
  sessionId: params.hydraSessionId,
16681
17714
  cwd,
@@ -16692,16 +17725,15 @@ var SessionManager = class {
16692
17725
  listSessions: () => this.list(),
16693
17726
  historyStore: this.histories,
16694
17727
  historyMaxEntries: this.sessionHistoryMaxEntries,
16695
- // Prefer the stored value (set by a previous current_model_update);
16696
- // fall back to whatever the agent ships in its session/new response.
16697
- currentModel: params.currentModel ?? fresh.initialModel,
17728
+ currentModel: effectiveModel,
16698
17729
  currentMode: effectiveMode,
16699
17730
  currentUsage: params.currentUsage,
16700
17731
  agentCommands: params.agentCommands,
16701
17732
  agentModes: advertisedModes,
16702
- agentModels: params.agentModels ?? fresh.initialModels,
17733
+ agentModels: advertisedModels,
16703
17734
  firstPromptSeeded: !!params.title,
16704
- createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
17735
+ createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
17736
+ extensionCommands: this.extensionCommands
16705
17737
  });
16706
17738
  await this.attachManagerHooks(session);
16707
17739
  void session.seedFromImport().catch(() => void 0);
@@ -17553,6 +18585,13 @@ function usageSnapshotToPersisted(usage) {
17553
18585
  function persistedUsageToSnapshot(usage) {
17554
18586
  return usage ? { ...usage } : void 0;
17555
18587
  }
18588
+ function buildSessionLoadMeta(agentId, model) {
18589
+ if (!model)
18590
+ return void 0;
18591
+ if (agentId === "claude-acp")
18592
+ return { claudeCode: { options: { model } } };
18593
+ return void 0;
18594
+ }
17556
18595
  function extractInitialModel(result) {
17557
18596
  const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
17558
18597
  if (direct) {
@@ -17732,6 +18771,33 @@ async function restoreCurrentMode(opts) {
17732
18771
  return agentReportedMode;
17733
18772
  }
17734
18773
  }
18774
+ async function restoreCurrentModel(opts) {
18775
+ const { agent, upstreamSessionId, persistedModel, agentReportedModel, logger } = opts;
18776
+ if (!persistedModel) {
18777
+ return agentReportedModel;
18778
+ }
18779
+ if (persistedModel === agentReportedModel) {
18780
+ return persistedModel;
18781
+ }
18782
+ try {
18783
+ logger?.info(
18784
+ `resurrect: pushing persisted modelId=${JSON.stringify(persistedModel)} to agent (agentReported=${JSON.stringify(agentReportedModel)})`
18785
+ );
18786
+ await agent.connection.request("session/set_model", {
18787
+ sessionId: upstreamSessionId,
18788
+ modelId: persistedModel
18789
+ });
18790
+ logger?.info(
18791
+ `resurrect: session/set_model accepted, effectiveModel=${JSON.stringify(persistedModel)}`
18792
+ );
18793
+ return persistedModel;
18794
+ } catch (err) {
18795
+ logger?.warn(
18796
+ `resurrect: session/set_model rejected by agent for modelId=${JSON.stringify(persistedModel)} (${err.message}); session will use ${JSON.stringify(agentReportedModel)}`
18797
+ );
18798
+ return agentReportedModel;
18799
+ }
18800
+ }
17735
18801
  function parseModesList(list) {
17736
18802
  if (!Array.isArray(list)) {
17737
18803
  return [];
@@ -18687,6 +19753,55 @@ function withCode3(err, code) {
18687
19753
  return err;
18688
19754
  }
18689
19755
 
19756
+ // src/core/extension-commands.ts
19757
+ var ExtensionCommandRegistry = class {
19758
+ entries = /* @__PURE__ */ new Map();
19759
+ changeHandlers = [];
19760
+ register(name, connection, commands) {
19761
+ this.entries.set(name, { connection, commands: [...commands] });
19762
+ this.fireChanged();
19763
+ }
19764
+ clear(name) {
19765
+ if (this.entries.delete(name)) {
19766
+ this.fireChanged();
19767
+ }
19768
+ }
19769
+ get(name) {
19770
+ return this.entries.get(name);
19771
+ }
19772
+ has(name) {
19773
+ return this.entries.has(name);
19774
+ }
19775
+ // Snapshot of every (name, command) pair. Order is stable per-name
19776
+ // (insertion order of the map and the original commands list).
19777
+ list() {
19778
+ const out = [];
19779
+ for (const [name, entry] of this.entries) {
19780
+ for (const command of entry.commands) {
19781
+ out.push({ name, command });
19782
+ }
19783
+ }
19784
+ return out;
19785
+ }
19786
+ onChange(handler) {
19787
+ this.changeHandlers.push(handler);
19788
+ return () => {
19789
+ const i = this.changeHandlers.indexOf(handler);
19790
+ if (i >= 0) {
19791
+ this.changeHandlers.splice(i, 1);
19792
+ }
19793
+ };
19794
+ }
19795
+ fireChanged() {
19796
+ for (const h of this.changeHandlers) {
19797
+ try {
19798
+ h();
19799
+ } catch {
19800
+ }
19801
+ }
19802
+ }
19803
+ };
19804
+
18690
19805
  // src/daemon/server.ts
18691
19806
  init_paths();
18692
19807
 
@@ -19451,6 +20566,379 @@ function formatNumber(n) {
19451
20566
  init_types();
19452
20567
  init_hydra_version();
19453
20568
  init_remote_url();
20569
+
20570
+ // src/core/history-search.ts
20571
+ init_render_update();
20572
+ function parseQuery(raw) {
20573
+ const trimmed = raw.trim();
20574
+ if (trimmed.length === 0) {
20575
+ return { operator: "OR", terms: [] };
20576
+ }
20577
+ const tokenRe = /\w+:"[^"]*"|"[^"]*"|\S+/g;
20578
+ const tokens = [];
20579
+ let m;
20580
+ while ((m = tokenRe.exec(trimmed)) !== null) {
20581
+ tokens.push(m[0]);
20582
+ }
20583
+ let operator = "OR";
20584
+ let sawAnd = false;
20585
+ let sawOr = false;
20586
+ const termTokens = [];
20587
+ for (const tok of tokens) {
20588
+ const upper = tok.toUpperCase();
20589
+ if (upper === "AND") {
20590
+ sawAnd = true;
20591
+ } else if (upper === "OR") {
20592
+ sawOr = true;
20593
+ } else {
20594
+ termTokens.push(tok);
20595
+ }
20596
+ }
20597
+ if (sawAnd) {
20598
+ operator = "AND";
20599
+ } else if (sawOr) {
20600
+ operator = "OR";
20601
+ }
20602
+ const terms = termTokens.map((tok) => parseTermToken(tok)).filter((t) => t.term.length > 0);
20603
+ return { operator, terms };
20604
+ }
20605
+ function parseTermToken(tok) {
20606
+ const pq = /^(\w+):"([^"]*)"$/.exec(tok);
20607
+ if (pq) {
20608
+ return { scope: prefixToScope(pq[1]), term: pq[2] };
20609
+ }
20610
+ const q = /^"([^"]*)"$/.exec(tok);
20611
+ if (q) {
20612
+ return { scope: "all", term: q[1] };
20613
+ }
20614
+ const pb = /^(prompt|response|tool):([\s\S]*)$/i.exec(tok);
20615
+ if (pb) {
20616
+ return { scope: prefixToScope(pb[1]), term: pb[2].trim() };
20617
+ }
20618
+ return { scope: "all", term: tok.trim() };
20619
+ }
20620
+ function prefixToScope(prefix) {
20621
+ switch (prefix.toLowerCase()) {
20622
+ case "prompt":
20623
+ return "user";
20624
+ case "response":
20625
+ return "agent";
20626
+ case "tool":
20627
+ return "tool";
20628
+ default:
20629
+ return "all";
20630
+ }
20631
+ }
20632
+ function scopeMatchesKind(scope, kind) {
20633
+ if (scope === "all") {
20634
+ return true;
20635
+ }
20636
+ if (scope === "user") {
20637
+ return kind === "user";
20638
+ }
20639
+ if (scope === "agent") {
20640
+ return kind === "agent" || kind === "thought";
20641
+ }
20642
+ return kind === "tool" || kind === "tool-input";
20643
+ }
20644
+ var DEFAULT_MAX_SNIPPETS_PER_SESSION = 5;
20645
+ var DEFAULT_MAX_SESSIONS = 200;
20646
+ var SNIPPET_SIDE = 30;
20647
+ async function searchHistories(manager, query, opts = {}) {
20648
+ const parsed = parseQuery(query);
20649
+ if (parsed.terms.length === 0) {
20650
+ return { query, truncated: false, results: [] };
20651
+ }
20652
+ const maxPerSession = opts.maxSnippetsPerSession ?? DEFAULT_MAX_SNIPPETS_PER_SESSION;
20653
+ const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
20654
+ const allow = opts.sessionIds ? new Set(opts.sessionIds) : null;
20655
+ const all = await manager.list();
20656
+ const candidates = allow ? all.filter((s) => allow.has(s.sessionId)) : all;
20657
+ const results = [];
20658
+ let truncated = false;
20659
+ for (const candidate of candidates) {
20660
+ if (results.length >= maxSessions) {
20661
+ truncated = true;
20662
+ break;
20663
+ }
20664
+ const entries = await manager.loadHistory(candidate.sessionId).catch(
20665
+ () => []
20666
+ );
20667
+ const found = scanSessionEntries(entries, parsed, maxPerSession);
20668
+ if (found.snippets.length === 0) {
20669
+ continue;
20670
+ }
20671
+ const hit = {
20672
+ sessionId: candidate.sessionId,
20673
+ cwd: candidate.cwd,
20674
+ status: candidate.status,
20675
+ updatedAt: candidate.updatedAt,
20676
+ totalMatches: found.totalMatches,
20677
+ snippets: found.snippets
20678
+ };
20679
+ if (candidate.title !== void 0) {
20680
+ hit.title = candidate.title;
20681
+ }
20682
+ results.push(hit);
20683
+ }
20684
+ return { query, truncated, results };
20685
+ }
20686
+ function scanSessionEntries(entries, query, maxSnippets) {
20687
+ if (query.terms.length === 0) {
20688
+ return { totalMatches: 0, snippets: [] };
20689
+ }
20690
+ let totalMatches = 0;
20691
+ const snippets = [];
20692
+ for (const { scope, term } of query.terms) {
20693
+ const result = scanForTerm(entries, term, scope, maxSnippets - snippets.length);
20694
+ if (query.operator === "AND" && result.totalMatches === 0) {
20695
+ return { totalMatches: 0, snippets: [] };
20696
+ }
20697
+ totalMatches += result.totalMatches;
20698
+ snippets.push(...result.snippets);
20699
+ }
20700
+ return { totalMatches, snippets };
20701
+ }
20702
+ function scanForTerm(entries, term, scope, snippetBudget) {
20703
+ const needle = term.toLowerCase();
20704
+ let totalMatches = 0;
20705
+ const snippets = [];
20706
+ for (const entry of entries) {
20707
+ const fragments = extractSearchableFragments(entry).filter(
20708
+ (f) => scopeMatchesKind(scope, f.kind)
20709
+ );
20710
+ for (const frag of fragments) {
20711
+ const hay = frag.text.toLowerCase();
20712
+ let idx = hay.indexOf(needle);
20713
+ if (idx === -1) {
20714
+ continue;
20715
+ }
20716
+ let occurrences = 0;
20717
+ while (idx !== -1) {
20718
+ occurrences++;
20719
+ idx = hay.indexOf(needle, idx + needle.length);
20720
+ }
20721
+ totalMatches += occurrences;
20722
+ if (snippets.length < snippetBudget) {
20723
+ const first = hay.indexOf(needle);
20724
+ const snippet = {
20725
+ kind: frag.kind,
20726
+ text: buildSnippet(frag.text, first, needle.length),
20727
+ recordedAt: entry.recordedAt
20728
+ };
20729
+ if (frag.toolName !== void 0) {
20730
+ snippet.toolName = frag.toolName;
20731
+ }
20732
+ snippets.push(snippet);
20733
+ }
20734
+ }
20735
+ }
20736
+ return { totalMatches, snippets };
20737
+ }
20738
+ function extractSearchableFragments(entry) {
20739
+ if (entry.method !== "session/update") {
20740
+ return [];
20741
+ }
20742
+ const params = entry.params;
20743
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
20744
+ return [];
20745
+ }
20746
+ const update = params.update;
20747
+ if (!update || typeof update !== "object" || Array.isArray(update)) {
20748
+ return [];
20749
+ }
20750
+ const u = update;
20751
+ const tag = typeof u.sessionUpdate === "string" ? u.sessionUpdate : u.kind;
20752
+ if (typeof tag !== "string") {
20753
+ return [];
20754
+ }
20755
+ switch (tag) {
20756
+ case "agent_message_chunk": {
20757
+ const text = readContentText(u.content);
20758
+ return text ? [{ kind: "agent", text }] : [];
20759
+ }
20760
+ case "agent_thought":
20761
+ case "agent_thought_chunk": {
20762
+ const text = typeof u.text === "string" ? sanitizeWireText(u.text) : readContentText(u.content);
20763
+ return text ? [{ kind: "thought", text }] : [];
20764
+ }
20765
+ case "user_message_chunk": {
20766
+ if (isCompatPromptReceived(u)) {
20767
+ return [];
20768
+ }
20769
+ const text = readContentText(u.content);
20770
+ return text ? [{ kind: "user", text }] : [];
20771
+ }
20772
+ case "prompt_received": {
20773
+ const text = readPromptText(u.prompt);
20774
+ return text ? [{ kind: "user", text }] : [];
20775
+ }
20776
+ case "tool_call":
20777
+ case "tool_call_update": {
20778
+ return extractToolFragments(u);
20779
+ }
20780
+ default:
20781
+ return [];
20782
+ }
20783
+ }
20784
+ function extractToolFragments(u) {
20785
+ const toolName = readString2(u, "name");
20786
+ const title = readString2(u, "title");
20787
+ const out = [];
20788
+ if (title !== void 0) {
20789
+ const sanitized = sanitizeSingleLine(title);
20790
+ if (sanitized.length > 0) {
20791
+ const frag = { kind: "tool", text: sanitized };
20792
+ if (toolName !== void 0) {
20793
+ frag.toolName = toolName;
20794
+ }
20795
+ out.push(frag);
20796
+ }
20797
+ }
20798
+ if (toolName !== void 0 && toolName !== title) {
20799
+ const sanitized = sanitizeSingleLine(toolName);
20800
+ if (sanitized.length > 0) {
20801
+ out.push({ kind: "tool", toolName, text: sanitized });
20802
+ }
20803
+ }
20804
+ const rawInput = u.rawInput;
20805
+ if (rawInput && typeof rawInput === "object") {
20806
+ const serialized = safeStringify(rawInput);
20807
+ if (serialized.length > 0) {
20808
+ const frag = {
20809
+ kind: "tool-input",
20810
+ text: sanitizeSingleLine(serialized)
20811
+ };
20812
+ if (toolName !== void 0) {
20813
+ frag.toolName = toolName;
20814
+ }
20815
+ out.push(frag);
20816
+ }
20817
+ }
20818
+ const locations = u.locations;
20819
+ if (Array.isArray(locations) && locations.length > 0) {
20820
+ const serialized = safeStringify(locations);
20821
+ if (serialized.length > 0) {
20822
+ const frag = {
20823
+ kind: "tool-input",
20824
+ text: sanitizeSingleLine(serialized)
20825
+ };
20826
+ if (toolName !== void 0) {
20827
+ frag.toolName = toolName;
20828
+ }
20829
+ out.push(frag);
20830
+ }
20831
+ }
20832
+ const errorText = extractToolErrorText(u);
20833
+ if (errorText !== null) {
20834
+ const frag = { kind: "tool", text: errorText };
20835
+ if (toolName !== void 0) {
20836
+ frag.toolName = toolName;
20837
+ }
20838
+ out.push(frag);
20839
+ }
20840
+ return out;
20841
+ }
20842
+ function extractToolErrorText(u) {
20843
+ const content = u.content;
20844
+ if (Array.isArray(content)) {
20845
+ for (const block of content) {
20846
+ if (!block || typeof block !== "object") {
20847
+ continue;
20848
+ }
20849
+ const b = block;
20850
+ const inner = b.content;
20851
+ if (!inner || typeof inner !== "object") {
20852
+ continue;
20853
+ }
20854
+ const i = inner;
20855
+ if (i.type === "text" && typeof i.text === "string") {
20856
+ const s = sanitizeSingleLine(i.text);
20857
+ if (s.length > 0) {
20858
+ return s;
20859
+ }
20860
+ }
20861
+ }
20862
+ }
20863
+ const rawOutput = u.rawOutput;
20864
+ if (rawOutput && typeof rawOutput === "object") {
20865
+ const err = rawOutput.error;
20866
+ if (typeof err === "string") {
20867
+ const s = sanitizeSingleLine(err);
20868
+ if (s.length > 0) {
20869
+ return s;
20870
+ }
20871
+ }
20872
+ }
20873
+ return null;
20874
+ }
20875
+ function isCompatPromptReceived(u) {
20876
+ const meta = u._meta;
20877
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
20878
+ return false;
20879
+ }
20880
+ const hydra = meta["hydra-acp"];
20881
+ if (!hydra || typeof hydra !== "object" || Array.isArray(hydra)) {
20882
+ return false;
20883
+ }
20884
+ return hydra.compatFor === "prompt_received";
20885
+ }
20886
+ function readContentText(content) {
20887
+ if (typeof content === "string") {
20888
+ return sanitizeWireText(content);
20889
+ }
20890
+ if (!content || typeof content !== "object" || Array.isArray(content)) {
20891
+ return "";
20892
+ }
20893
+ const c = content;
20894
+ if (typeof c.text === "string") {
20895
+ return sanitizeWireText(c.text);
20896
+ }
20897
+ return "";
20898
+ }
20899
+ function readPromptText(prompt) {
20900
+ if (!Array.isArray(prompt)) {
20901
+ return "";
20902
+ }
20903
+ const parts = [];
20904
+ for (const block of prompt) {
20905
+ const text = readContentText(block);
20906
+ if (text.length > 0) {
20907
+ parts.push(text);
20908
+ }
20909
+ }
20910
+ return parts.join("");
20911
+ }
20912
+ function readString2(u, key) {
20913
+ const v = u[key];
20914
+ return typeof v === "string" ? v : void 0;
20915
+ }
20916
+ function safeStringify(value) {
20917
+ try {
20918
+ return JSON.stringify(value);
20919
+ } catch {
20920
+ return "";
20921
+ }
20922
+ }
20923
+ function buildSnippet(text, matchIdx, matchLen) {
20924
+ const flat = text.replace(/\s+/g, " ").trim();
20925
+ if (flat.length === 0) {
20926
+ return "";
20927
+ }
20928
+ const flatLower = flat.toLowerCase();
20929
+ const needleSlice = text.slice(matchIdx, matchIdx + matchLen).toLowerCase().replace(/\s+/g, " ").trim();
20930
+ let pos = needleSlice.length > 0 ? flatLower.indexOf(needleSlice) : 0;
20931
+ if (pos === -1) {
20932
+ pos = 0;
20933
+ }
20934
+ const start = Math.max(0, pos - SNIPPET_SIDE);
20935
+ const end = Math.min(flat.length, pos + needleSlice.length + SNIPPET_SIDE);
20936
+ const head = start > 0 ? "\u2026" : "";
20937
+ const tail = end < flat.length ? "\u2026" : "";
20938
+ return `${head}${flat.slice(start, end)}${tail}`;
20939
+ }
20940
+
20941
+ // src/daemon/routes/sessions.ts
19454
20942
  function resolveHydraHost(defaults) {
19455
20943
  if (defaults.publicHost && defaults.publicHost.length > 0) {
19456
20944
  return defaults.publicHost;
@@ -19466,6 +20954,17 @@ function registerSessionRoutes(app, manager, defaults) {
19466
20954
  const sessions = await manager.list({ cwd: query?.cwd });
19467
20955
  return { sessions };
19468
20956
  });
20957
+ app.get("/v1/sessions/search", async (request, reply) => {
20958
+ const query = request.query;
20959
+ const q = query?.q ?? "";
20960
+ if (q.trim().length === 0) {
20961
+ reply.code(400).send({ error: "q is required" });
20962
+ return reply;
20963
+ }
20964
+ const ids = query?.sessionIds ? query.sessionIds.split(",").filter((s) => s.length > 0) : void 0;
20965
+ const out = await searchHistories(manager, q, { sessionIds: ids });
20966
+ return out;
20967
+ });
19469
20968
  app.post("/v1/sessions", async (request, reply) => {
19470
20969
  const body = request.body ?? {};
19471
20970
  const cwd = expandHome(body.cwd ?? defaults.cwd);
@@ -20253,6 +21752,34 @@ function registerAcpWsEndpoint(app, deps) {
20253
21752
  }
20254
21753
  return buildInitializeResult();
20255
21754
  });
21755
+ if (processIdentity && deps.extensionCommands) {
21756
+ const registry = deps.extensionCommands;
21757
+ connection.onRequest("hydra-acp/register_commands", async (raw) => {
21758
+ const params = raw ?? {};
21759
+ const commands = Array.isArray(params.commands) ? params.commands.map((c) => {
21760
+ if (!c || typeof c !== "object") {
21761
+ return void 0;
21762
+ }
21763
+ const obj = c;
21764
+ if (typeof obj.verb !== "string" || obj.verb.length === 0) {
21765
+ return void 0;
21766
+ }
21767
+ const spec = { verb: obj.verb };
21768
+ if (typeof obj.argsHint === "string") {
21769
+ spec.argsHint = obj.argsHint;
21770
+ }
21771
+ if (typeof obj.description === "string") {
21772
+ spec.description = obj.description;
21773
+ }
21774
+ return spec;
21775
+ }).filter((s) => s !== void 0) : [];
21776
+ registry.register(processIdentity.name, connection, commands);
21777
+ return { ok: true, registered: commands.length };
21778
+ });
21779
+ connection.onClose(() => {
21780
+ registry.clear(processIdentity.name);
21781
+ });
21782
+ }
20256
21783
  if (processIdentity?.kind === "transformer") {
20257
21784
  connection.onRequest("transformer/initialize", async (raw) => {
20258
21785
  const params = raw ?? {};
@@ -20800,7 +22327,10 @@ function registerAcpWsEndpoint(app, deps) {
20800
22327
  return null;
20801
22328
  }
20802
22329
  app.log.info(decision.logMessage);
20803
- return decision.session.forwardRequest("session/set_model", rawParams);
22330
+ const { modelId } = rawParams;
22331
+ const result = await decision.session.forwardRequest("session/set_model", rawParams);
22332
+ decision.session.applyModelChange(modelId);
22333
+ return result;
20804
22334
  });
20805
22335
  connection.onRequest("session/set_mode", async (rawParams) => {
20806
22336
  const params = rawParams;
@@ -21191,13 +22721,15 @@ async function startDaemon(config, serviceToken) {
21191
22721
  stderrTailBytes: config.daemon.agentStderrTailBytes,
21192
22722
  logger: agentLogger
21193
22723
  });
22724
+ const extensionCommands = new ExtensionCommandRegistry();
21194
22725
  const manager = new SessionManager(registry, spawner, void 0, {
21195
22726
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
21196
22727
  defaultModels: config.defaultModels,
21197
22728
  defaultTransformers: config.defaultTransformers,
21198
22729
  sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
21199
22730
  logger: agentLogger,
21200
- npmRegistry: config.npmRegistry
22731
+ npmRegistry: config.npmRegistry,
22732
+ extensionCommands
21201
22733
  });
21202
22734
  const extensions = new ExtensionManager(extensionList(config), void 0, {
21203
22735
  tokenRegistry: processRegistry
@@ -21231,7 +22763,8 @@ async function startDaemon(config, serviceToken) {
21231
22763
  processRegistry,
21232
22764
  onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
21233
22765
  onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
21234
- transformers
22766
+ transformers,
22767
+ extensionCommands
21235
22768
  });
21236
22769
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
21237
22770
  const address = app.server.address();
@@ -24282,6 +25815,7 @@ async function main() {
24282
25815
  const positionalAgentId = afterLaunch[0];
24283
25816
  const agentArgs = afterLaunch.slice(1);
24284
25817
  const { flags: flags2 } = parseArgs(beforeLaunch);
25818
+ rejectUnknownFlags(flags2);
24285
25819
  if (flags2.reattach === true) {
24286
25820
  process.stderr.write(
24287
25821
  "hydra-acp launch: --reattach is not valid here. Pass --session <id-or-url> to attach to a specific session.\n"
@@ -24320,6 +25854,7 @@ async function main() {
24320
25854
  return;
24321
25855
  }
24322
25856
  const { positional, flags } = parseArgs(argv);
25857
+ rejectUnknownFlags(flags);
24323
25858
  if (flags.version === true || positional[0] === "--version") {
24324
25859
  process.stdout.write(`hydra-acp ${readVersion()}
24325
25860
  `);
@@ -24707,6 +26242,17 @@ function parseNumericFlag(flags, name) {
24707
26242
  }
24708
26243
  return void 0;
24709
26244
  }
26245
+ function rejectUnknownFlags(flags) {
26246
+ const unknown = validateKnownFlags(flags);
26247
+ if (unknown === void 0) {
26248
+ return;
26249
+ }
26250
+ process.stderr.write(`hydra-acp: unknown flag: --${unknown}
26251
+
26252
+ `);
26253
+ printHelp();
26254
+ process.exit(2);
26255
+ }
24710
26256
  function readShortPrompt(argv) {
24711
26257
  for (let i = 0; i < argv.length; i += 1) {
24712
26258
  const tok = argv[i];