@hydra-acp/cli 0.1.48 → 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,11 +10150,8 @@ uncaught: ${err.stack ?? err.message}
9982
10150
  }
9983
10151
  });
9984
10152
 
9985
- // src/tui/picker.ts
9986
- function createPickerPrefs() {
9987
- return { filters: { cwdOnly: false, hostFilter: "__local" } };
9988
- }
9989
- async function pickSession(term, opts) {
10153
+ // src/tui/prompt-utils.ts
10154
+ function resetTerminalModes() {
9990
10155
  process.stdout.write("\x1B[<u");
9991
10156
  process.stdout.write("\x1B[?2004l");
9992
10157
  process.stdout.write("\x1B[>4;0m");
@@ -9996,60 +10161,501 @@ async function pickSession(term, opts) {
9996
10161
  process.stdout.write("\x1B[?1006l");
9997
10162
  process.stdout.write("\x1B[?1l");
9998
10163
  process.stdout.write("\x1B>");
9999
- const sortSessions = (sessions) => {
10000
- const score = (s) => {
10001
- if (s.status !== "live") {
10002
- return 0;
10003
- }
10004
- return s.cwd === opts.cwd ? 2 : 1;
10005
- };
10006
- return [...sessions].sort((a, b) => {
10007
- const tier = score(b) - score(a);
10008
- if (tier !== 0) {
10009
- return tier;
10010
- }
10011
- return b.updatedAt.slice(0, 16).localeCompare(a.updatedAt.slice(0, 16));
10012
- });
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
10013
10206
  };
10014
- const prefs = opts.prefs ?? createPickerPrefs();
10015
- if (opts.prefs === void 0 && opts.currentSessionId !== void 0) {
10016
- const current = opts.sessions.find(
10017
- (s) => s.sessionId === opts.currentSessionId
10018
- );
10019
- if (current?.importedFromMachine) {
10020
- prefs.filters.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" };
10021
10255
  }
10256
+ return { kind: "resolve", action: choice.key };
10022
10257
  }
10023
- let allSessions = sortSessions(opts.sessions);
10024
- const applyPrefsFilters = (sessions) => {
10025
- let base = sessions;
10026
- if (prefs.filters.cwdOnly) {
10027
- base = base.filter((s) => s.cwd === opts.cwd);
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
+ };
10028
10277
  }
10029
- base = filterByHost(base, prefs.filters.hostFilter);
10030
- return base;
10031
- };
10032
- let visible = applyPrefsFilters(allSessions);
10033
- let rows = visible.map((s) => toRow(s, Date.now()));
10034
- let widths = computeWidths(rows);
10035
- let total = 1 + visible.length;
10036
- let selectedIdx = 0;
10037
- let scrollOffset = 0;
10038
- if (opts.currentSessionId !== void 0) {
10039
- const idx = visible.findIndex((s) => s.sessionId === opts.currentSessionId);
10040
- if (idx >= 0) {
10041
- selectedIdx = idx + 1;
10278
+ if (lower === "p") {
10279
+ return {
10280
+ kind: "continue",
10281
+ selected: Math.max(0, selected - 1)
10282
+ };
10042
10283
  }
10043
- }
10044
- let searchActive = false;
10284
+ const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
10285
+ if (idx >= 0) {
10286
+ const choice = choices[idx];
10287
+ if (choice) {
10288
+ return { kind: "resolve", action: choice.key };
10289
+ }
10290
+ }
10291
+ }
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;
10045
10643
  let searchTerm = "";
10046
10644
  let mode = "normal";
10047
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;
10048
10654
  let renameBuffer = "";
10049
10655
  let transientStatus = null;
10050
10656
  const composer = new InputDispatcher({ history: [] });
10051
- let termHeight = readTermHeight(term);
10052
- let termWidth = readTermWidth(term);
10657
+ let termHeight = readTermHeight2(term);
10658
+ let termWidth = readTermWidth2(term);
10053
10659
  let viewportSize = 0;
10054
10660
  let composerTitle = "";
10055
10661
  let composerRoom = 0;
@@ -10061,10 +10667,16 @@ async function pickSession(term, opts) {
10061
10667
  let headerLine = "";
10062
10668
  let sessionLines = [];
10063
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;
10064
10676
  const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
10065
10677
  const computeLayout = () => {
10066
- termHeight = readTermHeight(term);
10067
- termWidth = readTermWidth(term);
10678
+ termHeight = readTermHeight2(term);
10679
+ termWidth = readTermWidth2(term);
10068
10680
  const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
10069
10681
  composerRoom = Math.max(10, termWidth - BOX_HORIZONTAL_PAD);
10070
10682
  const titleBudget = Math.max(10, termWidth - 8);
@@ -10115,6 +10727,19 @@ async function pickSession(term, opts) {
10115
10727
  }
10116
10728
  adjustScroll();
10117
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
+ };
10118
10743
  const adjustScroll = () => {
10119
10744
  if (selectedIdx === 0) {
10120
10745
  return;
@@ -10240,59 +10865,370 @@ async function pickSession(term, opts) {
10240
10865
  if (visualOffset < 0 || visualOffset >= composerRows) {
10241
10866
  return;
10242
10867
  }
10243
- const col = 3 + composerCursorCol;
10244
- 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
+ }
10245
11100
  };
10246
- const renderFromScratch = () => {
10247
- if (mode === "help") {
10248
- renderHelp();
10249
- return;
10250
- }
11101
+ const renderFind = () => {
11102
+ computeFindBoxLayout();
11103
+ const focused = findSubMode === "input";
11104
+ const queryText = findComposer.state().buffer.join("\n");
10251
11105
  withSync(() => {
10252
11106
  term.hideCursor();
10253
- computeLayout();
10254
- adjustScroll();
10255
- startRow = 1;
10256
11107
  term.moveTo(1, 1).eraseDisplayBelow();
10257
- paintComposerTopBorder();
10258
- term("\n");
10259
- for (let v = 0; v < composerRows; v++) {
10260
- paintComposerBodyRow(composerWindowStart + v);
10261
- term("\n");
10262
- }
10263
- paintComposerBottomBorder();
10264
- term("\n\n");
10265
- term.dim.noFormat(` ${headerLine}`)("\n");
10266
- for (let v = 0; v < viewportSize; v++) {
10267
- paintSessionRow(scrollOffset + v);
10268
- 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();
10269
11149
  }
10270
- paintIndicator();
10271
- term("\n");
10272
- if (selectedIdx === 0) {
10273
- placeComposerCursor();
11150
+ if (focused) {
11151
+ term.moveTo(findBoxCursorCol(), findBoxCursorScreenRow());
10274
11152
  term.hideCursor(false);
10275
11153
  }
10276
11154
  });
10277
11155
  };
10278
- const renderHelp = () => {
11156
+ const repaintFindResult = (idx, focused) => {
11157
+ const viewportIdx = idx - findScrollOffset;
11158
+ if (viewportIdx < 0 || viewportIdx >= findViewportSize()) {
11159
+ return;
11160
+ }
10279
11161
  withSync(() => {
10280
- term.hideCursor();
10281
- term.moveTo(1, 1).eraseDisplayBelow();
10282
- term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
10283
- for (const entry of HELP_ENTRIES) {
10284
- if (entry === null) {
10285
- term("\n");
10286
- 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();
10287
11192
  }
10288
- const [keys, desc] = entry;
10289
- term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
10290
- term.noFormat(desc)("\n");
10291
11193
  }
10292
- term("\n");
10293
- term.dim.noFormat(" press any key to dismiss")("\n");
11194
+ term.moveTo(1, findResultsStartRow() + v * 2);
11195
+ paintFindIndicator();
10294
11196
  });
10295
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
+ };
10296
11232
  const repaintComposerChrome = () => {
10297
11233
  withSync(() => {
10298
11234
  const showCursor = selectedIdx === 0;
@@ -10441,23 +11377,48 @@ async function pickSession(term, opts) {
10441
11377
  let resolved = false;
10442
11378
  let autoRefreshTimer = null;
10443
11379
  let autoRefreshInFlight = false;
10444
- const onResize = () => {
10445
- if (resolved) {
10446
- 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();
10447
11388
  }
10448
- 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();
10449
11409
  };
10450
11410
  const cleanup = () => {
10451
11411
  if (resolved) {
10452
11412
  return;
10453
11413
  }
10454
11414
  resolved = true;
11415
+ focusStack.length = 0;
10455
11416
  if (autoRefreshTimer) {
10456
11417
  clearInterval(autoRefreshTimer);
10457
11418
  autoRefreshTimer = null;
10458
11419
  }
10459
- term.off("key", onKey);
10460
- term.off("resize", onResize);
11420
+ term.off("key", dispatch);
11421
+ term.off("resize", dispatchResize);
10461
11422
  process.stdout.write("\x1B[?2004l");
10462
11423
  const tClean = term;
10463
11424
  if (tClean.stdin && tkStdinHandler) {
@@ -10613,18 +11574,231 @@ ${cells}`;
10613
11574
  paintIndicator();
10614
11575
  return true;
10615
11576
  };
10616
- const onKey = (name, _matches, data) => {
10617
- 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();
10618
11595
  return;
10619
11596
  }
10620
- if (mode === "help") {
10621
- if (name === "CTRL_C") {
10622
- cleanup();
10623
- 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
+ }
10624
11663
  return;
10625
11664
  }
10626
- mode = "normal";
10627
- 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") {
10628
11802
  return;
10629
11803
  }
10630
11804
  if (mode === "rename") {
@@ -10688,6 +11862,10 @@ ${cells}`;
10688
11862
  return;
10689
11863
  }
10690
11864
  clearTransient();
11865
+ if (name === "CTRL_F") {
11866
+ openFindLayer();
11867
+ return;
11868
+ }
10691
11869
  if (selectedIdx === 0 && !searchActive) {
10692
11870
  if (name === "ESCAPE" || name === "CTRL_C" || name === "CTRL_D") {
10693
11871
  cleanup();
@@ -10759,8 +11937,7 @@ ${cells}`;
10759
11937
  return;
10760
11938
  }
10761
11939
  if (!searchActive && data?.isCharacter && name === "?") {
10762
- mode = "help";
10763
- renderHelp();
11940
+ openHelpLayer();
10764
11941
  return;
10765
11942
  }
10766
11943
  if (searchActive) {
@@ -10820,13 +11997,7 @@ ${cells}`;
10820
11997
  const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
10821
11998
  prefs.filters.cwdOnly = !prefs.filters.cwdOnly;
10822
11999
  applyFilter();
10823
- if (keepId !== void 0) {
10824
- const idx = visible.findIndex((s) => s.sessionId === keepId);
10825
- if (idx >= 0) {
10826
- selectedIdx = idx + 1;
10827
- adjustScroll();
10828
- }
10829
- }
12000
+ restoreCursorAfterFilter(keepId);
10830
12001
  renderFromScratch();
10831
12002
  return;
10832
12003
  }
@@ -10837,13 +12008,7 @@ ${cells}`;
10837
12008
  allSessions
10838
12009
  );
10839
12010
  applyFilter();
10840
- if (keepId !== void 0) {
10841
- const idx = visible.findIndex((s) => s.sessionId === keepId);
10842
- if (idx >= 0) {
10843
- selectedIdx = idx + 1;
10844
- adjustScroll();
10845
- }
10846
- }
12011
+ restoreCursorAfterFilter(keepId);
10847
12012
  renderFromScratch();
10848
12013
  return;
10849
12014
  }
@@ -10980,6 +12145,12 @@ ${cells}`;
10980
12145
  return;
10981
12146
  }
10982
12147
  };
12148
+ pushLayer({
12149
+ onKey: (name, _matches, data) => onKey(name, _matches, data),
12150
+ onResize: () => {
12151
+ if (!resolved) renderFromScratch();
12152
+ }
12153
+ });
10983
12154
  term.grabInput({});
10984
12155
  const tSetup = term;
10985
12156
  if (tSetup.stdin && typeof tSetup.onStdin === "function") {
@@ -10988,10 +12159,10 @@ ${cells}`;
10988
12159
  tSetup.stdin.on("data", rawStdinHandler);
10989
12160
  process.stdout.write("\x1B[?2004h");
10990
12161
  }
10991
- term.on("key", onKey);
10992
- term.on("resize", onResize);
12162
+ term.on("key", dispatch);
12163
+ term.on("resize", dispatchResize);
10993
12164
  autoRefreshTimer = setInterval(() => {
10994
- if (resolved || mode !== "normal" || searchActive || autoRefreshInFlight) {
12165
+ if (resolved || focusStack.length > 1 || mode !== "normal" || searchActive || autoRefreshInFlight) {
10995
12166
  return;
10996
12167
  }
10997
12168
  const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
@@ -11002,10 +12173,10 @@ ${cells}`;
11002
12173
  }, 3e3);
11003
12174
  });
11004
12175
  }
11005
- function readTermHeight(term) {
12176
+ function readTermHeight2(term) {
11006
12177
  return term.height ?? 24;
11007
12178
  }
11008
- function readTermWidth(term) {
12179
+ function readTermWidth2(term) {
11009
12180
  return term.width ?? 80;
11010
12181
  }
11011
12182
  function formatComposerTitle(cwd, maxWidth) {
@@ -11060,7 +12231,7 @@ function matchesSearch(s, term) {
11060
12231
  }
11061
12232
  return false;
11062
12233
  }
11063
- 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;
11064
12235
  var init_picker = __esm({
11065
12236
  "src/tui/picker.ts"() {
11066
12237
  "use strict";
@@ -11070,9 +12241,11 @@ var init_picker = __esm({
11070
12241
  init_discovery();
11071
12242
  init_input();
11072
12243
  init_screen();
12244
+ init_import_action_prompt();
11073
12245
  init_sync();
11074
12246
  ROW_PREFIX_WIDTH = 2;
11075
12247
  PICKER_COMPOSER_MAX_ROWS = 4;
12248
+ FIND_BOX_MAX_ROWS = 4;
11076
12249
  BOX_HORIZONTAL_PAD = 4;
11077
12250
  HELP_KEYS_WIDTH = 20;
11078
12251
  HELP_ENTRIES = [
@@ -11085,7 +12258,8 @@ var init_picker = __esm({
11085
12258
  ["Enter", "open selected session"],
11086
12259
  ["v", "view-only (open transcript without spawning the agent)"],
11087
12260
  null,
11088
- ["/", "search sessions"],
12261
+ ["/", "search sessions (metadata)"],
12262
+ ["^f", "find in session history (content + tool inputs)"],
11089
12263
  ["o", "toggle cwd-only filter"],
11090
12264
  ["h", "cycle host filter (local / <peer> / all)"],
11091
12265
  ["r", "refresh from daemon"],
@@ -11121,100 +12295,10 @@ async function validateLocalCwd(input) {
11121
12295
  }
11122
12296
  return { ok: true, path: resolved };
11123
12297
  }
11124
- var init_cwd = __esm({
11125
- "src/core/cwd.ts"() {
11126
- "use strict";
11127
- init_config();
11128
- }
11129
- });
11130
-
11131
- // src/tui/prompt-utils.ts
11132
- function resetTerminalModes() {
11133
- process.stdout.write("\x1B[<u");
11134
- process.stdout.write("\x1B[?2004l");
11135
- process.stdout.write("\x1B[>4;0m");
11136
- process.stdout.write("\x1B[>5;0m");
11137
- process.stdout.write("\x1B[?1000l");
11138
- process.stdout.write("\x1B[?1002l");
11139
- process.stdout.write("\x1B[?1006l");
11140
- process.stdout.write("\x1B[?1l");
11141
- process.stdout.write("\x1B>");
11142
- }
11143
- function readTermWidth2(term) {
11144
- return term.width ?? 80;
11145
- }
11146
- function readTermHeight2(term) {
11147
- return term.height ?? 24;
11148
- }
11149
- function drawBox(term, opts) {
11150
- const termW = readTermWidth2(term);
11151
- const termH = readTermHeight2(term);
11152
- const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
11153
- const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
11154
- const contentW = Math.min(desiredContentW, maxContentW);
11155
- const w = contentW + 2;
11156
- const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
11157
- const h = contentH + 2;
11158
- const x = Math.max(1, Math.floor((termW - w) / 2) + 1);
11159
- const y = Math.max(1, Math.floor((termH - h) / 2) + 1);
11160
- term.moveTo(1, 1).eraseDisplayBelow();
11161
- const topInner = HORIZ.repeat(w - 2);
11162
- const top = renderTitleStrip(topInner, opts.title);
11163
- term.moveTo(x, y);
11164
- term.dim.noFormat(TL);
11165
- paintTopStrip(term, top);
11166
- term.dim.noFormat(TR);
11167
- for (let row = 1; row <= contentH; row++) {
11168
- term.moveTo(x, y + row);
11169
- term.dim.noFormat(VERT);
11170
- term.moveTo(x + w - 1, y + row);
11171
- term.dim.noFormat(VERT);
11172
- }
11173
- term.moveTo(x, y + h - 1);
11174
- term.dim.noFormat(BL + HORIZ.repeat(w - 2) + BR);
11175
- return {
11176
- x,
11177
- y,
11178
- w,
11179
- h,
11180
- contentX: x + 1,
11181
- contentY: y + 1,
11182
- contentW,
11183
- contentH
11184
- };
11185
- }
11186
- function renderTitleStrip(innerDashes, title) {
11187
- if (!title) {
11188
- return { dashes: innerDashes };
11189
- }
11190
- const chip = ` ${title} `;
11191
- if (chip.length + 4 > innerDashes.length) {
11192
- return { dashes: innerDashes };
11193
- }
11194
- const offset = 2;
11195
- const dashes = innerDashes.slice(0, offset) + " ".repeat(chip.length) + innerDashes.slice(offset + chip.length);
11196
- return { dashes, title: { offset, text: chip } };
11197
- }
11198
- function paintTopStrip(term, strip) {
11199
- if (!strip.title) {
11200
- term.dim.noFormat(strip.dashes);
11201
- return;
11202
- }
11203
- term.dim.noFormat(strip.dashes.slice(0, strip.title.offset));
11204
- term.brightCyan.noFormat(strip.title.text);
11205
- term.dim.noFormat(strip.dashes.slice(strip.title.offset + strip.title.text.length));
11206
- }
11207
- var MAX_BOX_WIDTH, HORIZ, VERT, TL, TR, BL, BR;
11208
- var init_prompt_utils = __esm({
11209
- "src/tui/prompt-utils.ts"() {
12298
+ var init_cwd = __esm({
12299
+ "src/core/cwd.ts"() {
11210
12300
  "use strict";
11211
- MAX_BOX_WIDTH = 64;
11212
- HORIZ = "\u2500";
11213
- VERT = "\u2502";
11214
- TL = "\u250C";
11215
- TR = "\u2510";
11216
- BL = "\u2514";
11217
- BR = "\u2518";
12301
+ init_config();
11218
12302
  }
11219
12303
  });
11220
12304
 
@@ -11246,7 +12330,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11246
12330
  for (const hr of headerRows) {
11247
12331
  term.moveTo(layout.contentX, layout.contentY + row);
11248
12332
  term.dim.noFormat(` ${hr.label}`);
11249
- term.noFormat(truncate2(hr.value, innerW - hr.label.length - 2));
12333
+ term.noFormat(truncate3(hr.value, innerW - hr.label.length - 2));
11250
12334
  row++;
11251
12335
  }
11252
12336
  row++;
@@ -11257,7 +12341,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11257
12341
  row += 2;
11258
12342
  if (errorLine !== null) {
11259
12343
  term.moveTo(layout.contentX, layout.contentY + row);
11260
- term.red.noFormat(` ${truncate2(errorLine, innerW - 2)}`);
12344
+ term.red.noFormat(` ${truncate3(errorLine, innerW - 2)}`);
11261
12345
  } else {
11262
12346
  term.moveTo(layout.contentX, layout.contentY + row);
11263
12347
  term.dim.noFormat(
@@ -11293,7 +12377,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11293
12377
  term.dim.noFormat("\u2502");
11294
12378
  term.moveTo(layout.contentX, layout.contentY + errRow);
11295
12379
  if (errorLine !== null) {
11296
- term.red.noFormat(` ${truncate2(errorLine, layout.contentW - 2)}`);
12380
+ term.red.noFormat(` ${truncate3(errorLine, layout.contentW - 2)}`);
11297
12381
  } else {
11298
12382
  term.dim.noFormat(
11299
12383
  " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
@@ -11389,7 +12473,7 @@ async function promptForImportCwd(term, session, opts = {}) {
11389
12473
  term.on("resize", onResize);
11390
12474
  });
11391
12475
  }
11392
- function truncate2(s, max) {
12476
+ function truncate3(s, max) {
11393
12477
  if (max <= 1) {
11394
12478
  return "";
11395
12479
  }
@@ -11417,226 +12501,6 @@ var init_import_cwd_prompt = __esm({
11417
12501
  }
11418
12502
  });
11419
12503
 
11420
- // src/tui/import-action-prompt.ts
11421
- function actionPromptStep(selected, key, choices = ACTION_CHOICES) {
11422
- if (key.kind === "cancel") {
11423
- return { kind: "cancel" };
11424
- }
11425
- if (key.kind === "back") {
11426
- return { kind: "back" };
11427
- }
11428
- if (key.kind === "enter") {
11429
- const choice = choices[selected];
11430
- if (!choice) {
11431
- return { kind: "back" };
11432
- }
11433
- return { kind: "resolve", action: choice.key };
11434
- }
11435
- if (key.kind === "up") {
11436
- return {
11437
- kind: "continue",
11438
- selected: Math.max(0, selected - 1)
11439
- };
11440
- }
11441
- if (key.kind === "down") {
11442
- return {
11443
- kind: "continue",
11444
- selected: Math.min(choices.length - 1, selected + 1)
11445
- };
11446
- }
11447
- if (key.kind === "char") {
11448
- const lower = key.ch.toLowerCase();
11449
- if (lower === "n") {
11450
- return {
11451
- kind: "continue",
11452
- selected: Math.min(choices.length - 1, selected + 1)
11453
- };
11454
- }
11455
- if (lower === "p") {
11456
- return {
11457
- kind: "continue",
11458
- selected: Math.max(0, selected - 1)
11459
- };
11460
- }
11461
- const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
11462
- if (idx >= 0) {
11463
- const choice = choices[idx];
11464
- if (choice) {
11465
- return { kind: "resolve", action: choice.key };
11466
- }
11467
- }
11468
- }
11469
- return { kind: "continue", selected };
11470
- }
11471
- async function promptForImportAction(term, session) {
11472
- resetTerminalModes();
11473
- const shortId2 = stripHydraSessionPrefix(session.sessionId);
11474
- const fromMachine = session.importedFromMachine ?? "another machine";
11475
- const originalCwd = shortenHomePath(session.cwd);
11476
- let selected = ACTION_CHOICES.findIndex((c) => c.key === "view");
11477
- if (selected < 0) {
11478
- selected = 0;
11479
- }
11480
- const render = () => {
11481
- const choiceRows = ACTION_CHOICES.length * 2;
11482
- const contentHeight = 7 + choiceRows + 2;
11483
- const layout = drawBox(term, {
11484
- contentHeight,
11485
- title: "Imported session"
11486
- });
11487
- const innerW = layout.contentW;
11488
- const headerRows = [
11489
- { label: "session: ", value: shortId2 },
11490
- { label: "from: ", value: fromMachine },
11491
- { label: "cwd: ", value: originalCwd }
11492
- ];
11493
- let row = 0;
11494
- for (const hr of headerRows) {
11495
- term.moveTo(layout.contentX, layout.contentY + row);
11496
- term.dim.noFormat(` ${hr.label}`);
11497
- term.noFormat(truncate3(hr.value, innerW - hr.label.length - 2));
11498
- row++;
11499
- }
11500
- row++;
11501
- term.moveTo(layout.contentX, layout.contentY + row);
11502
- term.noFormat(" What do you want to do?");
11503
- row += 2;
11504
- for (let i = 0; i < ACTION_CHOICES.length; i++) {
11505
- const choice = ACTION_CHOICES[i];
11506
- if (!choice) {
11507
- continue;
11508
- }
11509
- const pointer = i === selected ? "\u276F" : " ";
11510
- const label = ` ${pointer} ${choice.label}`;
11511
- term.moveTo(layout.contentX, layout.contentY + row);
11512
- if (i === selected) {
11513
- term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
11514
- } else {
11515
- term.noFormat(label);
11516
- }
11517
- row++;
11518
- term.moveTo(layout.contentX, layout.contentY + row);
11519
- term.dim.noFormat(` ${choice.description}`);
11520
- row++;
11521
- }
11522
- row++;
11523
- term.moveTo(layout.contentX, layout.contentY + row);
11524
- term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 f/v jump \xB7 Esc back");
11525
- return layout;
11526
- };
11527
- render();
11528
- term.hideCursor();
11529
- return await new Promise((resolve6) => {
11530
- let resolved = false;
11531
- const cleanup = () => {
11532
- if (resolved) {
11533
- return;
11534
- }
11535
- resolved = true;
11536
- term.off("key", onKey);
11537
- term.off("resize", onResize);
11538
- term.grabInput(false);
11539
- term.hideCursor(false);
11540
- term.moveTo(1, 1).eraseDisplayBelow();
11541
- };
11542
- const finish = (value) => {
11543
- cleanup();
11544
- resolve6(value);
11545
- };
11546
- const onResize = () => {
11547
- if (resolved) {
11548
- return;
11549
- }
11550
- render();
11551
- };
11552
- const onKey = (name, _matches, data) => {
11553
- const input = mapKey(name, data);
11554
- if (!input) {
11555
- return;
11556
- }
11557
- const step = actionPromptStep(selected, input);
11558
- if (step.kind === "cancel") {
11559
- finish("cancel");
11560
- return;
11561
- }
11562
- if (step.kind === "back") {
11563
- finish("back");
11564
- return;
11565
- }
11566
- if (step.kind === "resolve") {
11567
- finish(step.action);
11568
- return;
11569
- }
11570
- if (step.selected !== selected) {
11571
- selected = step.selected;
11572
- render();
11573
- }
11574
- };
11575
- term.grabInput({});
11576
- term.on("key", onKey);
11577
- term.on("resize", onResize);
11578
- });
11579
- }
11580
- function mapKey(name, data) {
11581
- if (name === "UP") {
11582
- return { kind: "up" };
11583
- }
11584
- if (name === "DOWN") {
11585
- return { kind: "down" };
11586
- }
11587
- if (name === "ENTER" || name === "KP_ENTER") {
11588
- return { kind: "enter" };
11589
- }
11590
- if (name === "ESCAPE") {
11591
- return { kind: "back" };
11592
- }
11593
- if (name === "CTRL_C" || name === "CTRL_D") {
11594
- return { kind: "cancel" };
11595
- }
11596
- if (data?.isCharacter) {
11597
- return { kind: "char", ch: name };
11598
- }
11599
- return null;
11600
- }
11601
- function truncate3(s, max) {
11602
- if (max <= 1) {
11603
- return "";
11604
- }
11605
- if (s.length <= max) {
11606
- return s;
11607
- }
11608
- return s.slice(0, Math.max(0, max - 1)) + "\u2026";
11609
- }
11610
- function padRight(s, w) {
11611
- if (s.length >= w) {
11612
- return s.slice(0, w);
11613
- }
11614
- return s + " ".repeat(w - s.length);
11615
- }
11616
- var ACTION_CHOICES;
11617
- var init_import_action_prompt = __esm({
11618
- "src/tui/import-action-prompt.ts"() {
11619
- "use strict";
11620
- init_paths();
11621
- init_session();
11622
- init_prompt_utils();
11623
- ACTION_CHOICES = [
11624
- {
11625
- key: "fork-local",
11626
- label: "Fork locally",
11627
- hotkey: "f",
11628
- description: "spawn a local fork \u2014 original imported copy stays as-is"
11629
- },
11630
- {
11631
- key: "view",
11632
- label: "View transcript",
11633
- hotkey: "v",
11634
- description: "open read-only, no agent spawn"
11635
- }
11636
- ];
11637
- }
11638
- });
11639
-
11640
12504
  // src/tui/clipboard.ts
11641
12505
  import { spawn as nodeSpawn } from "child_process";
11642
12506
  import fs21 from "fs/promises";
@@ -12035,41 +12899,69 @@ function formatEvent(event) {
12035
12899
  return [];
12036
12900
  }
12037
12901
  }
12038
- function applyInlineMarkup(text) {
12902
+ function applyInlineMarkup(text, opts) {
12903
+ const codeOpen = opts?.codeOpen ?? "^C";
12904
+ const boldReset = opts?.boldReset ?? "^:";
12905
+ const codeReset = opts?.codeReset ?? "^:";
12039
12906
  let s = text.replace(/\^/g, "^^");
12040
- s = s.replace(/\*\*(.+?)\*\*/g, "^+$1^:");
12041
- s = s.replace(/`([^`]+)`/g, "^C$1^:");
12907
+ s = s.replace(/\*\*(.+?)\*\*/g, `^+$1${boldReset}`);
12908
+ s = s.replace(/`([^`]+)`/g, `${codeOpen}$1${codeReset}`);
12042
12909
  return s;
12043
12910
  }
12044
- function parseAgentMarkdown(text) {
12911
+ function parseMarkdown(text, opts) {
12912
+ const {
12913
+ proseStyle,
12914
+ highlightCode,
12915
+ prefixStyle,
12916
+ firstPrefix = " ",
12917
+ inlineOpts
12918
+ } = opts;
12045
12919
  const out = [];
12046
12920
  const lines = text.split("\n");
12047
12921
  let inCode = false;
12048
12922
  let codeLang = "";
12049
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
+ };
12050
12937
  const flushCode = () => {
12051
- if (codeBuffer.length === 0) {
12938
+ if (codeBuffer.length === 0)
12052
12939
  return;
12053
- }
12054
- const highlighted = highlightFencedBlock(codeLang, codeBuffer);
12055
- for (const piece of highlighted) {
12056
- const entry = {
12057
- prefix: " ",
12058
- body: piece.body,
12059
- bodyStyle: "code",
12060
- fillRow: true
12061
- };
12062
- if (piece.ansi) {
12063
- 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);
12064
12954
  }
12065
- out.push(entry);
12955
+ } else {
12956
+ for (const cl of codeBuffer)
12957
+ line(cl.replace(/\^/g, "^^"), proseStyle);
12066
12958
  }
12067
12959
  codeBuffer = [];
12068
12960
  codeLang = "";
12069
12961
  };
12070
12962
  for (let i = 0; i < lines.length; i++) {
12071
- const line = lines[i];
12072
- const fence = line.match(/^\s*```\s*(\w*)\s*$/);
12963
+ const l = lines[i];
12964
+ const fence = l.match(/^\s*```\s*(\w*)\s*$/);
12073
12965
  if (fence) {
12074
12966
  if (!inCode) {
12075
12967
  inCode = true;
@@ -12081,68 +12973,81 @@ function parseAgentMarkdown(text) {
12081
12973
  continue;
12082
12974
  }
12083
12975
  if (inCode) {
12084
- codeBuffer.push(line);
12976
+ codeBuffer.push(l);
12085
12977
  continue;
12086
12978
  }
12087
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
12979
+ const heading = l.match(/^(#{1,6})\s+(.*)$/);
12088
12980
  if (heading) {
12089
12981
  const level = heading[1].length;
12090
- const text2 = heading[2] ?? "";
12091
- const style = level === 1 ? "heading-1" : level === 2 ? "heading-2" : "heading-3";
12092
- out.push({
12093
- prefix: " ",
12094
- body: text2,
12095
- bodyStyle: style
12096
- });
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());
12097
12985
  continue;
12098
12986
  }
12099
12987
  const next = lines[i + 1];
12100
- if (line.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(line).length === parseTableRow(next).length) {
12101
- const header = parseTableRow(line);
12988
+ if (l.includes("|") && next !== void 0 && isTableSeparatorLine(next) && parseTableRow(l).length === parseTableRow(next).length) {
12989
+ const header = parseTableRow(l);
12102
12990
  const body = [];
12103
12991
  let j = i + 2;
12104
12992
  while (j < lines.length && lines[j].includes("|")) {
12105
12993
  body.push(parseTableRow(lines[j]));
12106
12994
  j++;
12107
12995
  }
12108
- 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
+ }
12109
13002
  i = j - 1;
12110
13003
  continue;
12111
13004
  }
12112
- const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
13005
+ const bullet = l.match(/^(\s*)[-*+]\s+(.*)$/);
12113
13006
  if (bullet) {
12114
13007
  const indent = bullet[1] ?? "";
12115
- const item = bullet[2] ?? "";
12116
- out.push({
12117
- prefix: " ",
12118
- body: `${indent}\u2022 ${applyInlineMarkup(item)}`,
12119
- bodyStyle: "agent"
12120
- });
13008
+ const item = bullet[2] ?? "";
13009
+ line(
13010
+ `${indent}\u2022 ${applyInlineMarkup(item, inlineOpts)}`,
13011
+ proseStyle,
13012
+ nextPrefix()
13013
+ );
12121
13014
  continue;
12122
13015
  }
12123
- const ordered = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
13016
+ const ordered = l.match(/^(\s*)(\d+)\.\s+(.*)$/);
12124
13017
  if (ordered) {
12125
13018
  const indent = ordered[1] ?? "";
12126
13019
  const num = ordered[2] ?? "";
12127
13020
  const item = ordered[3] ?? "";
12128
- out.push({
12129
- prefix: " ",
12130
- body: `${indent}${num}. ${applyInlineMarkup(item)}`,
12131
- bodyStyle: "agent"
12132
- });
13021
+ line(
13022
+ `${indent}${num}. ${applyInlineMarkup(item, inlineOpts)}`,
13023
+ proseStyle,
13024
+ nextPrefix()
13025
+ );
12133
13026
  continue;
12134
13027
  }
12135
- out.push({
12136
- prefix: " ",
12137
- body: applyInlineMarkup(line),
12138
- bodyStyle: "agent"
12139
- });
13028
+ const isBlank = l.trim() === "";
13029
+ line(
13030
+ applyInlineMarkup(l, inlineOpts),
13031
+ proseStyle,
13032
+ isBlank ? " " : nextPrefix()
13033
+ );
12140
13034
  }
12141
- if (inCode) {
13035
+ if (inCode)
12142
13036
  flushCode();
12143
- }
12144
13037
  return out;
12145
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
+ }
12146
13051
  function parseTableRow(line) {
12147
13052
  let s = line.trim();
12148
13053
  if (s.startsWith("|")) {
@@ -14169,6 +15074,33 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14169
15074
  agentKey = null;
14170
15075
  agentBuffer = "";
14171
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
+ };
14172
15104
  const renderToolsBlock = () => {
14173
15105
  if (toolsBlockStartedAt === null) {
14174
15106
  return;
@@ -14310,6 +15242,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14310
15242
  recordHistoryEntry(event.text);
14311
15243
  }
14312
15244
  closeAgentText();
15245
+ closeThought();
14313
15246
  if (toolsBlockStartedAt !== null) {
14314
15247
  toolsBlockEndedAt = Date.now();
14315
15248
  renderToolsBlock();
@@ -14333,16 +15266,18 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14333
15266
  return;
14334
15267
  }
14335
15268
  if (event.kind === "agent-text") {
15269
+ closeThought();
14336
15270
  appendAgentText(event.text);
14337
15271
  return;
14338
15272
  }
14339
15273
  if (event.kind === "agent-thought") {
14340
15274
  closeAgentText();
14341
- screen.appendStreaming(event.text, "\xB7 ", "thought", "thought");
15275
+ appendThought(event.text);
14342
15276
  return;
14343
15277
  }
14344
15278
  if (event.kind === "exit-plan-mode") {
14345
15279
  closeAgentText();
15280
+ closeThought();
14346
15281
  const existing = exitPlanStates.get(event.toolCallId);
14347
15282
  const merged = {
14348
15283
  plan: event.plan ?? existing?.plan ?? "",
@@ -14360,12 +15295,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14360
15295
  }
14361
15296
  if (event.kind === "tool-call") {
14362
15297
  closeAgentText();
15298
+ closeThought();
14363
15299
  recordToolCall(event.toolCallId, event.title, event.status, void 0);
14364
15300
  renderToolsBlock();
14365
15301
  return;
14366
15302
  }
14367
15303
  if (event.kind === "plan") {
14368
15304
  closeAgentText();
15305
+ closeThought();
14369
15306
  lastPlanEvent = event;
14370
15307
  const lines = formatEvent(event);
14371
15308
  if (lines.length > 0) {
@@ -14375,6 +15312,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14375
15312
  }
14376
15313
  if (event.kind === "tool-call-update") {
14377
15314
  closeAgentText();
15315
+ closeThought();
14378
15316
  recordToolCall(
14379
15317
  event.toolCallId,
14380
15318
  event.title,
@@ -14397,6 +15335,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14397
15335
  if (event.kind === "turn-complete") {
14398
15336
  currentHeadMessageId = void 0;
14399
15337
  closeAgentText();
15338
+ closeThought();
14400
15339
  let effectiveStopReason = event.amended ? "amended" : event.stopReason;
14401
15340
  if (!event.amended && upstreamInterruptedSeen && (effectiveStopReason === void 0 || effectiveStopReason === "end_turn")) {
14402
15341
  effectiveStopReason = "error";
@@ -14491,6 +15430,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14491
15430
  resolve6({ outcome: { outcome: "cancelled" } });
14492
15431
  }
14493
15432
  closeAgentText();
15433
+ closeThought();
14494
15434
  };
14495
15435
  const markToolsBlockRecoveryFailed = () => {
14496
15436
  if (toolsBlockStartedAt === null) {
@@ -14939,6 +15879,9 @@ import { dirname as dirname6, resolve as resolve5 } from "path";
14939
15879
  var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
14940
15880
  "all",
14941
15881
  "detach",
15882
+ "disabled",
15883
+ "follow",
15884
+ "force",
14942
15885
  "foreground",
14943
15886
  "help",
14944
15887
  "info",
@@ -14951,6 +15894,31 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
14951
15894
  "stream",
14952
15895
  "version"
14953
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
+ }
14954
15922
  function parseArgs(argv) {
14955
15923
  const positional = [];
14956
15924
  const flags = {};
@@ -16453,6 +17421,7 @@ var SessionManager = class {
16453
17421
  this.defaultTransformers = options.defaultTransformers ?? [];
16454
17422
  this.logger = options.logger;
16455
17423
  this.npmRegistry = options.npmRegistry;
17424
+ this.extensionCommands = options.extensionCommands;
16456
17425
  }
16457
17426
  registry;
16458
17427
  sessions = /* @__PURE__ */ new Map();
@@ -16471,6 +17440,7 @@ var SessionManager = class {
16471
17440
  metaWriteQueues = /* @__PURE__ */ new Map();
16472
17441
  logger;
16473
17442
  npmRegistry;
17443
+ extensionCommands;
16474
17444
  async create(params) {
16475
17445
  const fresh = await this.bootstrapAgent({
16476
17446
  agentId: params.agentId,
@@ -16524,7 +17494,8 @@ var SessionManager = class {
16524
17494
  agentModes: fresh.initialModes,
16525
17495
  agentModels: fresh.initialModels,
16526
17496
  transformChain: params.transformChain,
16527
- parentSessionId: params.parentSessionId
17497
+ parentSessionId: params.parentSessionId,
17498
+ extensionCommands: this.extensionCommands
16528
17499
  });
16529
17500
  await this.attachManagerHooks(session);
16530
17501
  return session;
@@ -16595,12 +17566,14 @@ var SessionManager = class {
16595
17566
  }
16596
17567
  let loadResult;
16597
17568
  try {
17569
+ const loadMeta = buildSessionLoadMeta(params.agentId, params.currentModel);
16598
17570
  loadResult = await agent.connection.request(
16599
17571
  "session/load",
16600
17572
  {
16601
17573
  sessionId: params.upstreamSessionId,
16602
17574
  cwd: params.cwd,
16603
- mcpServers: []
17575
+ mcpServers: [],
17576
+ ...loadMeta && { _meta: loadMeta }
16604
17577
  }
16605
17578
  );
16606
17579
  } catch (err) {
@@ -16616,7 +17589,10 @@ var SessionManager = class {
16616
17589
  () => void 0
16617
17590
  );
16618
17591
  } else {
16619
- 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
+ );
16620
17596
  }
16621
17597
  const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
16622
17598
  const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
@@ -16634,6 +17610,30 @@ var SessionManager = class {
16634
17610
  this.logger?.info(
16635
17611
  `resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
16636
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
+ }
16637
17637
  const session = new Session({
16638
17638
  sessionId: params.hydraSessionId,
16639
17639
  cwd: params.cwd,
@@ -16650,11 +17650,7 @@ var SessionManager = class {
16650
17650
  listSessions: () => this.list(),
16651
17651
  historyStore: this.histories,
16652
17652
  historyMaxEntries: this.sessionHistoryMaxEntries,
16653
- // Prefer what we previously stored from a current_model_update; if
16654
- // we never captured one (e.g. old opencode sessions on disk before
16655
- // this fix), fall back to the model the agent ships in its
16656
- // session/load response body.
16657
- currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
17653
+ currentModel: effectiveModel,
16658
17654
  currentMode: effectiveMode,
16659
17655
  currentUsage: params.currentUsage,
16660
17656
  agentCommands: params.agentCommands,
@@ -16663,13 +17659,14 @@ var SessionManager = class {
16663
17659
  // snapshot — the proxy's available models can change between daemon
16664
17660
  // restarts (quota resets, rollouts), so meta.json is intentionally
16665
17661
  // treated as a cold fallback here, not the authoritative source.
16666
- agentModels: nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels,
17662
+ agentModels: advertisedModels,
16667
17663
  // Only gate the first-prompt title heuristic when we actually have
16668
17664
  // a title to preserve. A title-less session (lost to a write race
16669
17665
  // or never seeded) should re-derive from the next prompt rather
16670
17666
  // than stay stuck.
16671
17667
  firstPromptSeeded: !!params.title,
16672
- 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
16673
17670
  });
16674
17671
  await this.attachManagerHooks(session);
16675
17672
  return session;
@@ -16688,7 +17685,11 @@ var SessionManager = class {
16688
17685
  cwd,
16689
17686
  agentArgs: params.agentArgs,
16690
17687
  mcpServers: [],
16691
- 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
16692
17693
  });
16693
17694
  const advertisedModes = params.agentModes ?? fresh.initialModes;
16694
17695
  const effectiveMode = await restoreCurrentMode({
@@ -16699,6 +17700,15 @@ var SessionManager = class {
16699
17700
  advertisedModes,
16700
17701
  logger: this.logger
16701
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");
16702
17712
  const session = new Session({
16703
17713
  sessionId: params.hydraSessionId,
16704
17714
  cwd,
@@ -16715,16 +17725,15 @@ var SessionManager = class {
16715
17725
  listSessions: () => this.list(),
16716
17726
  historyStore: this.histories,
16717
17727
  historyMaxEntries: this.sessionHistoryMaxEntries,
16718
- // Prefer the stored value (set by a previous current_model_update);
16719
- // fall back to whatever the agent ships in its session/new response.
16720
- currentModel: params.currentModel ?? fresh.initialModel,
17728
+ currentModel: effectiveModel,
16721
17729
  currentMode: effectiveMode,
16722
17730
  currentUsage: params.currentUsage,
16723
17731
  agentCommands: params.agentCommands,
16724
17732
  agentModes: advertisedModes,
16725
- agentModels: params.agentModels ?? fresh.initialModels,
17733
+ agentModels: advertisedModels,
16726
17734
  firstPromptSeeded: !!params.title,
16727
- 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
16728
17737
  });
16729
17738
  await this.attachManagerHooks(session);
16730
17739
  void session.seedFromImport().catch(() => void 0);
@@ -17576,6 +18585,13 @@ function usageSnapshotToPersisted(usage) {
17576
18585
  function persistedUsageToSnapshot(usage) {
17577
18586
  return usage ? { ...usage } : void 0;
17578
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
+ }
17579
18595
  function extractInitialModel(result) {
17580
18596
  const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
17581
18597
  if (direct) {
@@ -17755,6 +18771,33 @@ async function restoreCurrentMode(opts) {
17755
18771
  return agentReportedMode;
17756
18772
  }
17757
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
+ }
17758
18801
  function parseModesList(list) {
17759
18802
  if (!Array.isArray(list)) {
17760
18803
  return [];
@@ -18710,6 +19753,55 @@ function withCode3(err, code) {
18710
19753
  return err;
18711
19754
  }
18712
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
+
18713
19805
  // src/daemon/server.ts
18714
19806
  init_paths();
18715
19807
 
@@ -19474,6 +20566,379 @@ function formatNumber(n) {
19474
20566
  init_types();
19475
20567
  init_hydra_version();
19476
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
19477
20942
  function resolveHydraHost(defaults) {
19478
20943
  if (defaults.publicHost && defaults.publicHost.length > 0) {
19479
20944
  return defaults.publicHost;
@@ -19489,6 +20954,17 @@ function registerSessionRoutes(app, manager, defaults) {
19489
20954
  const sessions = await manager.list({ cwd: query?.cwd });
19490
20955
  return { sessions };
19491
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
+ });
19492
20968
  app.post("/v1/sessions", async (request, reply) => {
19493
20969
  const body = request.body ?? {};
19494
20970
  const cwd = expandHome(body.cwd ?? defaults.cwd);
@@ -20276,6 +21752,34 @@ function registerAcpWsEndpoint(app, deps) {
20276
21752
  }
20277
21753
  return buildInitializeResult();
20278
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
+ }
20279
21783
  if (processIdentity?.kind === "transformer") {
20280
21784
  connection.onRequest("transformer/initialize", async (raw) => {
20281
21785
  const params = raw ?? {};
@@ -20823,7 +22327,10 @@ function registerAcpWsEndpoint(app, deps) {
20823
22327
  return null;
20824
22328
  }
20825
22329
  app.log.info(decision.logMessage);
20826
- 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;
20827
22334
  });
20828
22335
  connection.onRequest("session/set_mode", async (rawParams) => {
20829
22336
  const params = rawParams;
@@ -21214,13 +22721,15 @@ async function startDaemon(config, serviceToken) {
21214
22721
  stderrTailBytes: config.daemon.agentStderrTailBytes,
21215
22722
  logger: agentLogger
21216
22723
  });
22724
+ const extensionCommands = new ExtensionCommandRegistry();
21217
22725
  const manager = new SessionManager(registry, spawner, void 0, {
21218
22726
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
21219
22727
  defaultModels: config.defaultModels,
21220
22728
  defaultTransformers: config.defaultTransformers,
21221
22729
  sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
21222
22730
  logger: agentLogger,
21223
- npmRegistry: config.npmRegistry
22731
+ npmRegistry: config.npmRegistry,
22732
+ extensionCommands
21224
22733
  });
21225
22734
  const extensions = new ExtensionManager(extensionList(config), void 0, {
21226
22735
  tokenRegistry: processRegistry
@@ -21254,7 +22763,8 @@ async function startDaemon(config, serviceToken) {
21254
22763
  processRegistry,
21255
22764
  onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
21256
22765
  onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
21257
- transformers
22766
+ transformers,
22767
+ extensionCommands
21258
22768
  });
21259
22769
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
21260
22770
  const address = app.server.address();
@@ -24305,6 +25815,7 @@ async function main() {
24305
25815
  const positionalAgentId = afterLaunch[0];
24306
25816
  const agentArgs = afterLaunch.slice(1);
24307
25817
  const { flags: flags2 } = parseArgs(beforeLaunch);
25818
+ rejectUnknownFlags(flags2);
24308
25819
  if (flags2.reattach === true) {
24309
25820
  process.stderr.write(
24310
25821
  "hydra-acp launch: --reattach is not valid here. Pass --session <id-or-url> to attach to a specific session.\n"
@@ -24343,6 +25854,7 @@ async function main() {
24343
25854
  return;
24344
25855
  }
24345
25856
  const { positional, flags } = parseArgs(argv);
25857
+ rejectUnknownFlags(flags);
24346
25858
  if (flags.version === true || positional[0] === "--version") {
24347
25859
  process.stdout.write(`hydra-acp ${readVersion()}
24348
25860
  `);
@@ -24730,6 +26242,17 @@ function parseNumericFlag(flags, name) {
24730
26242
  }
24731
26243
  return void 0;
24732
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
+ }
24733
26256
  function readShortPrompt(argv) {
24734
26257
  for (let i = 0; i < argv.length; i += 1) {
24735
26258
  const tok = argv[i];