@mrciphersmith/keryx 0.3.56 → 0.3.57

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.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/dist/cli.js +1597 -87
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -20104,7 +20104,7 @@ var package_default;
20104
20104
  var init_package = __esm(() => {
20105
20105
  package_default = {
20106
20106
  name: "@mrciphersmith/keryx",
20107
- version: "0.3.56",
20107
+ version: "0.3.57",
20108
20108
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
20109
20109
  private: false,
20110
20110
  publishConfig: {
@@ -20152,7 +20152,7 @@ var init_package = __esm(() => {
20152
20152
  "test:core": "bun test src/cli src/core src/impact-evidence/ src/shell-source-audits.test.ts src/acp/ src/assets/ src/bundle/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/governance/ src/product/ src/health/ src/integrations/ src/job/ src/learning/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/rules/ src/sac/ src/security/ src/stack/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/ scripts/",
20153
20153
  "test:client:terminal": "bun test src/tui/ src/commands/shell",
20154
20154
  "test:client:streaming": "bun test src/harness/provider/",
20155
- "test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/rewind/ src/remote/ src/commands/sessions",
20155
+ "test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/rewind/ src/remote/ src/docs/ src/commands/sessions",
20156
20156
  "test:client:runtime": "bun test src/harness/ src/mcp-client/ src/mcp-servers/ src/agents/ src/commands/agent src/commands/harness src/commands/providers",
20157
20157
  "test:client": "bun run test:client:terminal && bun run test:client:streaming && bun run test:client:cancel-resume && bun run test:client:runtime",
20158
20158
  check: "bun run lint && bun run typecheck && bun run typecheck:scripts && bun test",
@@ -111379,6 +111379,19 @@ function parseApprovalCallback(data) {
111379
111379
  }
111380
111380
  return { approvalId: match[1], decision: match[2] };
111381
111381
  }
111382
+ function choiceCallbackData(promptId, index) {
111383
+ return `pk:${promptId}:${index}`;
111384
+ }
111385
+ function parseChoiceCallback(data) {
111386
+ const match = CHOICE_CALLBACK_PATTERN.exec(data);
111387
+ if (match === null) {
111388
+ return;
111389
+ }
111390
+ return { promptId: match[1], index: Number(match[2]) };
111391
+ }
111392
+ function isMessageState(value) {
111393
+ return typeof value === "string" && MESSAGE_STATES.includes(value);
111394
+ }
111382
111395
  function encodeSseEvent(name, data, id) {
111383
111396
  return `event: ${name}
111384
111397
  ${id === undefined ? "" : `id: ${id}
@@ -111428,7 +111441,7 @@ class SseParser {
111428
111441
  return frames;
111429
111442
  }
111430
111443
  }
111431
- var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, CHANNELS_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", SSE_KEEPALIVE_FRAME = `: keepalive
111444
+ var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, CHANNELS_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", CHOICE_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
111432
111445
 
111433
111446
  `;
111434
111447
  var init_protocol2 = __esm(() => {
@@ -111438,6 +111451,8 @@ var init_protocol2 = __esm(() => {
111438
111451
  heartbeat: "POST",
111439
111452
  reply: "POST",
111440
111453
  approval: "POST",
111454
+ prompt: "POST",
111455
+ state: "POST",
111441
111456
  ack: "POST",
111442
111457
  stream: "GET"
111443
111458
  };
@@ -111455,6 +111470,8 @@ var init_protocol2 = __esm(() => {
111455
111470
  MAX_APPROVAL_TIMEOUT_MS = 60 * 60000;
111456
111471
  SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
111457
111472
  APPROVAL_CALLBACK_PATTERN = /^ap:(ap[0-9a-f]{12}):(allow|deny)$/;
111473
+ CHOICE_CALLBACK_PATTERN = /^pk:(pk[0-9a-f]{12}):(\d{1,3})$/;
111474
+ MESSAGE_STATES = ["reading", "working", "done", "failed"];
111458
111475
  });
111459
111476
 
111460
111477
  // src/remote/paths.ts
@@ -111835,6 +111852,8 @@ class RemoteClient {
111835
111852
  inboundChain = Promise.resolve();
111836
111853
  waiters = new Map;
111837
111854
  earlyDecisions = new Map;
111855
+ choiceWaiters = new Map;
111856
+ earlyChoices = new Map;
111838
111857
  firstResult;
111839
111858
  name;
111840
111859
  threadId;
@@ -111950,6 +111969,45 @@ class RemoteClient {
111950
111969
  this.waiters.set(approvalId, { resolve, timer });
111951
111970
  });
111952
111971
  }
111972
+ async requestChoice(text, rows, timeoutMs, forUserId) {
111973
+ if (!this.connected) {
111974
+ return;
111975
+ }
111976
+ const body = { sessionId: this.options.sessionId, text, rows, timeoutMs, ...forUserId === undefined ? {} : { forUserId } };
111977
+ let promptId;
111978
+ try {
111979
+ const response = await this.post("prompt", body);
111980
+ if (!response.ok) {
111981
+ return;
111982
+ }
111983
+ promptId = (await response.json()).promptId;
111984
+ } catch {
111985
+ return;
111986
+ }
111987
+ const early = this.earlyChoices.get(promptId);
111988
+ if (early !== undefined) {
111989
+ this.earlyChoices.delete(promptId);
111990
+ return early;
111991
+ }
111992
+ if (!this.connected) {
111993
+ return;
111994
+ }
111995
+ return new Promise((resolve) => {
111996
+ const timer = setTimeout(() => {
111997
+ this.choiceWaiters.delete(promptId);
111998
+ resolve(undefined);
111999
+ }, timeoutMs);
112000
+ this.choiceWaiters.set(promptId, { resolve, timer });
112001
+ });
112002
+ }
112003
+ async reportState(updateId, state) {
112004
+ if (!this.connected) {
112005
+ return;
112006
+ }
112007
+ try {
112008
+ await this.post("state", { sessionId: this.options.sessionId, updateId, state });
112009
+ } catch {}
112010
+ }
111953
112011
  target(route, query = "") {
111954
112012
  const token = readShellToken(this.options.dir);
111955
112013
  const endpoint = readEndpoint(this.options.dir);
@@ -112142,6 +112200,9 @@ class RemoteClient {
112142
112200
  } else if (event === "approval") {
112143
112201
  const approval = parsed;
112144
112202
  this.resolveApproval(approval.approvalId, approval.decision);
112203
+ } else if (event === "choice") {
112204
+ const choice = parsed;
112205
+ this.resolveChoice(choice.promptId, choice.index);
112145
112206
  } else if (event === "callback") {
112146
112207
  const callback = parsed;
112147
112208
  if (this.options.onCallback !== undefined) {
@@ -112199,6 +112260,30 @@ class RemoteClient {
112199
112260
  this.waiters.delete(approvalId);
112200
112261
  waiter.resolve(decision);
112201
112262
  }
112263
+ resolveChoice(promptId, index) {
112264
+ const waiter = this.choiceWaiters.get(promptId);
112265
+ if (waiter === undefined) {
112266
+ if (this.earlyChoices.size >= MAX_EARLY_DECISIONS) {
112267
+ const oldest = this.earlyChoices.keys().next().value;
112268
+ if (oldest !== undefined) {
112269
+ this.earlyChoices.delete(oldest);
112270
+ }
112271
+ }
112272
+ this.earlyChoices.set(promptId, index);
112273
+ return;
112274
+ }
112275
+ clearTimeout(waiter.timer);
112276
+ this.choiceWaiters.delete(promptId);
112277
+ waiter.resolve(index);
112278
+ }
112279
+ failChoices() {
112280
+ for (const [id, waiter] of [...this.choiceWaiters.entries()]) {
112281
+ clearTimeout(waiter.timer);
112282
+ this.choiceWaiters.delete(id);
112283
+ waiter.resolve(undefined);
112284
+ }
112285
+ this.earlyChoices.clear();
112286
+ }
112202
112287
  failApprovals() {
112203
112288
  for (const [id, waiter] of [...this.waiters.entries()]) {
112204
112289
  clearTimeout(waiter.timer);
@@ -112206,6 +112291,7 @@ class RemoteClient {
112206
112291
  waiter.resolve("deny");
112207
112292
  }
112208
112293
  this.earlyDecisions.clear();
112294
+ this.failChoices();
112209
112295
  }
112210
112296
  }
112211
112297
  var UNVERIFIED_SERVE_MESSAGE = "the program answering on keryx serve's port did not prove it is the keryx serve this shell trusts; its answer was ignored. Restart `keryx serve` (an older serve cannot prove itself; update keryx on both sides)", FatalClientError, TransientClientError, DEFAULT_BACKOFF_INITIAL_MS = 250, DEFAULT_BACKOFF_MAX_MS = 15000, DEFAULT_REQUEST_TIMEOUT_MS = 1e4, MAX_EARLY_DECISIONS = 32, MAX_COMPLETED_IDS = 256;
@@ -112226,6 +112312,539 @@ var init_client3 = __esm(() => {
112226
112312
  };
112227
112313
  });
112228
112314
 
112315
+ // src/remote/command-gateway.ts
112316
+ function menuName(name) {
112317
+ return name.replace(/-/g, "_");
112318
+ }
112319
+ function split(line) {
112320
+ const trimmed = line.trim();
112321
+ const space = trimmed.search(/\s/);
112322
+ const head = space === -1 ? trimmed : trimmed.slice(0, space);
112323
+ const args = space === -1 ? "" : trimmed.slice(space).trim();
112324
+ const at = head.indexOf("@");
112325
+ const bare = (at === -1 ? head : head.slice(0, at)).replace(/^\/+/, "").toLowerCase();
112326
+ return { token: bare, args };
112327
+ }
112328
+ function refuse3(command, reason, known) {
112329
+ return { kind: "refuse", command, reason, known };
112330
+ }
112331
+ function refusalText(command, reason) {
112332
+ return `Not available remotely: /${command}. ${reason.charAt(0).toUpperCase()}${reason.slice(1)}${/[.!?]$/.test(reason) ? "" : "."}`;
112333
+ }
112334
+ function classifyRemoteCommand(line) {
112335
+ const { token, args } = split(line);
112336
+ if (token.length === 0) {
112337
+ return refuse3("", "an empty command", false);
112338
+ }
112339
+ const canonical = token.replace(/_/g, "-");
112340
+ const refusedReason = REMOTE_REFUSED[canonical] ?? REMOTE_REFUSED[token];
112341
+ const spec = BY_NAME.get(canonical) ?? BY_MENU.get(token);
112342
+ if (canonical === "remote-control") {
112343
+ const sub = args.split(/\s+/)[0]?.toLowerCase() ?? "";
112344
+ return refuse3(canonical, sub === "off" ? "it would delete this topic; turn it off in the shell" : refusedReason ?? "its panel is local", true);
112345
+ }
112346
+ if (spec === undefined) {
112347
+ if (refusedReason !== undefined) {
112348
+ return refuse3(canonical, refusedReason, true);
112349
+ }
112350
+ return refuse3(canonical, "it is not on the list of commands a topic may run; /help shows the list", false);
112351
+ }
112352
+ const name = spec.name;
112353
+ const rest = args.trim();
112354
+ const first = rest.split(/\s+/)[0]?.toLowerCase() ?? "";
112355
+ const asTyped = rest.length === 0 ? `/${name}` : `/${name} ${rest}`;
112356
+ switch (name) {
112357
+ case "mode":
112358
+ if (first === "trust" || first === "auto") {
112359
+ return { kind: "confirm", command: name, line: `/mode ${first}`, summary: `Switch the permission mode to ${first}?` };
112360
+ }
112361
+ if (first === "" || first === "ask") {
112362
+ return { kind: "run", command: name, line: asTyped };
112363
+ }
112364
+ return refuse3(name, "the mode is ask, trust or auto", true);
112365
+ case "plan":
112366
+ if (first === "off") {
112367
+ return { kind: "confirm", command: name, line: "/plan off", summary: "Turn read-only mode off?" };
112368
+ }
112369
+ if (first === "") {
112370
+ return { kind: "run", command: name, line: "/plan" };
112371
+ }
112372
+ return first === "on" ? { kind: "run", command: name, line: "/plan on" } : refuse3(name, "use /plan on or /plan off", true);
112373
+ case "theme":
112374
+ return first === "" ? refuse3(name, "bare /theme opens a picker in the shell; use /theme <name>", true) : { kind: "run", command: name, line: asTyped };
112375
+ case "think":
112376
+ return first === "auto" || first === "expand" || first === "hide" ? { kind: "run", command: name, line: `/think ${first}` } : refuse3(name, "bare /think and /think collapse only flip the view in the shell; use /think auto, expand or hide", true);
112377
+ case "delegate":
112378
+ return confirmDelegate(rest);
112379
+ case "external":
112380
+ if (first === "on" || first === "off") {
112381
+ return {
112382
+ kind: "confirm",
112383
+ command: name,
112384
+ line: `/external ${first}`,
112385
+ summary: `Switch sending private work to external providers ${first}? This changes what leaves this machine.`
112386
+ };
112387
+ }
112388
+ return first === "" ? { kind: "run", command: name, line: "/external" } : refuse3(name, "use /external, /external on or /external off", true);
112389
+ case "external-agents":
112390
+ if (first === "on" || first === "off") {
112391
+ return {
112392
+ kind: "confirm",
112393
+ command: name,
112394
+ line: `/external-agents ${first}`,
112395
+ summary: `Turn the external agent runtime ${first}? External agents are paid and run outside keryx.`
112396
+ };
112397
+ }
112398
+ return first === "" ? { kind: "run", command: name, line: "/external-agents" } : refuse3(name, "use /external_agents, /external_agents on or /external_agents off", true);
112399
+ default:
112400
+ break;
112401
+ }
112402
+ switch (spec.kind) {
112403
+ case "builtin":
112404
+ return { kind: "builtin", command: name, args: rest };
112405
+ case "picker":
112406
+ return { kind: "picker", command: name, args: rest };
112407
+ default:
112408
+ return { kind: "run", command: name, line: name === "new" || name === "clear" ? `/${name}` : asTyped };
112409
+ }
112410
+ }
112411
+ function confirmDelegate(rest) {
112412
+ const match = /^(\S+)\s+([\s\S]+)$/.exec(rest);
112413
+ if (match === null) {
112414
+ return refuse3("delegate", "it needs an agent and a task: /delegate <agent> <task>", true);
112415
+ }
112416
+ const agent = (match[1] ?? "").toLowerCase();
112417
+ const task = (match[2] ?? "").trim();
112418
+ const shown = task.length > 200 ? `${task.slice(0, 200)}... [shortened for this question: the full task is ${task.length} characters and runs as typed]` : task;
112419
+ return {
112420
+ kind: "confirm",
112421
+ command: "delegate",
112422
+ line: `/delegate ${match[1] ?? ""} ${task}`,
112423
+ agent,
112424
+ summary: `Hand this task to ${agent}? It is an external agent and runs outside keryx; the run is paid.
112425
+ Task: ${shown}`
112426
+ };
112427
+ }
112428
+ function remoteMenu() {
112429
+ return REMOTE_COMMANDS.map((spec) => ({ command: menuName(spec.name), description: spec.description.slice(0, 256) }));
112430
+ }
112431
+ function remoteHelpText() {
112432
+ const lines = ["Commands you can use from this topic:"];
112433
+ for (const spec of REMOTE_COMMANDS) {
112434
+ lines.push(`/${menuName(spec.name)} - ${spec.description}`);
112435
+ }
112436
+ lines.push("", "Anything else typed here goes to the model as a message. Other commands run in the shell.");
112437
+ return lines.join(`
112438
+ `);
112439
+ }
112440
+ function isBusyDeferred(name) {
112441
+ return BUSY_DEFERRED_COMMANDS.includes(name);
112442
+ }
112443
+ var REMOTE_COMMANDS, REMOTE_REFUSED, BUSY_DEFERRED_COMMANDS, BUSY_REASON = "main is busy: command deferred", BY_NAME, BY_MENU;
112444
+ var init_command_gateway = __esm(() => {
112445
+ REMOTE_COMMANDS = [
112446
+ { name: "help", kind: "builtin", description: "List the commands you can use from this topic" },
112447
+ { name: "status", kind: "text", description: "Show the session: model, mode, queue, usage" },
112448
+ { name: "doctor", kind: "text", description: "Run the health checks" },
112449
+ { name: "model", kind: "picker", description: "Pick another model with buttons" },
112450
+ { name: "connect", kind: "picker", description: "Switch to a connected provider with buttons" },
112451
+ { name: "resume", kind: "picker", description: "Return to an earlier session with buttons" },
112452
+ { name: "sessions", kind: "builtin", description: "List recent sessions" },
112453
+ { name: "new", kind: "text", description: "Start a new session; this topic stays bound to it" },
112454
+ { name: "clear", kind: "text", description: "Same as /new" },
112455
+ { name: "compact", kind: "text", description: "Compact the conversation: /compact [focus]" },
112456
+ { name: "think", kind: "text", description: "Set how reasoning shows: /think <auto|expand|hide>" },
112457
+ { name: "goal", kind: "text", description: "Show or set the session goal" },
112458
+ { name: "queue", kind: "text", description: "Manage the queue: /queue <remove|edit|force> [N]" },
112459
+ { name: "mode", kind: "confirm", description: "Show or set the permission mode; trust and auto ask for a button press" },
112460
+ { name: "plan", kind: "confirm", description: "Read-only mode: /plan [on|off]; bare shows it, off asks for a button press" },
112461
+ { name: "reasoning", kind: "text", description: "Show or set reasoning effort" },
112462
+ { name: "theme", kind: "text", description: "Set the theme: /theme <name>" },
112463
+ { name: "jevrules", kind: "text", description: "Show the Jev rules" },
112464
+ { name: "staledocs", kind: "text", description: "Show stale documents" },
112465
+ { name: "opencomments", kind: "text", description: "Show open review comments" },
112466
+ { name: "contract", kind: "text", description: "Show the contract check" },
112467
+ { name: "triage", kind: "text", description: "Advisory triage of the latest review package" },
112468
+ { name: "risk", kind: "text", description: "Risk map of the working diff" },
112469
+ { name: "scenarios", kind: "text", description: "User scenarios the working diff likely changes" },
112470
+ { name: "delegate", kind: "confirm", description: "Hand a paid task to an external agent: /delegate <agent> <task>; asks first" },
112471
+ { name: "external", kind: "confirm", description: "Show or switch sending work to external providers; on and off ask first" },
112472
+ { name: "external-agents", kind: "confirm", description: "Show or switch the external agent runtime; on and off ask first" }
112473
+ ];
112474
+ REMOTE_REFUSED = {
112475
+ exit: "it would close the shell",
112476
+ quit: "it would close the shell",
112477
+ channels: "it manages the Telegram connection itself",
112478
+ provider: "it asks for provider keys and addresses, which are never typed into a chat",
112479
+ "search-provider": "it asks for provider keys, which are never typed into a chat",
112480
+ "search-connect": "it changes where searches are sent",
112481
+ "remote-control": "turning remote control off would delete this topic, and its panel is local",
112482
+ integrate: "it edits other tools' configuration on this machine",
112483
+ copy: "it uses this machine's clipboard",
112484
+ game: "it needs the shell's screen",
112485
+ setup: "it asks for keys and changes the configuration",
112486
+ mcp: "it can trust tools and change server connections; use the shell",
112487
+ guard: "it changes a safety guard; use the shell",
112488
+ route: "it changes where work is routed; use the shell",
112489
+ editguard: "it changes a safety guard; use the shell",
112490
+ settings: "it edits settings, some of them secret; use the shell",
112491
+ bus: "it talks to other agents on this machine",
112492
+ review: "it needs the shell's screen",
112493
+ reviews: "it needs the shell's screen",
112494
+ product: "it needs the shell's screen",
112495
+ governance: "it needs the shell's screen",
112496
+ expand: "it needs the shell's screen",
112497
+ interrupt: "use the Stop button on a message, or stop it in the shell",
112498
+ demote: "it works on the shell's own tasks",
112499
+ models: "use /model",
112500
+ "external-diff": "listing and applying external patches stays in the shell, where the patch hash is typed",
112501
+ flows: "it opens a panel in the shell",
112502
+ ac: "it opens a panel in the shell",
112503
+ workspace: "it opens a panel in the shell",
112504
+ approvals: "it opens a panel in the shell",
112505
+ triggers: "it opens a panel in the shell",
112506
+ routing: "it opens a panel in the shell",
112507
+ schedules: "it opens a panel in the shell",
112508
+ schedule: "it opens a form in the shell",
112509
+ rewind: "it opens a picker of earlier points in the shell",
112510
+ conform: "it opens a picker in the shell",
112511
+ ci: "it opens a panel in the shell",
112512
+ jevprofile: "it opens a panel with toggles in the shell"
112513
+ };
112514
+ BUSY_DEFERRED_COMMANDS = ["new", "clear", "resume", "sessions", "compact", "model", "connect"];
112515
+ BY_NAME = new Map(REMOTE_COMMANDS.map((spec) => [spec.name, spec]));
112516
+ BY_MENU = new Map(REMOTE_COMMANDS.map((spec) => [menuName(spec.name), spec]));
112517
+ });
112518
+
112519
+ // src/remote/command-router.ts
112520
+ function ended(end, result) {
112521
+ try {
112522
+ end?.(result);
112523
+ } catch {}
112524
+ }
112525
+
112526
+ class RemoteCommandRouter {
112527
+ ctx;
112528
+ chain = Promise.resolve();
112529
+ detached = 0;
112530
+ detachedDone = [];
112531
+ constructor(ctx) {
112532
+ this.ctx = ctx;
112533
+ }
112534
+ handle(line, fromId, end) {
112535
+ const decision = classifyRemoteCommand(line);
112536
+ switch (decision.kind) {
112537
+ case "refuse":
112538
+ this.refuse(decision);
112539
+ ended(end, "failed");
112540
+ return;
112541
+ case "builtin":
112542
+ this.ctx.record(`/${decision.command}`);
112543
+ if (decision.command === "sessions") {
112544
+ this.detach(async () => {
112545
+ await this.listSessionsText();
112546
+ ended(end, "done");
112547
+ });
112548
+ } else {
112549
+ this.ctx.reply(this.ctx.compose(remoteHelpText())).catch(() => false);
112550
+ ended(end, "done");
112551
+ }
112552
+ return;
112553
+ case "run":
112554
+ this.enqueue(decision.line, decision.command, end);
112555
+ return;
112556
+ case "picker":
112557
+ this.detach(async () => {
112558
+ const result = await this.picker(decision.command, fromId, end);
112559
+ if (result !== "deferred") {
112560
+ ended(end, result);
112561
+ }
112562
+ });
112563
+ return;
112564
+ case "confirm":
112565
+ this.detach(async () => {
112566
+ const result = await this.confirm(decision, fromId, end);
112567
+ if (result !== "deferred") {
112568
+ ended(end, result);
112569
+ }
112570
+ });
112571
+ return;
112572
+ }
112573
+ }
112574
+ async idle() {
112575
+ await this.chain;
112576
+ if (this.detached > 0) {
112577
+ await new Promise((resolve) => this.detachedDone.push(resolve));
112578
+ await this.idle();
112579
+ }
112580
+ }
112581
+ detach(work) {
112582
+ this.detached += 1;
112583
+ work().catch(() => {
112584
+ return;
112585
+ }).finally(() => {
112586
+ this.detached -= 1;
112587
+ if (this.detached === 0) {
112588
+ for (const done of this.detachedDone.splice(0)) {
112589
+ done();
112590
+ }
112591
+ }
112592
+ });
112593
+ }
112594
+ refuse(decision) {
112595
+ const shown = decision.command.length === 0 ? "/" : `/${decision.command}`;
112596
+ this.ctx.record(`refused ${shown}: ${decision.reason}`);
112597
+ this.ctx.reply(refusalText(decision.command, decision.reason)).catch(() => false);
112598
+ }
112599
+ enqueue(line, command, end) {
112600
+ this.chain = this.chain.then(() => this.execute(command, line, async () => this.ctx.host.runCommand === undefined ? undefined : this.ctx.host.runCommand(line), end)).catch(() => {
112601
+ return;
112602
+ });
112603
+ }
112604
+ busyReason(line, command) {
112605
+ if (!this.ctx.host.isBusy()) {
112606
+ return;
112607
+ }
112608
+ const fromHost = this.ctx.host.busyRefusal?.(line);
112609
+ if (fromHost !== undefined) {
112610
+ return fromHost;
112611
+ }
112612
+ return this.ctx.host.busyRefusal === undefined && isBusyDeferred(command) ? BUSY_REASON : undefined;
112613
+ }
112614
+ async execute(command, line, work, end) {
112615
+ const { limits } = this.ctx;
112616
+ const busy = this.busyReason(line, command);
112617
+ if (busy !== undefined) {
112618
+ this.ctx.record(`/${command} not run: ${busy}`);
112619
+ ended(end, "failed");
112620
+ await this.ctx.reply(`/${command} was not run: ${busy}.`).catch(() => false);
112621
+ return;
112622
+ }
112623
+ this.ctx.record(`/${command} started`);
112624
+ const holds = SESSION_COMMANDS.includes(command);
112625
+ if (holds) {
112626
+ this.ctx.holdTopic(true);
112627
+ }
112628
+ let finished = false;
112629
+ const notice = setTimeout(() => {
112630
+ if (!finished) {
112631
+ this.ctx.reply(`Still running /${command}...`).catch(() => false);
112632
+ }
112633
+ }, limits.runningNoticeMs);
112634
+ let limitTimer;
112635
+ const limit = new Promise((resolve) => {
112636
+ limitTimer = setTimeout(() => resolve("limit"), limits.commandLimitMs);
112637
+ });
112638
+ let outcome;
112639
+ try {
112640
+ outcome = await Promise.race([work(), limit]);
112641
+ } catch (error) {
112642
+ outcome = { output: error instanceof Error ? error.message : String(error), ok: false };
112643
+ } finally {
112644
+ finished = true;
112645
+ clearTimeout(notice);
112646
+ if (limitTimer !== undefined) {
112647
+ clearTimeout(limitTimer);
112648
+ }
112649
+ if (holds) {
112650
+ this.ctx.holdTopic(false);
112651
+ }
112652
+ }
112653
+ if (outcome === "limit") {
112654
+ this.ctx.record(`/${command} still running: stopped waiting after the time limit`);
112655
+ ended(end, "failed");
112656
+ await this.ctx.reply(`/${command} is still running in the shell. Stopped waiting for it after ${Math.max(1, Math.round(limits.commandLimitMs / 1000))} seconds.`).catch(() => false);
112657
+ return;
112658
+ }
112659
+ if (outcome === undefined) {
112660
+ this.ctx.record(`/${command} not run: this shell cannot run it from the topic`);
112661
+ ended(end, "failed");
112662
+ await this.ctx.reply(`/${command} was not run: this shell cannot run it from the topic.`).catch(() => false);
112663
+ return;
112664
+ }
112665
+ this.ctx.record(`/${command} ${outcome.ok ? "done" : "failed"}`);
112666
+ ended(end, outcome.ok ? "done" : "failed");
112667
+ if (this.ctx.takeSwitched()) {
112668
+ return;
112669
+ }
112670
+ const text = outcome.output.trim();
112671
+ if (text.length === 0) {
112672
+ await this.ctx.reply(outcome.ok ? `/${command}: done. It printed nothing.` : `/${command} failed. The details are in the shell.`).catch(() => false);
112673
+ return;
112674
+ }
112675
+ await this.ctx.reply(this.ctx.compose(outcome.ok ? text : `/${command} failed:
112676
+ ${text}`)).catch(() => false);
112677
+ }
112678
+ async confirm(decision, fromId, end) {
112679
+ const yes = decision.agent !== undefined ? `Yes, send to ${decision.agent} (paid)` : "Yes";
112680
+ this.ctx.record(`/${decision.command} waiting for a press in the topic`);
112681
+ const index = await this.ctx.choose(this.ctx.compose(decision.summary), [[this.label(yes), "No"]], this.ctx.limits.choiceTimeoutMs, fromId);
112682
+ if (index === 0) {
112683
+ this.ctx.record(`/${decision.command} confirmed in the topic`);
112684
+ this.enqueue(decision.line, decision.command, end);
112685
+ return "deferred";
112686
+ }
112687
+ this.ctx.record(index === 1 ? `/${decision.command} declined in the topic; nothing changed` : `/${decision.command} not confirmed in time; nothing changed`);
112688
+ return index === 1 ? "done" : "failed";
112689
+ }
112690
+ label(text) {
112691
+ const one = this.ctx.compose(text).replace(/\s+/g, " ").trim();
112692
+ const shown = one.length === 0 ? "(unnamed)" : one;
112693
+ return shown.length > LABEL_CHARS ? `${shown.slice(0, LABEL_CHARS - 3)}...` : shown;
112694
+ }
112695
+ async pick(title, options, fromId) {
112696
+ const pages = Math.max(1, Math.ceil(options.length / PICKER_PAGE_SIZE));
112697
+ let page = 0;
112698
+ for (;; ) {
112699
+ const slice = options.slice(page * PICKER_PAGE_SIZE, (page + 1) * PICKER_PAGE_SIZE);
112700
+ const rows = slice.map((option) => [this.label(option.label)]);
112701
+ const nav = [];
112702
+ if (page > 0) {
112703
+ nav.push("Previous");
112704
+ }
112705
+ if (page < pages - 1) {
112706
+ nav.push("More");
112707
+ }
112708
+ if (nav.length > 0) {
112709
+ rows.push([...nav]);
112710
+ }
112711
+ const text = pages > 1 ? `${title}
112712
+ Page ${page + 1} of ${pages}` : title;
112713
+ const index = await this.ctx.choose(this.ctx.compose(text), rows, this.ctx.limits.choiceTimeoutMs, fromId);
112714
+ if (index === undefined) {
112715
+ return;
112716
+ }
112717
+ if (index < slice.length) {
112718
+ return slice[index];
112719
+ }
112720
+ const pressed = nav[index - slice.length];
112721
+ page += pressed === "More" ? 1 : -1;
112722
+ }
112723
+ }
112724
+ async picker(command, fromId, end) {
112725
+ const line = `/${command}`;
112726
+ const busy = this.busyReason(line, command);
112727
+ if (busy !== undefined) {
112728
+ this.ctx.record(`${line} not run: ${busy}`);
112729
+ await this.ctx.reply(`${line} was not run: ${busy}.`).catch(() => false);
112730
+ return "failed";
112731
+ }
112732
+ this.ctx.record(line);
112733
+ switch (command) {
112734
+ case "model":
112735
+ return await this.pickModel(undefined, fromId);
112736
+ case "connect":
112737
+ return await this.pickProvider(fromId);
112738
+ case "resume":
112739
+ return await this.pickSession(fromId, end);
112740
+ default:
112741
+ await this.ctx.reply(`${line} is not available from this shell.`).catch(() => false);
112742
+ return "failed";
112743
+ }
112744
+ }
112745
+ async pickModel(providerId, fromId) {
112746
+ const host = this.ctx.host;
112747
+ if (host.listModels === undefined || host.switchModel === undefined) {
112748
+ await this.ctx.reply("/model is not available from this shell.").catch(() => false);
112749
+ return "failed";
112750
+ }
112751
+ const listing = await host.listModels(providerId);
112752
+ if (listing === undefined || listing.models.length === 0) {
112753
+ await this.ctx.reply(this.ctx.compose(`There are no models to pick from${providerId !== undefined ? ` for ${providerId}` : ""}. Use the shell to connect a provider.`)).catch(() => false);
112754
+ return "failed";
112755
+ }
112756
+ const current = listing.models.find((model) => model.current === true);
112757
+ const title = `Pick a model (${listing.provider})${current !== undefined ? `
112758
+ Now: ${current.label}` : ""}`;
112759
+ const options = listing.models.map((model) => ({ id: model.id, label: model.current === true ? `* ${model.label}` : model.label }));
112760
+ const chosen = await this.pick(title, options, fromId);
112761
+ if (chosen === undefined) {
112762
+ this.ctx.record("/model not answered in time; nothing changed");
112763
+ return "failed";
112764
+ }
112765
+ const line = "/model";
112766
+ const busy = this.busyReason(line, "model");
112767
+ if (busy !== undefined) {
112768
+ await this.ctx.reply(`/model was not run: ${busy}.`).catch(() => false);
112769
+ return "failed";
112770
+ }
112771
+ const outcome = await host.switchModel(chosen.id, providerId);
112772
+ this.ctx.record(`/model ${outcome.ok ? "switched" : "failed"}`);
112773
+ const text = outcome.output.trim();
112774
+ await this.ctx.reply(this.ctx.compose(text.length > 0 ? text : outcome.ok ? `Model: ${chosen.label.replace(/^\* /, "")}.` : "The model was not changed.")).catch(() => false);
112775
+ return outcome.ok ? "done" : "failed";
112776
+ }
112777
+ async pickProvider(fromId) {
112778
+ const host = this.ctx.host;
112779
+ if (host.listProviders === undefined || host.listModels === undefined || host.switchModel === undefined) {
112780
+ await this.ctx.reply("/connect is not available from this shell.").catch(() => false);
112781
+ return "failed";
112782
+ }
112783
+ const providers = await host.listProviders();
112784
+ if (providers.length === 0) {
112785
+ await this.ctx.reply("No provider is connected yet. Connecting one needs a key, which is never typed into a chat; use the shell.").catch(() => false);
112786
+ return "failed";
112787
+ }
112788
+ const options = providers.map((provider) => ({ id: provider.id, label: provider.current === true ? `* ${provider.label}` : provider.label }));
112789
+ const chosen = await this.pick("Connected providers. Pick one to switch to:", options, fromId);
112790
+ if (chosen === undefined) {
112791
+ this.ctx.record("/connect not answered in time; nothing changed");
112792
+ return "failed";
112793
+ }
112794
+ return await this.pickModel(chosen.id, fromId);
112795
+ }
112796
+ async pickSession(fromId, end) {
112797
+ const host = this.ctx.host;
112798
+ if (host.listSessions === undefined || host.resumeSession === undefined) {
112799
+ await this.ctx.reply("/resume is not available from this shell.").catch(() => false);
112800
+ return "failed";
112801
+ }
112802
+ const sessions = (await host.listSessions()).filter((session) => session.current !== true);
112803
+ if (sessions.length === 0) {
112804
+ await this.ctx.reply("There is no earlier session to return to.").catch(() => false);
112805
+ return "done";
112806
+ }
112807
+ const chosen = await this.pick("Return to an earlier session:", sessions.map((s) => ({ id: s.id, label: s.label })), fromId);
112808
+ if (chosen === undefined) {
112809
+ this.ctx.record("/resume not answered in time; nothing changed");
112810
+ return "failed";
112811
+ }
112812
+ const resume = host.resumeSession.bind(host);
112813
+ this.chain = this.chain.then(() => this.execute("resume", "/resume", () => resume(chosen.id), end)).catch(() => {
112814
+ return;
112815
+ });
112816
+ return "deferred";
112817
+ }
112818
+ async listSessionsText() {
112819
+ const host = this.ctx.host;
112820
+ const busy = this.busyReason("/sessions", "sessions");
112821
+ if (busy !== undefined) {
112822
+ this.ctx.record(`/sessions not run: ${busy}`);
112823
+ await this.ctx.reply(`/sessions was not run: ${busy}.`).catch(() => false);
112824
+ return;
112825
+ }
112826
+ if (host.listSessions === undefined) {
112827
+ await this.ctx.reply("/sessions is not available from this shell.").catch(() => false);
112828
+ return;
112829
+ }
112830
+ const sessions = await host.listSessions();
112831
+ if (sessions.length === 0) {
112832
+ await this.ctx.reply("There are no recorded sessions yet.").catch(() => false);
112833
+ return;
112834
+ }
112835
+ const lines = sessions.slice(0, 20).map((session) => `${session.current === true ? "* " : "- "}${session.label}`);
112836
+ await this.ctx.reply(this.ctx.compose(`Recent sessions:
112837
+ ${lines.join(`
112838
+ `)}`)).catch(() => false);
112839
+ }
112840
+ }
112841
+ var DEFAULT_ROUTER_LIMITS, PICKER_PAGE_SIZE = 7, LABEL_CHARS = 56, SESSION_COMMANDS;
112842
+ var init_command_router = __esm(() => {
112843
+ init_command_gateway();
112844
+ DEFAULT_ROUTER_LIMITS = { runningNoticeMs: 4000, commandLimitMs: 120000, choiceTimeoutMs: 120000 };
112845
+ SESSION_COMMANDS = ["new", "clear", "resume"];
112846
+ });
112847
+
112229
112848
  // src/remote/shell-bridge.ts
112230
112849
  function preview2(text) {
112231
112850
  const one = redactSensitiveText(text).replace(/\s+/g, " ").trim();
@@ -112287,11 +112906,59 @@ class RemoteBridge {
112287
112906
  runTimer;
112288
112907
  timedOut = false;
112289
112908
  closing;
112909
+ router;
112910
+ keepTopic = false;
112911
+ switched = false;
112912
+ waiting = [];
112913
+ currentUpdate;
112290
112914
  constructor(options) {
112291
112915
  this.options = options;
112292
112916
  this.host = options.host;
112293
112917
  this.now = options.now ?? Date.now;
112294
112918
  this.approvalTimeoutMs = options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
112919
+ this.router = new RemoteCommandRouter({
112920
+ host: {
112921
+ isBusy: () => this.host.isBusy(),
112922
+ cancelTurn: () => this.host.cancelTurn(),
112923
+ ...this.host.runCommand !== undefined ? { runCommand: (line) => this.host.runCommand(line) } : {},
112924
+ ...this.host.busyRefusal !== undefined ? { busyRefusal: (line) => this.host.busyRefusal(line) } : {},
112925
+ ...this.host.listModels !== undefined ? { listModels: (id) => this.host.listModels(id) } : {},
112926
+ ...this.host.listProviders !== undefined ? { listProviders: () => this.host.listProviders() } : {},
112927
+ ...this.host.switchModel !== undefined ? { switchModel: (model, provider) => this.host.switchModel(model, provider) } : {},
112928
+ ...this.host.listSessions !== undefined ? { listSessions: () => this.host.listSessions() } : {},
112929
+ ...this.host.resumeSession !== undefined ? { resumeSession: (id) => this.host.resumeSession(id) } : {}
112930
+ },
112931
+ reply: async (text) => this.client !== undefined ? await this.client.reply(text) : false,
112932
+ record: (text) => this.push("command", text),
112933
+ compose: composeReply,
112934
+ choose: async (text, rows, timeoutMs, forUserId) => {
112935
+ const client = this.client;
112936
+ if (client === undefined || !client.connected) {
112937
+ return;
112938
+ }
112939
+ try {
112940
+ return await client.requestChoice(text, rows, timeoutMs, forUserId);
112941
+ } catch {
112942
+ return;
112943
+ }
112944
+ },
112945
+ holdTopic: (on) => {
112946
+ this.keepTopic = on;
112947
+ if (on) {
112948
+ this.switched = false;
112949
+ }
112950
+ },
112951
+ takeSwitched: () => {
112952
+ const was = this.switched;
112953
+ this.switched = false;
112954
+ return was;
112955
+ },
112956
+ limits: {
112957
+ runningNoticeMs: options.commandRunningNoticeMs ?? DEFAULT_ROUTER_LIMITS.runningNoticeMs,
112958
+ commandLimitMs: options.commandLimitMs ?? DEFAULT_ROUTER_LIMITS.commandLimitMs,
112959
+ choiceTimeoutMs: options.choiceTimeoutMs ?? DEFAULT_ROUTER_LIMITS.choiceTimeoutMs
112960
+ }
112961
+ });
112295
112962
  }
112296
112963
  get active() {
112297
112964
  return this.client !== undefined;
@@ -112367,6 +113034,7 @@ class RemoteBridge {
112367
113034
  this.client = undefined;
112368
113035
  this.clientConnected = false;
112369
113036
  this.endTurnState();
113037
+ this.waiting.length = 0;
112370
113038
  this.host.recordOff();
112371
113039
  const dropped = this.host.dropQueuedTelegramLines?.() ?? [];
112372
113040
  if (dropped.length > 0) {
@@ -112406,26 +113074,73 @@ class RemoteBridge {
112406
113074
  this.push("status", `stopped${status.reason !== undefined ? `: ${preview2(status.reason)}` : ""}`);
112407
113075
  }
112408
113076
  }
112409
- accept(text, _meta) {
113077
+ accept(text, meta) {
112410
113078
  const line = text.trim();
112411
113079
  if (line.length === 0 || this.client === undefined)
112412
113080
  return;
112413
113081
  if (line.startsWith("/")) {
112414
- this.push("line", `Telegram: slash command refused (${preview2(line.split(/\s+/)[0] ?? "")})`);
112415
- this.client.reply("Slash commands are not run from Telegram. Send plain text.").catch(() => false);
113082
+ this.push("line", `Telegram: ${preview2(line)}`);
113083
+ this.router.handle(line, meta.fromId, (result) => this.report(meta.updateId, result));
112416
113084
  return;
112417
113085
  }
112418
113086
  this.push("line", `Telegram: ${preview2(line)}`);
113087
+ this.waiting.push({ updateId: meta.updateId, line });
112419
113088
  if (this.host.isBusy()) {
112420
113089
  this.host.enqueue(line);
112421
113090
  } else {
113091
+ this.report(meta.updateId, "reading");
112422
113092
  this.host.runLine(line);
112423
113093
  }
112424
113094
  }
113095
+ report(updateId, state) {
113096
+ const client = this.client;
113097
+ if (client?.reportState === undefined)
113098
+ return;
113099
+ const previous = this.reportTails.get(updateId);
113100
+ const post = client.reportState.bind(client);
113101
+ const started = previous === undefined ? post(updateId, state) : previous.then(() => post(updateId, state));
113102
+ const tail = started.then(() => {
113103
+ return;
113104
+ }, () => {
113105
+ return;
113106
+ }).then(() => {
113107
+ if (this.reportTails.get(updateId) === tail)
113108
+ this.reportTails.delete(updateId);
113109
+ });
113110
+ this.reportTails.set(updateId, tail);
113111
+ }
113112
+ reportTails = new Map;
113113
+ idle() {
113114
+ return this.router.idle();
113115
+ }
113116
+ get keepingTopic() {
113117
+ return this.keepTopic && this.client !== undefined;
113118
+ }
113119
+ sessionLeaving() {
113120
+ if (this.client === undefined)
113121
+ return;
113122
+ this.endTurnState();
113123
+ this.host.recordOff();
113124
+ this.push("status", "session changing; this topic stays bound");
113125
+ }
113126
+ sessionEntered(kind = "new") {
113127
+ const client = this.client;
113128
+ if (client === undefined)
113129
+ return;
113130
+ this.switched = true;
113131
+ if (client.name !== undefined)
113132
+ this.host.recordOn(client.name);
113133
+ this.push("status", kind === "new" ? "new session; same topic" : "resumed session; same topic");
113134
+ client.reply(kind === "new" ? "--- new session ---" : "--- resumed session ---").catch(() => false);
113135
+ }
112425
113136
  turnStarted(source) {
112426
113137
  this.endTurnState();
112427
113138
  if (source !== TG_SOURCE || this.client === undefined)
112428
113139
  return;
113140
+ const next = this.waiting.shift();
113141
+ this.currentUpdate = next?.updateId;
113142
+ if (next !== undefined)
113143
+ this.report(next.updateId, "working");
112429
113144
  this.tgTurn = true;
112430
113145
  this.lastAssistantText = "";
112431
113146
  this.timedOut = false;
@@ -112454,6 +113169,12 @@ class RemoteBridge {
112454
113169
  if (client === undefined)
112455
113170
  return;
112456
113171
  this.push("line", `Telegram line removed from the queue: ${preview2(line)}`);
113172
+ const index = this.waiting.findIndex((entry) => entry.line === line.trim());
113173
+ if (index >= 0) {
113174
+ const [gone] = this.waiting.splice(index, 1);
113175
+ if (gone !== undefined)
113176
+ this.report(gone.updateId, "failed");
113177
+ }
112457
113178
  client.reply(`Your message "${preview2(line)}" was removed from the queue in the shell, so it will not run.`).catch(() => false);
112458
113179
  }
112459
113180
  async turnSettled(outcome) {
@@ -112462,9 +113183,12 @@ class RemoteBridge {
112462
113183
  const client = this.client;
112463
113184
  const text = this.lastAssistantText;
112464
113185
  const timedOut = this.timedOut;
113186
+ const updateId = this.currentUpdate;
112465
113187
  this.endTurnState();
112466
113188
  if (client === undefined)
112467
113189
  return;
113190
+ if (updateId !== undefined)
113191
+ this.report(updateId, outcome.failed || timedOut ? "failed" : "done");
112468
113192
  let body;
112469
113193
  if (timedOut) {
112470
113194
  body = "Stopped: this run went over the time limit for runs started from Telegram.";
@@ -112480,6 +113204,7 @@ class RemoteBridge {
112480
113204
  }
112481
113205
  endTurnState() {
112482
113206
  this.tgTurn = false;
113207
+ this.currentUpdate = undefined;
112483
113208
  this.lastAssistantText = "";
112484
113209
  this.timedOut = false;
112485
113210
  if (this.runTimer !== undefined) {
@@ -112508,6 +113233,7 @@ var TG_SOURCE = "tg", REMOTE_EVENT_LIMIT = 20, REMOTE_REPLY_MAX_CHARS = 16000, E
112508
113233
  var init_shell_bridge = __esm(() => {
112509
113234
  init_service2();
112510
113235
  init_client3();
113236
+ init_command_router();
112511
113237
  init_protocol2();
112512
113238
  });
112513
113239
 
@@ -113995,6 +114721,9 @@ var init_channels_surface = __esm(() => {
113995
114721
  });
113996
114722
 
113997
114723
  // src/tui/remote-control-surface.ts
114724
+ function commandEchoText(line, source) {
114725
+ return source === TG_SOURCE ? `${TG_ECHO_MARKER} ${line}` : `\u276F ${line}`;
114726
+ }
113998
114727
  function labelTelegramLine(text) {
113999
114728
  return `[${TG_LABEL}] ${text}`;
114000
114729
  }
@@ -114048,6 +114777,12 @@ function formatRemoteEventLines(status) {
114048
114777
  return ["No remote events yet."];
114049
114778
  return [...status.events].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock(event.at)} ${event.kind.padEnd(8)} ${event.text}`);
114050
114779
  }
114780
+ function formatRemoteCommandLines(status) {
114781
+ const commands = status.events.filter((event) => event.kind === "command");
114782
+ if (commands.length === 0)
114783
+ return ["No remote commands yet."];
114784
+ return [...commands].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock(event.at)} ${event.text}`);
114785
+ }
114051
114786
  function renderRemoteControlText(status) {
114052
114787
  return `${[...formatRemoteStatusLines(status), "", "Recent events:", ...formatRemoteEventLines(status)].join(`
114053
114788
  `)}
@@ -114084,7 +114819,11 @@ function presentRemoteControl(open, otui, chrome, options) {
114084
114819
  let unsubscribeKey;
114085
114820
  const linesFor = (tab) => {
114086
114821
  const status = options.getStatus();
114087
- return tab === "events" ? formatRemoteEventLines(status) : formatRemoteStatusLines(status);
114822
+ if (tab === "events")
114823
+ return formatRemoteEventLines(status);
114824
+ if (tab === "commands")
114825
+ return formatRemoteCommandLines(status);
114826
+ return formatRemoteStatusLines(status);
114088
114827
  };
114089
114828
  const content = (tab) => {
114090
114829
  const lines = linesFor(tab);
@@ -114211,7 +114950,8 @@ var init_remote_control_surface = __esm(() => {
114211
114950
  STATE_WORDS = { off: "off", on: "on", offline: "offline" };
114212
114951
  REMOTE_TABS = [
114213
114952
  { id: "status", label: "Status" },
114214
- { id: "events", label: "Events" }
114953
+ { id: "events", label: "Events" },
114954
+ { id: "commands", label: "Commands" }
114215
114955
  ];
114216
114956
  });
114217
114957
 
@@ -118415,7 +119155,7 @@ function viewWriteRun(cwd, runIdOrPrefix, deps = {}) {
118415
119155
  const patch = readVerifiedPatch(located.dir, located.record);
118416
119156
  return patch.ok ? { record: located.record, patch: patch.text } : { record: located.record };
118417
119157
  }
118418
- function refuse3(reason, detail) {
119158
+ function refuse4(reason, detail) {
118419
119159
  return { kind: "refused", reason, detail };
118420
119160
  }
118421
119161
  function decisionBody(decision) {
@@ -118425,33 +119165,33 @@ function decisionBody(decision) {
118425
119165
  async function landWriteRun(input, deps = {}) {
118426
119166
  const located = locate(input.cwd, input.runId, deps.dataDir);
118427
119167
  if (located === undefined)
118428
- return refuse3("not-found", `no external write run "${input.runId}" in this checkout`);
119168
+ return refuse4("not-found", `no external write run "${input.runId}" in this checkout`);
118429
119169
  const { record, dir } = located;
118430
119170
  if (record.state !== "pending-review" || located.decided) {
118431
- return refuse3("not-pending", record.state === "refused" ? "the run was refused at capture; it can only be discarded" : "the run was already decided");
119171
+ return refuse4("not-pending", record.state === "refused" ? "the run was refused at capture; it can only be discarded" : "the run was already decided");
118432
119172
  }
118433
119173
  const patch = readVerifiedPatch(dir, record);
118434
119174
  if (!patch.ok)
118435
- return refuse3(patch.reason, patch.detail);
119175
+ return refuse4(patch.reason, patch.detail);
118436
119176
  if (input.confirmedPatchHash !== record.patchHash) {
118437
- return refuse3("hash-mismatch", "the confirmed hash is not the hash of the stored patch");
119177
+ return refuse4("hash-mismatch", "the confirmed hash is not the hash of the stored patch");
118438
119178
  }
118439
119179
  if (record.redacted) {
118440
- return refuse3("redacted", "the stored patch was altered by redaction, so landing it would land content the agent did not write");
119180
+ return refuse4("redacted", "the stored patch was altered by redaction, so landing it would land content the agent did not write");
118441
119181
  }
118442
119182
  const binary = record.files.filter((file) => file.binary === true).map((file) => file.path);
118443
119183
  if (binary.length > 0) {
118444
- return refuse3("binary", `binary content is not carried by the patch: ${binary.join(", ")}`);
119184
+ return refuse4("binary", `binary content is not carried by the patch: ${binary.join(", ")}`);
118445
119185
  }
118446
119186
  if (record.flaggedPaths.length > 0 && input.allowFlagged !== true) {
118447
- return refuse3("flagged", `the run changes flagged paths (${record.flaggedPaths.join(", ")}); allow them explicitly to land`);
119187
+ return refuse4("flagged", `the run changes flagged paths (${record.flaggedPaths.join(", ")}); allow them explicitly to land`);
118448
119188
  }
118449
119189
  const git = deps.git ?? defaultGit;
118450
119190
  const repo = record.projectRoot;
118451
119191
  const branch = `external/${record.runId}`;
118452
119192
  try {
118453
119193
  await git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo);
118454
- return refuse3("branch-exists", `branch ${branch} already exists`);
119194
+ return refuse4("branch-exists", `branch ${branch} already exists`);
118455
119195
  } catch {}
118456
119196
  const ownedDir = deps.worktreesDir === undefined ? await mkdtemp6(path261.join(tmpdir12(), "keryx-external-land-")) : undefined;
118457
119197
  const worktreesDir = deps.worktreesDir ?? ownedDir;
@@ -118473,7 +119213,7 @@ async function landWriteRun(input, deps = {}) {
118473
119213
  return;
118474
119214
  });
118475
119215
  createdBranch = false;
118476
- return refuse3("not-pending", "the run was decided by someone else while it was being landed");
119216
+ return refuse4("not-pending", "the run was decided by someone else while it was being landed");
118477
119217
  }
118478
119218
  return { kind: "landed", branch, commit };
118479
119219
  } catch (error) {
@@ -118481,7 +119221,7 @@ async function landWriteRun(input, deps = {}) {
118481
119221
  await git(["branch", "-D", branch], repo).catch(() => {
118482
119222
  return;
118483
119223
  });
118484
- return refuse3("apply-failed", errorMessage3(error));
119224
+ return refuse4("apply-failed", errorMessage3(error));
118485
119225
  } finally {
118486
119226
  await port.remove(worktreeId).catch(() => {
118487
119227
  return;
@@ -145920,6 +146660,16 @@ async function launchTuiAgentShell(opts) {
145920
146660
  detachExternal = attached.detach;
145921
146661
  return attached.operator;
145922
146662
  })();
146663
+ const commandWork = new Set;
146664
+ const commandWorkFailures = [];
146665
+ const trackCommandWork = (work) => {
146666
+ const settled = work().catch((error) => {
146667
+ commandWorkFailures.push(error instanceof Error ? error.message : String(error));
146668
+ }).finally(() => {
146669
+ commandWork.delete(settled);
146670
+ });
146671
+ commandWork.add(settled);
146672
+ };
145923
146673
  const runDelegate = (line) => {
145924
146674
  const parsed = parseDelegateCommand(line.trim().replace(/^\/\S+\s*/, ""));
145925
146675
  if (!parsed.ok) {
@@ -146244,7 +146994,7 @@ async function launchTuiAgentShell(opts) {
146244
146994
  }).catch(() => {});
146245
146995
  };
146246
146996
  const showExternalStatus = () => {
146247
- loadExternalStatus().then((status) => {
146997
+ trackCommandWork(() => loadExternalStatus().then((status) => {
146248
146998
  externalStatus = status;
146249
146999
  refreshExternalSidebar();
146250
147000
  const jevAvailable = resolveJevApiKey(process.env) !== undefined;
@@ -146255,7 +147005,7 @@ async function launchTuiAgentShell(opts) {
146255
147005
  }).catch((error) => {
146256
147006
  io.onSystem?.(`external: ${error instanceof Error ? error.message : String(error)}
146257
147007
  `);
146258
- });
147008
+ }));
146259
147009
  };
146260
147010
  const approvalContext = createApprovalContextLoader(opts.session?.cwd ?? process.cwd());
146261
147011
  const confirmScheduleCard = async (card) => {
@@ -146727,6 +147477,10 @@ async function launchTuiAgentShell(opts) {
146727
147477
  liveDeps = deps;
146728
147478
  let splash = undefined;
146729
147479
  const stopRemoteForSessionSwitch = () => {
147480
+ if (remoteBridge?.keepingTopic === true) {
147481
+ remoteBridge.sessionLeaving();
147482
+ return;
147483
+ }
146730
147484
  if (remoteBridge?.active !== true)
146731
147485
  return;
146732
147486
  const closing = remoteBridge.disable();
@@ -146741,6 +147495,8 @@ async function launchTuiAgentShell(opts) {
146741
147495
  }
146742
147496
  stopRemoteForSessionSwitch();
146743
147497
  liveSession = opened.handle;
147498
+ if (remoteBridge?.keepingTopic === true)
147499
+ remoteBridge.sessionEntered(opened.resumed ? "resumed" : "new");
146744
147500
  io.trustedMcpTools?.clear();
146745
147501
  history = previewHistory === true ? opened.history.slice(-SESSION_PREVIEW_MESSAGE_COUNT) : opened.history;
146746
147502
  archive = opened.archive.length > 0 ? [...opened.archive] : [...opened.history];
@@ -147103,6 +147859,8 @@ Staying in the current session.
147103
147859
  io.trustedMcpTools?.clear();
147104
147860
  stopRemoteForSessionSwitch();
147105
147861
  liveSession = opened.handle;
147862
+ if (remoteBridge?.keepingTopic === true)
147863
+ remoteBridge.sessionEntered("new");
147106
147864
  history = [];
147107
147865
  archive = [];
147108
147866
  nextArchiveIndex = 0;
@@ -147115,8 +147873,10 @@ Staying in the current session.
147115
147873
  }
147116
147874
  return true;
147117
147875
  };
147876
+ let pendingResumeTarget;
147118
147877
  const resumeSessionInteractive = async () => {
147119
- const found = await pickRecentSession();
147878
+ const found = pendingResumeTarget ?? await pickRecentSession();
147879
+ pendingResumeTarget = undefined;
147120
147880
  if (found === undefined) {
147121
147881
  return;
147122
147882
  }
@@ -148473,7 +149233,7 @@ ${formatThemeList(getThemeId())}`);
148473
149233
  if (line.startsWith("/")) {
148474
149234
  transcript.add(new otui.TextRenderable(r, {
148475
149235
  id: `c${uid++}`,
148476
- content: otui.t`${roleChunk(otui, "accent", `\u276F ${line}`)}`,
149236
+ content: otui.t`${roleChunk(otui, "accent", commandEchoText(line, source))}`,
148477
149237
  marginTop: 1
148478
149238
  }));
148479
149239
  }
@@ -148726,7 +149486,7 @@ ${formatThemeList(getThemeId())}`);
148726
149486
  const cwd = inspectorCwd();
148727
149487
  io.onSystem?.(`review-jev-risk: scoring the working diff's hunks\u2026
148728
149488
  `);
148729
- (async () => {
149489
+ trackCommandWork(async () => {
148730
149490
  try {
148731
149491
  io.onSystem?.(`${await runJevRiskForShell(cwd)}
148732
149492
  `);
@@ -148734,14 +149494,14 @@ ${formatThemeList(getThemeId())}`);
148734
149494
  io.onSystem?.(`review-jev-risk: ${error instanceof Error ? error.message : String(error)}
148735
149495
  `);
148736
149496
  }
148737
- })();
149497
+ });
148738
149498
  return;
148739
149499
  }
148740
149500
  if (isJevScenariosCommand(command.name)) {
148741
149501
  const cwd = inspectorCwd();
148742
149502
  io.onSystem?.(`review-jev-scenarios: checking likely-affected scenarios\u2026
148743
149503
  `);
148744
- (async () => {
149504
+ trackCommandWork(async () => {
148745
149505
  try {
148746
149506
  io.onSystem?.(`${await runJevScenariosForShell(cwd)}
148747
149507
  `);
@@ -148749,14 +149509,14 @@ ${formatThemeList(getThemeId())}`);
148749
149509
  io.onSystem?.(`review-jev-scenarios: ${error instanceof Error ? error.message : String(error)}
148750
149510
  `);
148751
149511
  }
148752
- })();
149512
+ });
148753
149513
  return;
148754
149514
  }
148755
149515
  if (isJevRulesCommand(command.name)) {
148756
149516
  const cwd = inspectorCwd();
148757
149517
  io.onSystem?.(`review-jev-rules: checking the working diff against project rules\u2026
148758
149518
  `);
148759
- (async () => {
149519
+ trackCommandWork(async () => {
148760
149520
  try {
148761
149521
  io.onSystem?.(`${await runJevRulesForShell(cwd)}
148762
149522
  `);
@@ -148764,14 +149524,14 @@ ${formatThemeList(getThemeId())}`);
148764
149524
  io.onSystem?.(`review-jev-rules: ${error instanceof Error ? error.message : String(error)}
148765
149525
  `);
148766
149526
  }
148767
- })();
149527
+ });
148768
149528
  return;
148769
149529
  }
148770
149530
  if (isStaledocsCommand(command.name)) {
148771
149531
  const cwd = inspectorCwd();
148772
149532
  io.onSystem?.(`review-jev-docs: checking the working diff against documentation\u2026
148773
149533
  `);
148774
- (async () => {
149534
+ trackCommandWork(async () => {
148775
149535
  try {
148776
149536
  io.onSystem?.(`${await runStaledocsForShell(cwd)}
148777
149537
  `);
@@ -148779,7 +149539,7 @@ ${formatThemeList(getThemeId())}`);
148779
149539
  io.onSystem?.(`review-jev-docs: ${error instanceof Error ? error.message : String(error)}
148780
149540
  `);
148781
149541
  }
148782
- })();
149542
+ });
148783
149543
  return;
148784
149544
  }
148785
149545
  if (isOpencommentsCommand(command.name)) {
@@ -148794,7 +149554,7 @@ ${formatThemeList(getThemeId())}`);
148794
149554
  }
148795
149555
  io.onSystem?.(`review-jev-comments: checking open comments on ${repoArg}#${prNumber}\u2026
148796
149556
  `);
148797
- (async () => {
149557
+ trackCommandWork(async () => {
148798
149558
  try {
148799
149559
  io.onSystem?.(`${await runOpencommentsForShell(cwd, repoArg, prNumber)}
148800
149560
  `);
@@ -148802,14 +149562,14 @@ ${formatThemeList(getThemeId())}`);
148802
149562
  io.onSystem?.(`review-jev-comments: ${error instanceof Error ? error.message : String(error)}
148803
149563
  `);
148804
149564
  }
148805
- })();
149565
+ });
148806
149566
  return;
148807
149567
  }
148808
149568
  if (isJevContractCommand(command.name)) {
148809
149569
  const cwd = inspectorCwd();
148810
149570
  io.onSystem?.(`review-jev-contract: checking claims against the working diff\u2026
148811
149571
  `);
148812
- (async () => {
149572
+ trackCommandWork(async () => {
148813
149573
  try {
148814
149574
  io.onSystem?.(`${await runJevContractForShell(cwd)}
148815
149575
  `);
@@ -148817,14 +149577,14 @@ ${formatThemeList(getThemeId())}`);
148817
149577
  io.onSystem?.(`review-jev-contract: ${error instanceof Error ? error.message : String(error)}
148818
149578
  `);
148819
149579
  }
148820
- })();
149580
+ });
148821
149581
  return;
148822
149582
  }
148823
149583
  if (isJevTriageCommand(command.name)) {
148824
149584
  const cwd = inspectorCwd();
148825
149585
  io.onSystem?.(`review-jev-triage: triaging the latest review package\u2026
148826
149586
  `);
148827
- (async () => {
149587
+ trackCommandWork(async () => {
148828
149588
  try {
148829
149589
  io.onSystem?.(`${await runJevTriageForShell(cwd)}
148830
149590
  `);
@@ -148832,7 +149592,7 @@ ${formatThemeList(getThemeId())}`);
148832
149592
  io.onSystem?.(`review-jev-triage: ${error instanceof Error ? error.message : String(error)}
148833
149593
  `);
148834
149594
  }
148835
- })();
149595
+ });
148836
149596
  return;
148837
149597
  }
148838
149598
  if (isRewindCommand(command.name)) {
@@ -148888,13 +149648,13 @@ ${formatThemeList(getThemeId())}`);
148888
149648
  }
148889
149649
  if (isExternalAgentsCommand(command.name)) {
148890
149650
  const arg = line.trim().split(/\s+/).slice(1).join(" ");
148891
- runExternalAgentsCommand(arg, opts.session?.cwd ?? process.cwd()).then((text) => io.onSystem?.(text));
149651
+ trackCommandWork(() => runExternalAgentsCommand(arg, opts.session?.cwd ?? process.cwd()).then((text) => io.onSystem?.(text)));
148892
149652
  return;
148893
149653
  }
148894
149654
  if (isExternalCommand(command.name)) {
148895
149655
  const arg = line.trim().split(/\s+/).slice(1).join(" ").trim().toLowerCase();
148896
149656
  if (arg === "on" || arg === "off") {
148897
- setExternalEnabled(arg);
149657
+ trackCommandWork(() => setExternalEnabled(arg));
148898
149658
  return;
148899
149659
  }
148900
149660
  if (arg.length > 0) {
@@ -148938,11 +149698,11 @@ ${formatThemeList(getThemeId())}`);
148938
149698
  return;
148939
149699
  }
148940
149700
  if (command.name === "/doctor") {
148941
- (async () => {
149701
+ trackCommandWork(async () => {
148942
149702
  const report = await buildDoctorReport(sessionCwd);
148943
149703
  io.onSystem?.(`${formatDoctorReport2(report)}
148944
149704
  `);
148945
- })();
149705
+ });
148946
149706
  return;
148947
149707
  }
148948
149708
  if (command.name === "/setup") {
@@ -149414,6 +150174,110 @@ ${formatThemeList(getThemeId())}`);
149414
150174
  runLine(drained.question, "operator", drained.source);
149415
150175
  }
149416
150176
  });
150177
+ const captureShellOutput = async (run) => {
150178
+ const chunks = [];
150179
+ let active = true;
150180
+ let ok = true;
150181
+ const previous = io.onSystem;
150182
+ const previousToast = chrome.showToast;
150183
+ const capture = (text) => {
150184
+ if (active)
150185
+ chunks.push(text);
150186
+ previous?.(text);
150187
+ };
150188
+ const captureToast = (message) => {
150189
+ if (active)
150190
+ chunks.push(`${message}
150191
+ `);
150192
+ previousToast(message);
150193
+ };
150194
+ const before = new Set(commandWork);
150195
+ commandWorkFailures.length = 0;
150196
+ io.onSystem = capture;
150197
+ chrome.showToast = captureToast;
150198
+ try {
150199
+ await run();
150200
+ let seen = chunks.length;
150201
+ let quiet = 0;
150202
+ while (quiet < 2 || [...commandWork].some((work) => !before.has(work))) {
150203
+ await new Promise((resolve) => setTimeout(resolve, 150));
150204
+ if (chunks.length !== seen) {
150205
+ seen = chunks.length;
150206
+ quiet = 0;
150207
+ } else {
150208
+ quiet += 1;
150209
+ }
150210
+ }
150211
+ } catch (error) {
150212
+ ok = false;
150213
+ chunks.push(`${error instanceof Error ? error.message : String(error)}
150214
+ `);
150215
+ } finally {
150216
+ active = false;
150217
+ if (io.onSystem === capture) {
150218
+ if (previous)
150219
+ io.onSystem = previous;
150220
+ else
150221
+ delete io.onSystem;
150222
+ }
150223
+ if (chrome.showToast === captureToast)
150224
+ chrome.showToast = previousToast;
150225
+ }
150226
+ if (commandWorkFailures.length > 0) {
150227
+ ok = false;
150228
+ chunks.push(`${commandWorkFailures.splice(0).join(`
150229
+ `)}
150230
+ `);
150231
+ }
150232
+ return { output: chunks.join("").trim(), ok };
150233
+ };
150234
+ const echoRemoteCommand = (line) => {
150235
+ transcript.add(new otui.TextRenderable(r, {
150236
+ id: `c${uid++}`,
150237
+ content: otui.t`${roleChunk(otui, "accent", commandEchoText(line, TG_SOURCE))}`,
150238
+ marginTop: 1
150239
+ }));
150240
+ };
150241
+ const remoteStatusText = async () => {
150242
+ const cwd = inspectorCwd();
150243
+ const [workspaces, flows] = await Promise.all([loadInspectorWorkspaces(cwd), loadInspectorFlows(cwd)]);
150244
+ const snapshot = buildSessionInfoSnapshot({
150245
+ summary: liveSession.summary,
150246
+ selection: currentSel,
150247
+ version: package_default.version,
150248
+ usage: lastUsage,
150249
+ estimateTokens: estimateContextTokens(history),
150250
+ sessionText: history.map((message) => message.content).join(`
150251
+ `),
150252
+ workspaces,
150253
+ flows
150254
+ });
150255
+ return `${formatSessionInfoText(snapshot).trimEnd()}
150256
+
150257
+ Mode: ${permissionMode}${readOnly ? " (read-only on)" : ""}
150258
+ Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} waiting`;
150259
+ };
150260
+ const runRemoteCommand = async (line) => {
150261
+ const name = findAgentCommand(line, "agent")?.name;
150262
+ const first = (line.trim().split(/\s+/)[1] ?? "").toLowerCase();
150263
+ if (name === "/status") {
150264
+ echoRemoteCommand(line);
150265
+ return await captureShellOutput(async () => {
150266
+ io.onSystem?.(`${await remoteStatusText()}
150267
+ `);
150268
+ });
150269
+ }
150270
+ if (name === "/mode" && (first === "" || first === "auto")) {
150271
+ echoRemoteCommand(line);
150272
+ return await captureShellOutput(() => {
150273
+ if (first === "auto")
150274
+ commitPermissionMode("auto");
150275
+ io.onSystem?.(`Permission mode: ${permissionMode} (ask, trust or auto)
150276
+ `);
150277
+ });
150278
+ }
150279
+ return await captureShellOutput(() => runLine(line, "operator", TG_SOURCE));
150280
+ };
149417
150281
  remoteBridge = new RemoteBridge({
149418
150282
  host: {
149419
150283
  sessionId: () => liveSession.summary.id,
@@ -149452,6 +150316,78 @@ ${formatThemeList(getThemeId())}`);
149452
150316
  paintMainQueue();
149453
150317
  return dropped.map((item) => item.question);
149454
150318
  },
150319
+ runCommand: runRemoteCommand,
150320
+ busyRefusal: (line) => {
150321
+ const command = findAgentCommand(line, "agent");
150322
+ const decision = classifyBusyDispatch({
150323
+ line,
150324
+ commandName: command?.name,
150325
+ isSessionInfo: isSessionInfoCommand(line),
150326
+ isFlows: isFlowsCommand(line),
150327
+ isWorkspace: isWorkspaceCommand(line),
150328
+ isReview: isReviewCommand(line),
150329
+ isMcp: isMcpToolsCommand(line),
150330
+ isMcpConsumer: isMcpConsumerCommand(line)
150331
+ });
150332
+ return decision === "deferred" || decision === "not-a-command" ? BUSY_REASON : undefined;
150333
+ },
150334
+ listProviders: async () => {
150335
+ const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
150336
+ const connected = await filterConnectedDetectedProviders(detected, {});
150337
+ return connected.map((prov) => ({
150338
+ id: prov.name,
150339
+ label: prov.label ?? prov.name,
150340
+ ...prov.name === currentSel.provider ? { current: true } : {}
150341
+ }));
150342
+ },
150343
+ listModels: async (providerId) => {
150344
+ const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
150345
+ const name = providerId ?? currentSel.provider;
150346
+ const prov = detected.find((d) => d.name === name);
150347
+ if (prov === undefined)
150348
+ return;
150349
+ const resolved = await modelsForPicker(prov);
150350
+ return {
150351
+ provider: prov.label ?? prov.name,
150352
+ models: resolved.models.map((id) => ({
150353
+ id,
150354
+ label: id,
150355
+ ...name === currentSel.provider && id === currentSel.model ? { current: true } : {}
150356
+ }))
150357
+ };
150358
+ },
150359
+ switchModel: async (modelId, providerId) => {
150360
+ const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
150361
+ const name = providerId ?? currentSel.provider;
150362
+ const prov = detected.find((d) => d.name === name);
150363
+ if (prov === undefined)
150364
+ return { output: "That provider is not connected here.", ok: false };
150365
+ const baseUrl = name === currentSel.provider ? currentSel.baseUrl : prov.baseUrl;
150366
+ await switchTo(baseUrl === undefined ? { provider: name, model: modelId } : { provider: name, model: modelId, baseUrl });
150367
+ try {
150368
+ await applyRuntimeSwitchToSlate({
150369
+ slateSession,
150370
+ runtime: { provider: currentSel.provider, model: currentSel.model },
150371
+ history,
150372
+ onHistoryChange: io.onHistoryChange
150373
+ });
150374
+ } catch {}
150375
+ return { output: `Model: ${currentSel.provider}/${currentSel.model}`, ok: true };
150376
+ },
150377
+ listSessions: async () => listSessions(sessionCwd).map((row) => ({
150378
+ id: row.id,
150379
+ label: `${shortSessionId(row.id)} \xB7 ${row.title}`,
150380
+ ...row.id === liveSession.summary.id ? { current: true } : {}
150381
+ })),
150382
+ resumeSession: async (id) => {
150383
+ const found = findSession(sessionCwd, id);
150384
+ if (found === undefined)
150385
+ return { output: "Session not found in this project.", ok: false };
150386
+ pendingResumeTarget = found;
150387
+ const outcome = await captureShellOutput(() => resumeSessionInteractive());
150388
+ pendingResumeTarget = undefined;
150389
+ return liveSession.summary.id === found.id ? { ...outcome, ok: true } : { ...outcome, ok: false };
150390
+ },
149455
150391
  onChange: () => liveRemotePanel?.refresh()
149456
150392
  }
149457
150393
  });
@@ -149504,6 +150440,7 @@ var init_tui_shell = __esm(() => {
149504
150440
  init_product_open_surface();
149505
150441
  init_reviews_inspector();
149506
150442
  init_shell_bridge();
150443
+ init_command_gateway();
149507
150444
  init_channels_client();
149508
150445
  init_channels_surface();
149509
150446
  init_remote_control_surface();
@@ -184248,7 +185185,7 @@ function finishReasonToStopReason(finishReason) {
184248
185185
  return "end_turn";
184249
185186
  }
184250
185187
  }
184251
- function refuse4(error) {
185188
+ function refuse5(error) {
184252
185189
  throw new AcpError(error.code, error.message, error.data);
184253
185190
  }
184254
185191
  async function runAcpServer(options) {
@@ -184321,7 +185258,7 @@ async function runAcpServer(options) {
184321
185258
  if (!activeTurns.has(sessionId)) {
184322
185259
  return;
184323
185260
  }
184324
- refuse4(invalidRequest({
185261
+ refuse5(invalidRequest({
184325
185262
  reason: `${method}: a turn is already running for session ${sessionId}; ` + "wait for its session/prompt response \u2014 send session/cancel first if you do not want to wait for it to finish, " + "then prompt again once the cancelled turn has replied",
184326
185263
  method,
184327
185264
  sessionId,
@@ -184370,7 +185307,7 @@ async function runAcpServer(options) {
184370
185307
  let stopping = false;
184371
185308
  function refuseIfStopping(method) {
184372
185309
  if (stopping) {
184373
- refuse4({
185310
+ refuse5({
184374
185311
  code: JSON_RPC_ERROR_CODES.invalidRequest,
184375
185312
  message: `${method}: keryx acp is shutting down`,
184376
185313
  data: { method, condition: "shutting-down" }
@@ -184379,7 +185316,7 @@ async function runAcpServer(options) {
184379
185316
  }
184380
185317
  function requireProvider(method) {
184381
185318
  if (options.provider === undefined) {
184382
- refuse4({
185319
+ refuse5({
184383
185320
  code: JSON_RPC_ERROR_CODES.invalidRequest,
184384
185321
  message: options.providerUnavailable ?? `${method}: keryx acp has no provider configured`,
184385
185322
  data: { method, condition: "provider-not-configured" }
@@ -184494,7 +185431,7 @@ async function runAcpServer(options) {
184494
185431
  const choice = findAcpModelChoice(choices, requested, allowBareModelId);
184495
185432
  const available = choices.map((entry) => entry.value);
184496
185433
  if (choice === undefined) {
184497
- refuse4(invalidParams(`${method}: "${requested.length > 80 ? `${requested.slice(0, 80)}\u2026` : requested}" is not a model this session can run; ` + `choose one of: ${available.join(", ")}`, { value: requested, available }));
185434
+ refuse5(invalidParams(`${method}: "${requested.length > 80 ? `${requested.slice(0, 80)}\u2026` : requested}" is not a model this session can run; ` + `choose one of: ${available.join(", ")}`, { value: requested, available }));
184498
185435
  }
184499
185436
  const seq = beginSwitch(sessionId);
184500
185437
  const current = sessionModel(sessionId, method);
@@ -184503,7 +185440,7 @@ async function runAcpServer(options) {
184503
185440
  return "cancelled";
184504
185441
  }
184505
185442
  if (typeof built === "string") {
184506
- refuse4(invalidParams(`${method}: cannot switch to ${choice.value}: ${built}`, { value: choice.value, reason: built }));
185443
+ refuse5(invalidParams(`${method}: cannot switch to ${choice.value}: ${built}`, { value: choice.value, reason: built }));
184507
185444
  }
184508
185445
  const next = { choice, binding: built };
184509
185446
  applySwitch(sessionId, seq, next);
@@ -184620,14 +185557,14 @@ async function runAcpServer(options) {
184620
185557
  })();
184621
185558
  function requireInitialized(method) {
184622
185559
  if (!initialized) {
184623
- refuse4(invalidRequest({ reason: `${method}: call initialize before any other request`, method }));
185560
+ refuse5(invalidRequest({ reason: `${method}: call initialize before any other request`, method }));
184624
185561
  }
184625
185562
  }
184626
185563
  function handleInitialize(params) {
184627
185564
  const obj = requireObjectParams(params, ACP_AGENT_METHODS.initialize);
184628
185565
  const negotiation = negotiateProtocolVersion(obj["protocolVersion"]);
184629
185566
  if (negotiation.kind === "refuse") {
184630
- refuse4(negotiation.error);
185567
+ refuse5(negotiation.error);
184631
185568
  }
184632
185569
  initialized = true;
184633
185570
  const rawCaps = obj["clientCapabilities"];
@@ -184751,13 +185688,13 @@ Switch with /model <model>; it applies from the next turn.`;
184751
185688
  const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionPrompt);
184752
185689
  const promptField = obj["prompt"];
184753
185690
  if (!Array.isArray(promptField)) {
184754
- refuse4(invalidParams(`${ACP_AGENT_METHODS.sessionPrompt}: prompt must be an array of content blocks`, {
185691
+ refuse5(invalidParams(`${ACP_AGENT_METHODS.sessionPrompt}: prompt must be an array of content blocks`, {
184755
185692
  received: promptField
184756
185693
  }));
184757
185694
  }
184758
185695
  const state = registry.get(sessionId);
184759
185696
  if (state === undefined) {
184760
- refuse4({
185697
+ refuse5({
184761
185698
  code: JSON_RPC_ERROR_CODES.resourceNotFound,
184762
185699
  message: `Unknown ACP session: ${sessionId}`,
184763
185700
  data: { sessionId }
@@ -184849,7 +185786,7 @@ Switch with /model <model>; it applies from the next turn.`;
184849
185786
  const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionList);
184850
185787
  const rawCwd = obj["cwd"];
184851
185788
  if (rawCwd !== undefined && rawCwd !== null && typeof rawCwd !== "string") {
184852
- refuse4(invalidParams(`${ACP_AGENT_METHODS.sessionList}: cwd must be a string, null or omitted`, { received: rawCwd }));
185789
+ refuse5(invalidParams(`${ACP_AGENT_METHODS.sessionList}: cwd must be a string, null or omitted`, { received: rawCwd }));
184853
185790
  }
184854
185791
  const cwd = typeof rawCwd === "string" && rawCwd.length > 0 ? rawCwd : process.cwd();
184855
185792
  const summaries = listSessions(cwd, options.dataDir);
@@ -184915,14 +185852,14 @@ Switch with /model <model>; it applies from the next turn.`;
184915
185852
  if (!(cause instanceof AcpSessionTranscriptUnreadableError)) {
184916
185853
  throw cause;
184917
185854
  }
184918
- refuse4({
185855
+ refuse5({
184919
185856
  code: JSON_RPC_ERROR_CODES.internalError,
184920
185857
  message: `Cannot load session ${sessionId}: ${cause.cause.message}`,
184921
185858
  data: { sessionId, cwd, file: cause.cause.file, reason: cause.cause.reason }
184922
185859
  });
184923
185860
  }
184924
185861
  if (state === undefined) {
184925
- refuse4({
185862
+ refuse5({
184926
185863
  code: JSON_RPC_ERROR_CODES.resourceNotFound,
184927
185864
  message: `Unknown ACP session: ${sessionId}`,
184928
185865
  data: { sessionId, cwd }
@@ -184944,17 +185881,17 @@ Switch with /model <model>; it applies from the next turn.`;
184944
185881
  const configId = requireStringField(obj, "configId", method);
184945
185882
  const value = obj["value"];
184946
185883
  if (typeof value !== "string" || value.length === 0) {
184947
- refuse4(invalidParams(`${method}: value must be a non-empty string`, { received: value }));
185884
+ refuse5(invalidParams(`${method}: value must be a non-empty string`, { received: value }));
184948
185885
  }
184949
185886
  if (registry.get(sessionId) === undefined) {
184950
- refuse4({
185887
+ refuse5({
184951
185888
  code: JSON_RPC_ERROR_CODES.resourceNotFound,
184952
185889
  message: `Unknown ACP session: ${sessionId}`,
184953
185890
  data: { sessionId }
184954
185891
  });
184955
185892
  }
184956
185893
  if (configId !== ACP_MODEL_CONFIG_ID) {
184957
- refuse4(invalidParams(`${method}: keryx has no config option "${configId}"; the only one is "${ACP_MODEL_CONFIG_ID}"`, {
185894
+ refuse5(invalidParams(`${method}: keryx has no config option "${configId}"; the only one is "${ACP_MODEL_CONFIG_ID}"`, {
184958
185895
  configId
184959
185896
  }));
184960
185897
  }
@@ -186676,40 +187613,40 @@ var init_serve_turn = __esm(() => {
186676
187613
  });
186677
187614
 
186678
187615
  // src/lib/serve-server.ts
186679
- function refuse5(reason, message) {
187616
+ function refuse6(reason, message) {
186680
187617
  return { ok: false, state: "refused", reason, message };
186681
187618
  }
186682
187619
  function resolveServeStartup(input) {
186683
187620
  const { config, credential } = input;
186684
187621
  if (config === null) {
186685
- return refuse5("no-configuration", serveConfigAdvice(input.configState ?? "absent"));
187622
+ return refuse6("no-configuration", serveConfigAdvice(input.configState ?? "absent"));
186686
187623
  }
186687
187624
  if (!config.enabled) {
186688
- return refuse5("disabled", serveConfigAdvice("valid"));
187625
+ return refuse6("disabled", serveConfigAdvice("valid"));
186689
187626
  }
186690
187627
  if (config.credentialRef.store !== "auth-json") {
186691
- return refuse5("unsupported-credential-store", `credentialRef.store "${config.credentialRef.store}" is not implemented in this release; only "auth-json" is supported.`);
187628
+ return refuse6("unsupported-credential-store", `credentialRef.store "${config.credentialRef.store}" is not implemented in this release; only "auth-json" is supported.`);
186692
187629
  }
186693
187630
  if (credential.status === "unreadable") {
186694
- return refuse5("unreadable-credential", `${credential.message}. Inspect it, then run \`keryx serve token rotate\`.`);
187631
+ return refuse6("unreadable-credential", `${credential.message}. Inspect it, then run \`keryx serve token rotate\`.`);
186695
187632
  }
186696
187633
  if (credential.status === "absent") {
186697
- return refuse5("no-credential", "no serve credential exists. Run `keryx serve token issue` \u2014 the token is printed once and never again.");
187634
+ return refuse6("no-credential", "no serve credential exists. Run `keryx serve token issue` \u2014 the token is printed once and never again.");
186698
187635
  }
186699
187636
  if (credential.record.id !== config.credentialRef.id) {
186700
- return refuse5("no-credential", "the configured credential reference does not match the credential in the store. Run `keryx serve token rotate` to re-issue and re-point it.");
187637
+ return refuse6("no-credential", "the configured credential reference does not match the credential in the store. Run `keryx serve token rotate` to re-issue and re-point it.");
186701
187638
  }
186702
187639
  const nonLoopback = !isLoopbackAddress(config.bind.address);
186703
187640
  if (nonLoopback && config.bind.acknowledgeNonLoopback !== true) {
186704
- return refuse5("non-loopback-not-acknowledged", `the configured bind address is reachable beyond loopback. Run \`keryx serve config set --bind ${config.bind.address} --acknowledge-non-loopback\` to acknowledge it explicitly; there is no TLS in this release.`);
187641
+ return refuse6("non-loopback-not-acknowledged", `the configured bind address is reachable beyond loopback. Run \`keryx serve config set --bind ${config.bind.address} --acknowledge-non-loopback\` to acknowledge it explicitly; there is no TLS in this release.`);
186705
187642
  }
186706
187643
  const remoteProfile = resolveRemoteProfile(config.profile);
186707
187644
  if (remoteProfile === null) {
186708
- return refuse5("unknown-profile", `profile "${config.profile}" is not implemented in this release; valid profiles are ${REMOTE_PROFILE_NAMES.join(", ")}. Run \`keryx serve config set --profile remote-restricted\``);
187645
+ return refuse6("unknown-profile", `profile "${config.profile}" is not implemented in this release; valid profiles are ${REMOTE_PROFILE_NAMES.join(", ")}. Run \`keryx serve config set --profile remote-restricted\``);
186709
187646
  }
186710
187647
  const comparison = compareProfiles((input.localBaseline ?? localBaselineProfile)(), remoteProfile);
186711
187648
  if (!comparison.ok) {
186712
- return refuse5("widening-profile", `profile "${config.profile}" would grant more than the local profile allows (${comparison.widened.join(", ")}). Run \`keryx serve config set --profile remote-restricted\``);
187649
+ return refuse6("widening-profile", `profile "${config.profile}" would grant more than the local profile allows (${comparison.widened.join(", ")}). Run \`keryx serve config set --profile remote-restricted\``);
186713
187650
  }
186714
187651
  return { ok: true, config, credential: credential.record, nonLoopback, profile: remoteProfile };
186715
187652
  }
@@ -187057,11 +187994,11 @@ async function startServeListener(input) {
187057
187994
  error: internalErrorResponse
187058
187995
  });
187059
187996
  } catch (error) {
187060
- return refuse5("bind-failed", `could not bind ${hostname}:${startup.config.bind.port}: ${error instanceof Error ? error.message : String(error)}`);
187997
+ return refuse6("bind-failed", `could not bind ${hostname}:${startup.config.bind.port}: ${error instanceof Error ? error.message : String(error)}`);
187061
187998
  }
187062
187999
  if (typeof server.port !== "number" || server.port <= 0) {
187063
188000
  await server.stop(true);
187064
- return refuse5("bind-failed", "the listener reported no bound port");
188001
+ return refuse6("bind-failed", "the listener reported no bound port");
187065
188002
  }
187066
188003
  boundPort = server.port;
187067
188004
  try {
@@ -187111,6 +188048,8 @@ var init_serve_server = __esm(() => {
187111
188048
  ["heartbeat", "POST"],
187112
188049
  ["reply", "POST"],
187113
188050
  ["approval", "POST"],
188051
+ ["prompt", "POST"],
188052
+ ["state", "POST"],
187114
188053
  ["ack", "POST"],
187115
188054
  ["stream", "GET"],
187116
188055
  ["channels-status", "GET"],
@@ -187348,6 +188287,46 @@ function createHttpBotApi(options) {
187348
188287
  }
187349
188288
  return call("sendMessage", body);
187350
188289
  },
188290
+ async editMessageReplyMarkup(params) {
188291
+ await call("editMessageReplyMarkup", {
188292
+ chat_id: params.chatId,
188293
+ message_id: params.messageId,
188294
+ reply_markup: { inline_keyboard: params.inlineKeyboard ?? [] }
188295
+ });
188296
+ },
188297
+ async editMessageText(params) {
188298
+ const body = {
188299
+ chat_id: params.chatId,
188300
+ message_id: params.messageId,
188301
+ text: params.text,
188302
+ reply_markup: { inline_keyboard: params.inlineKeyboard ?? [] }
188303
+ };
188304
+ if (params.parseMode !== undefined) {
188305
+ body.parse_mode = params.parseMode;
188306
+ }
188307
+ await call("editMessageText", body);
188308
+ },
188309
+ async setMyCommands(params) {
188310
+ const body = { commands: params.commands };
188311
+ if (params.chatId !== undefined) {
188312
+ body.scope = { type: "chat", chat_id: params.chatId };
188313
+ }
188314
+ await call("setMyCommands", body);
188315
+ },
188316
+ async setMessageReaction(params) {
188317
+ await call("setMessageReaction", {
188318
+ chat_id: params.chatId,
188319
+ message_id: params.messageId,
188320
+ reaction: params.emoji === undefined ? [] : [{ type: "emoji", emoji: params.emoji }]
188321
+ });
188322
+ },
188323
+ async sendChatAction(params) {
188324
+ const body = { chat_id: params.chatId, action: params.action };
188325
+ if (params.messageThreadId !== undefined) {
188326
+ body.message_thread_id = params.messageThreadId;
188327
+ }
188328
+ await call("sendChatAction", body);
188329
+ },
187351
188330
  async createForumTopic(params) {
187352
188331
  return call("createForumTopic", { chat_id: params.chatId, name: params.name });
187353
188332
  },
@@ -187808,6 +188787,22 @@ async function sendHtml(api, params, onFallback) {
187808
188787
  return api.sendMessage(plain);
187809
188788
  }
187810
188789
  }
188790
+ async function editHtml(api, params, onFallback) {
188791
+ try {
188792
+ await api.editMessageText({ ...params, text: renderTelegramHtml(params.text), parseMode: "HTML" });
188793
+ } catch (error) {
188794
+ if (!isEntityParseError(error)) {
188795
+ throw error;
188796
+ }
188797
+ try {
188798
+ onFallback?.(error);
188799
+ } catch {}
188800
+ await api.editMessageText(params);
188801
+ }
188802
+ }
188803
+ function isNotModified(error) {
188804
+ return isBotApiError(error) && error.kind === "rejected" && /message is not modified/i.test(error.message);
188805
+ }
187811
188806
  var OUTSIDE, WORD, ALNUM, SPACE, SINGLE_OPEN_BAD, SINGLE_CLOSE_BAD, HTTP_URL, LANGUAGE, EXPANDABLE_QUOTE_LINES = 8, TAGS2, HEADING2, BULLET, QUOTE, SIMPLE_TAGS;
187812
188807
  var init_format_html = __esm(() => {
187813
188808
  init_format2();
@@ -188168,6 +189163,7 @@ class OutboundQueue {
188168
189163
  onDrop;
188169
189164
  onFallback;
188170
189165
  retryDelayMs;
189166
+ sentHooks = new Map;
188171
189167
  blockedUntil = 0;
188172
189168
  flushing;
188173
189169
  constructor(options) {
@@ -188208,8 +189204,12 @@ class OutboundQueue {
188208
189204
  ...message.keyboard === undefined || index !== parts.length - 1 ? {} : { keyboard: message.keyboard },
188209
189205
  createdAt: this.now()
188210
189206
  };
189207
+ if (message.onSent !== undefined && index === parts.length - 1) {
189208
+ this.sentHooks.set(entry.id, message.onSent);
189209
+ }
188211
189210
  const { dropped } = this.log.add(entry);
188212
189211
  for (const gone of dropped) {
189212
+ this.sentHooks.delete(gone.id);
188213
189213
  this.onDrop(gone, "outbound queue is full; oldest message dropped");
188214
189214
  }
188215
189215
  entries.push(entry);
@@ -188220,6 +189220,7 @@ class OutboundQueue {
188220
189220
  let removed = 0;
188221
189221
  for (const entry of this.log.pending()) {
188222
189222
  if (entry.chatId === chatId && entry.threadId === threadId) {
189223
+ this.sentHooks.delete(entry.id);
188223
189224
  this.log.ack(entry.id);
188224
189225
  removed += 1;
188225
189226
  }
@@ -188234,6 +189235,21 @@ class OutboundQueue {
188234
189235
  }
188235
189236
  return this.flushing;
188236
189237
  }
189238
+ notifySent(entry, messageId) {
189239
+ const hook = this.sentHooks.get(entry.id);
189240
+ if (hook === undefined) {
189241
+ return;
189242
+ }
189243
+ this.sentHooks.delete(entry.id);
189244
+ try {
189245
+ hook({
189246
+ messageId,
189247
+ chatId: entry.chatId,
189248
+ ...entry.threadId === undefined ? {} : { threadId: entry.threadId },
189249
+ text: entry.text
189250
+ });
189251
+ } catch {}
189252
+ }
188237
189253
  async run() {
188238
189254
  let sent = 0;
188239
189255
  let dropped = 0;
@@ -188247,7 +189263,7 @@ class OutboundQueue {
188247
189263
  return { sent, dropped, remaining: this.log.size, retryInMs: wait, stoppedBy: "rate limited" };
188248
189264
  }
188249
189265
  try {
188250
- await sendHtml(this.api, {
189266
+ const result = await sendHtml(this.api, {
188251
189267
  chatId: entry.chatId,
188252
189268
  text: entry.text,
188253
189269
  ...entry.threadId === undefined ? {} : { messageThreadId: entry.threadId },
@@ -188255,6 +189271,7 @@ class OutboundQueue {
188255
189271
  }, () => this.onFallback(entry));
188256
189272
  this.log.ack(entry.id);
188257
189273
  sent += 1;
189274
+ this.notifySent(entry, result.message_id);
188258
189275
  } catch (error) {
188259
189276
  const description = describe(error);
188260
189277
  if (isBotApiError(error) && error.kind === "rate-limited") {
@@ -188266,6 +189283,7 @@ class OutboundQueue {
188266
189283
  return { sent, dropped, remaining: this.log.size, retryInMs: this.retryDelayMs, stoppedBy: description };
188267
189284
  }
188268
189285
  this.log.ack(entry.id);
189286
+ this.sentHooks.delete(entry.id);
188269
189287
  dropped += 1;
188270
189288
  this.onDrop(entry, description);
188271
189289
  }
@@ -188285,6 +189303,24 @@ var init_outbound_queue = __esm(() => {
188285
189303
  init_types11();
188286
189304
  });
188287
189305
 
189306
+ // src/remote/message-state.ts
189307
+ function isReactionForbidden(status, message) {
189308
+ if (status === 403) {
189309
+ return true;
189310
+ }
189311
+ return /REACTION_INVALID|REACTIONS?_(DISABLED|NOT_ALLOWED)|not enough rights|reactions? (are|is) (disabled|not allowed)|CHAT_ADMIN_REQUIRED/i.test(message);
189312
+ }
189313
+ var REACTION_FOR_STATE, TYPING_REFRESH_MS = 4000, STATE_CALL_TIMEOUT_MS = 5000, MAX_TRACKED_MESSAGES = 256;
189314
+ var init_message_state = __esm(() => {
189315
+ REACTION_FOR_STATE = {
189316
+ received: "\uD83D\uDC40",
189317
+ reading: "\uD83E\uDD14",
189318
+ working: "\u26A1",
189319
+ done: "\uD83D\uDC4D",
189320
+ failed: "\uD83D\uDC4E"
189321
+ };
189322
+ });
189323
+
188288
189324
  // src/remote/poller.ts
188289
189325
  function defaultSleep2(ms, signal) {
188290
189326
  return new Promise((resolve) => {
@@ -188563,6 +189599,9 @@ class RemoteHub {
188563
189599
  flushTimer;
188564
189600
  sweepTimer;
188565
189601
  stopped = false;
189602
+ tracked = new Map;
189603
+ typing = new Map;
189604
+ reactionsOff = false;
188566
189605
  constructor(options) {
188567
189606
  this.api = options.api;
188568
189607
  this.config = options.config;
@@ -188636,6 +189675,14 @@ class RemoteHub {
188636
189675
  for (const record of this.registry.records()) {
188637
189676
  this.dispatch(nameKey(record.name));
188638
189677
  }
189678
+ await this.publishCommandMenu();
189679
+ }
189680
+ async publishCommandMenu() {
189681
+ try {
189682
+ await this.api.setMyCommands({ commands: remoteMenu(), chatId: this.config.chatId });
189683
+ } catch (error) {
189684
+ this.event("menu-failed", describeError4(error));
189685
+ }
188639
189686
  }
188640
189687
  async stop() {
188641
189688
  this.stopped = true;
@@ -188651,6 +189698,10 @@ class RemoteHub {
188651
189698
  this.timers.clearTimeout(handle);
188652
189699
  }
188653
189700
  this.retryTimers.clear();
189701
+ for (const handle of this.typing.values()) {
189702
+ this.timers.clearInterval(handle);
189703
+ }
189704
+ this.typing.clear();
188654
189705
  await this.poller.stop();
188655
189706
  }
188656
189707
  pollerStatus() {
@@ -188753,6 +189804,7 @@ class RemoteHub {
188753
189804
  record.unavailableSince = Math.min(now, record.lastHeartbeat + SESSION_LEASE_STALE_MS);
188754
189805
  this.registry.save();
188755
189806
  this.event("session-unavailable", record.name);
189807
+ this.endActivity(record.sessionId);
188756
189808
  if (now - record.unavailableSince < this.config.orphanMs) {
188757
189809
  this.queueStatus(record, `Session unavailable: the shell stopped responding. This topic is deleted in ${formatDuration(this.config.orphanMs)} unless the session returns.`);
188758
189810
  }
@@ -188773,11 +189825,31 @@ class RemoteHub {
188773
189825
  chatId: record.chatId,
188774
189826
  threadId: record.threadId,
188775
189827
  text,
188776
- ...options.keyboard === undefined ? {} : { keyboard: options.keyboard }
189828
+ ...options.keyboard === undefined ? {} : { keyboard: options.keyboard },
189829
+ ...options.onSent === undefined ? {} : { onSent: options.onSent }
188777
189830
  });
188778
189831
  await this.flushOutbound();
188779
189832
  return true;
188780
189833
  }
189834
+ async settleMessage(sessionId, messageId, text, shortReply = text) {
189835
+ const record = this.registry.bySession(sessionId);
189836
+ if (record === undefined) {
189837
+ return "gone";
189838
+ }
189839
+ try {
189840
+ await editHtml(this.api, { chatId: record.chatId, messageId, text }, () => this.event("format-fallback", `Telegram refused the formatting; edited as plain text (topic ${record.threadId})`));
189841
+ return "edited";
189842
+ } catch (error) {
189843
+ if (isNotModified(error)) {
189844
+ return "edited";
189845
+ }
189846
+ try {
189847
+ await this.api.editMessageReplyMarkup({ chatId: record.chatId, messageId });
189848
+ } catch {}
189849
+ await this.send(sessionId, shortReply);
189850
+ return "replied";
189851
+ }
189852
+ }
188781
189853
  async flushOutbound() {
188782
189854
  const result = await this.outbound.flush();
188783
189855
  if (result.retryInMs !== undefined && !this.stopped && this.flushTimer === undefined) {
@@ -188946,6 +190018,7 @@ class RemoteHub {
188946
190018
  }
188947
190019
  async retire(record, why, strict = false) {
188948
190020
  this.outbound.discardForThread(record.chatId, record.threadId);
190021
+ this.stopTyping(record);
188949
190022
  try {
188950
190023
  await this.api.deleteForumTopic({ chatId: record.chatId, messageThreadId: record.threadId });
188951
190024
  } catch (error) {
@@ -188993,7 +190066,11 @@ class RemoteHub {
188993
190066
  if (typeof message.text !== "string" || message.text.length === 0) {
188994
190067
  return;
188995
190068
  }
188996
- const appended = this.inbound.append(key, { ...base, kind: "text", text: message.text });
190069
+ const appended = this.inbound.append(key, { ...base, kind: "text", text: message.text, messageId: message.message_id });
190070
+ if (appended.added) {
190071
+ const tracked = this.track(record, update.update_id, message.message_id);
190072
+ this.react(tracked, "received");
190073
+ }
188997
190074
  return { key, dropped: appended.dropped };
188998
190075
  }
188999
190076
  if (query?.data === undefined) {
@@ -189006,6 +190083,108 @@ class RemoteHub {
189006
190083
  });
189007
190084
  return { key, dropped: appended.dropped, callbackQueryId: query.id };
189008
190085
  }
190086
+ trackKey(threadId, updateId) {
190087
+ return `${threadId}:${updateId}`;
190088
+ }
190089
+ track(record, updateId, messageId) {
190090
+ const key = this.trackKey(record.threadId, updateId);
190091
+ const existing = this.tracked.get(key);
190092
+ if (existing !== undefined) {
190093
+ return existing;
190094
+ }
190095
+ const entry = { chatId: record.chatId, threadId: record.threadId, messageId, state: "received", chain: Promise.resolve() };
190096
+ this.tracked.set(key, entry);
190097
+ while (this.tracked.size > MAX_TRACKED_MESSAGES) {
190098
+ const oldest = this.tracked.keys().next().value;
190099
+ if (oldest === undefined) {
190100
+ break;
190101
+ }
190102
+ this.tracked.delete(oldest);
190103
+ }
190104
+ return entry;
190105
+ }
190106
+ react(message, state) {
190107
+ message.state = state;
190108
+ if (this.reactionsOff || this.stopped) {
190109
+ return;
190110
+ }
190111
+ message.chain = message.chain.then(async () => {
190112
+ if (this.reactionsOff) {
190113
+ return;
190114
+ }
190115
+ let timer;
190116
+ const timeout = new Promise((resolve) => {
190117
+ timer = this.timers.setTimeout(resolve, STATE_CALL_TIMEOUT_MS);
190118
+ });
190119
+ const call = this.api.setMessageReaction({ chatId: message.chatId, messageId: message.messageId, emoji: REACTION_FOR_STATE[state] }).then(() => {
190120
+ return;
190121
+ }, (error) => this.reactionFailed(error));
190122
+ await Promise.race([call, timeout]);
190123
+ this.timers.clearTimeout(timer);
190124
+ });
190125
+ }
190126
+ reactionFailed(error) {
190127
+ if (!isBotApiError(error) || error.kind !== "rejected" || !isReactionForbidden(error.status, error.message)) {
190128
+ return;
190129
+ }
190130
+ if (!this.reactionsOff) {
190131
+ this.reactionsOff = true;
190132
+ this.event("reactions-unavailable", `this bot may not react in the group; the topic shows typing only (${describeError4(error)})`);
190133
+ }
190134
+ }
190135
+ messageState(sessionId, updateId, state) {
190136
+ const record = this.registry.bySession(sessionId);
190137
+ if (record === undefined) {
190138
+ return false;
190139
+ }
190140
+ const message = this.tracked.get(this.trackKey(record.threadId, updateId));
190141
+ if (message === undefined) {
190142
+ return false;
190143
+ }
190144
+ this.react(message, state);
190145
+ if (state === "working") {
190146
+ this.startTyping(record);
190147
+ } else if (state === "done" || state === "failed") {
190148
+ this.stopTyping(record);
190149
+ }
190150
+ return true;
190151
+ }
190152
+ endActivity(sessionId) {
190153
+ const record = this.registry.bySession(sessionId);
190154
+ if (record === undefined) {
190155
+ return;
190156
+ }
190157
+ this.stopTyping(record);
190158
+ for (const message of this.tracked.values()) {
190159
+ if (message.threadId === record.threadId && (message.state === "reading" || message.state === "working")) {
190160
+ this.react(message, "failed");
190161
+ }
190162
+ }
190163
+ }
190164
+ typingKey(record) {
190165
+ return `${record.chatId}:${record.threadId}`;
190166
+ }
190167
+ startTyping(record) {
190168
+ const key = this.typingKey(record);
190169
+ if (this.typing.has(key) || this.stopped) {
190170
+ return;
190171
+ }
190172
+ const send = () => {
190173
+ this.api.sendChatAction({ chatId: record.chatId, action: "typing", messageThreadId: record.threadId }).catch(() => {
190174
+ return;
190175
+ });
190176
+ };
190177
+ send();
190178
+ this.typing.set(key, this.timers.setInterval(send, TYPING_REFRESH_MS));
190179
+ }
190180
+ stopTyping(record) {
190181
+ const key = this.typingKey(record);
190182
+ const handle = this.typing.get(key);
190183
+ if (handle !== undefined) {
190184
+ this.timers.clearInterval(handle);
190185
+ this.typing.delete(key);
190186
+ }
190187
+ }
189009
190188
  dispatch(key) {
189010
190189
  const running = this.dispatching.get(key);
189011
190190
  if (running !== undefined) {
@@ -189028,6 +190207,9 @@ class RemoteHub {
189028
190207
  }
189029
190208
  try {
189030
190209
  if (entry.kind === "text") {
190210
+ if (entry.messageId !== undefined) {
190211
+ this.track(record, entry.updateId, entry.messageId);
190212
+ }
189031
190213
  await this.consumer.deliver(record.sessionId, entry.text ?? "", {
189032
190214
  updateId: entry.updateId,
189033
190215
  threadId: record.threadId,
@@ -189073,6 +190255,8 @@ var init_hub = __esm(() => {
189073
190255
  init_journal2();
189074
190256
  init_naming();
189075
190257
  init_outbound_queue();
190258
+ init_command_gateway();
190259
+ init_message_state();
189076
190260
  init_poller();
189077
190261
  init_registry6();
189078
190262
  init_types11();
@@ -189186,6 +190370,7 @@ class ShellStream {
189186
190370
  onEnded;
189187
190371
  controller;
189188
190372
  closed = false;
190373
+ endedBecause;
189189
190374
  keepalive;
189190
190375
  encoder = new TextEncoder;
189191
190376
  constructor(sessionId, timers, onEnded) {
@@ -189209,6 +190394,7 @@ class ShellStream {
189209
190394
  if (this.closed) {
189210
190395
  return;
189211
190396
  }
190397
+ this.endedBecause = kind;
189212
190398
  if (kind !== undefined) {
189213
190399
  this.write(encodeSseEvent("status", { kind }));
189214
190400
  }
@@ -189229,6 +190415,18 @@ class ShellStream {
189229
190415
  this.onEnded(this);
189230
190416
  }
189231
190417
  }
190418
+ function clockText(at) {
190419
+ return `${new Date(at).toISOString().slice(11, 19)} UTC`;
190420
+ }
190421
+ function settledText(original, result) {
190422
+ if (original === undefined || original.length === 0) {
190423
+ return result;
190424
+ }
190425
+ const kept = `${original}
190426
+
190427
+ ${result}`;
190428
+ return kept.length <= MAX_SETTLED_TEXT_CHARS ? kept : result;
190429
+ }
189232
190430
  function isChannelsRoute(route) {
189233
190431
  return Object.hasOwn(CHANNELS_ROUTE_METHODS, route);
189234
190432
  }
@@ -189245,12 +190443,15 @@ class RemoteHttpSurface {
189245
190443
  keepaliveMs;
189246
190444
  ackTimeoutMs;
189247
190445
  newApprovalId;
190446
+ newPromptId;
189248
190447
  streams = new Map;
189249
190448
  completed = new Map;
189250
190449
  sent = new Map;
189251
190450
  bound = new Map;
189252
190451
  pendingAcks = new Map;
189253
190452
  approvals = new Map;
190453
+ choices = new Map;
190454
+ finished = new Map;
189254
190455
  constructor(options) {
189255
190456
  this.options = options;
189256
190457
  this.now = options.now ?? Date.now;
@@ -189258,6 +190459,7 @@ class RemoteHttpSurface {
189258
190459
  this.keepaliveMs = options.keepaliveMs ?? DEFAULT_KEEPALIVE_MS;
189259
190460
  this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
189260
190461
  this.newApprovalId = options.approvalIds ?? (() => `ap${randomBytes14(6).toString("hex")}`);
190462
+ this.newPromptId = options.promptIds ?? (() => `pk${randomBytes14(6).toString("hex")}`);
189261
190463
  this.consumer = {
189262
190464
  deliver: (sessionId, line, meta) => this.deliver(sessionId, line, meta),
189263
190465
  deliverCallback: (sessionId, callback) => this.deliverCallback(sessionId, callback)
@@ -189316,6 +190518,10 @@ class RemoteHttpSurface {
189316
190518
  return this.reply(hub, request);
189317
190519
  case "approval":
189318
190520
  return this.requestApproval(hub, request);
190521
+ case "prompt":
190522
+ return this.requestPrompt(hub, request);
190523
+ case "state":
190524
+ return this.messageState(hub, request);
189319
190525
  case "ack":
189320
190526
  return this.ack(hub, request);
189321
190527
  case "stream":
@@ -189336,6 +190542,9 @@ class RemoteHttpSurface {
189336
190542
  for (const id of [...this.approvals.keys()]) {
189337
190543
  this.dropApproval(id);
189338
190544
  }
190545
+ for (const id of [...this.choices.keys()]) {
190546
+ this.dropChoice(id);
190547
+ }
189339
190548
  }
189340
190549
  setChannels(handler) {
189341
190550
  this.channels = handler;
@@ -189443,6 +190652,9 @@ class RemoteHttpSurface {
189443
190652
  if (data.startsWith(RESERVED_CALLBACK_PREFIX)) {
189444
190653
  return { ok: false, message: `button data may not start with "${RESERVED_CALLBACK_PREFIX}" (reserved for approvals).` };
189445
190654
  }
190655
+ if (data.startsWith(RESERVED_CHOICE_PREFIX)) {
190656
+ return { ok: false, message: `button data may not start with "${RESERVED_CHOICE_PREFIX}" (reserved for pickers).` };
190657
+ }
189446
190658
  out.push({ text: redactSensitiveText(text), callback_data: data });
189447
190659
  }
189448
190660
  keyboard.push(out);
@@ -189521,7 +190733,8 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
189521
190733
  { text: "Allow", callback_data: approvalCallbackData(approvalId, "allow") },
189522
190734
  { text: "Deny", callback_data: approvalCallbackData(approvalId, "deny") }
189523
190735
  ]
189524
- ]
190736
+ ],
190737
+ onSent: (info) => this.onApprovalSent(approvalId, sessionId, info.messageId, info.text)
189525
190738
  });
189526
190739
  if (!sent) {
189527
190740
  this.dropApproval(approvalId);
@@ -189529,6 +190742,111 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
189529
190742
  }
189530
190743
  return ok2({ approvalId, expiresAt });
189531
190744
  }
190745
+ async requestPrompt(hub, request) {
190746
+ const body = await readJsonBody(request);
190747
+ if (!body.ok) {
190748
+ return body.response;
190749
+ }
190750
+ const value = body.value;
190751
+ if (!isSessionId(value.sessionId)) {
190752
+ return invalid2("sessionId must be 1 to 64 characters of letters, digits, '-' and '_'.");
190753
+ }
190754
+ if (typeof value.text !== "string" || value.text.length === 0 || value.text.length > MAX_APPROVAL_PROMPT_CHARS) {
190755
+ return invalid2(`text must be a string of 1 to ${MAX_APPROVAL_PROMPT_CHARS} characters.`);
190756
+ }
190757
+ const timeoutMs = value.timeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
190758
+ if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < MIN_APPROVAL_TIMEOUT_MS || timeoutMs > MAX_APPROVAL_TIMEOUT_MS) {
190759
+ return invalid2(`timeoutMs must be an integer from ${MIN_APPROVAL_TIMEOUT_MS} to ${MAX_APPROVAL_TIMEOUT_MS}.`);
190760
+ }
190761
+ if (value.forUserId !== undefined && (typeof value.forUserId !== "number" || !Number.isSafeInteger(value.forUserId))) {
190762
+ return invalid2("forUserId must be an integer.");
190763
+ }
190764
+ const rows = value.rows;
190765
+ if (!Array.isArray(rows) || rows.length === 0 || rows.length > MAX_KEYBOARD_ROWS) {
190766
+ return invalid2(`rows must be 1 to ${MAX_KEYBOARD_ROWS} rows of button labels.`);
190767
+ }
190768
+ const labels = [];
190769
+ const layout = [];
190770
+ for (const row of rows) {
190771
+ if (!Array.isArray(row) || row.length === 0 || row.length > MAX_KEYBOARD_BUTTONS_PER_ROW) {
190772
+ return invalid2(`each row must hold 1 to ${MAX_KEYBOARD_BUTTONS_PER_ROW} labels.`);
190773
+ }
190774
+ for (const label of row) {
190775
+ if (typeof label !== "string" || label.length === 0 || label.length > MAX_BUTTON_TEXT_CHARS) {
190776
+ return invalid2(`a label must be 1 to ${MAX_BUTTON_TEXT_CHARS} characters.`);
190777
+ }
190778
+ labels.push(redactSensitiveText(label));
190779
+ }
190780
+ layout.push(row.length);
190781
+ }
190782
+ if (labels.length > MAX_PROMPT_BUTTONS) {
190783
+ return invalid2(`a prompt may carry at most ${MAX_PROMPT_BUTTONS} buttons.`);
190784
+ }
190785
+ const sessionId = value.sessionId;
190786
+ if (!hub.hasSession(sessionId)) {
190787
+ return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
190788
+ }
190789
+ if (this.streams.get(sessionId) === undefined) {
190790
+ return fail4(409, "no-stream", "No stream is connected for this session; the prompt cannot be answered.");
190791
+ }
190792
+ let held = 0;
190793
+ for (const entry of this.choices.values()) {
190794
+ if (entry.sessionId === sessionId) {
190795
+ held += 1;
190796
+ }
190797
+ }
190798
+ if (held >= MAX_PENDING_CHOICES_PER_SESSION) {
190799
+ return fail4(429, "too-many-prompts", "Too many questions are waiting for this session.");
190800
+ }
190801
+ const promptId = this.newPromptId();
190802
+ const expiresAt = this.now() + timeoutMs;
190803
+ this.choices.set(promptId, {
190804
+ sessionId,
190805
+ expiresAt,
190806
+ labels,
190807
+ ...value.forUserId === undefined ? {} : { forUserId: value.forUserId },
190808
+ timer: this.timers.setTimeout(() => this.expireChoice(promptId), timeoutMs)
190809
+ });
190810
+ const keyboard = [];
190811
+ let position = 0;
190812
+ for (const width of layout) {
190813
+ const line = [];
190814
+ for (let column = 0;column < width; column += 1) {
190815
+ line.push({ text: labels[position], callback_data: choiceCallbackData(promptId, position) });
190816
+ position += 1;
190817
+ }
190818
+ keyboard.push(line);
190819
+ }
190820
+ const sent = await hub.send(sessionId, redactSensitiveText(value.text), {
190821
+ keyboard,
190822
+ onSent: (info) => this.onChoiceSent(promptId, sessionId, info.messageId, info.text)
190823
+ });
190824
+ if (!sent) {
190825
+ this.dropChoice(promptId);
190826
+ return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
190827
+ }
190828
+ return ok2({ promptId, expiresAt });
190829
+ }
190830
+ async messageState(hub, request) {
190831
+ const body = await readJsonBody(request);
190832
+ if (!body.ok) {
190833
+ return body.response;
190834
+ }
190835
+ const value = body.value;
190836
+ if (!isSessionId(value.sessionId)) {
190837
+ return invalid2("sessionId must be 1 to 64 characters of letters, digits, '-' and '_'.");
190838
+ }
190839
+ if (typeof value.updateId !== "number" || !Number.isSafeInteger(value.updateId) || value.updateId < 0) {
190840
+ return invalid2("updateId must be a non-negative integer.");
190841
+ }
190842
+ if (!isMessageState(value.state)) {
190843
+ return invalid2("state must be one of reading, working, done, failed.");
190844
+ }
190845
+ if (!hub.hasSession(value.sessionId)) {
190846
+ return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
190847
+ }
190848
+ return ok2({ shown: hub.messageState(value.sessionId, value.updateId, value.state) });
190849
+ }
189532
190850
  async ack(hub, request) {
189533
190851
  const body = await readJsonBody(request);
189534
190852
  if (!body.ok) {
@@ -189666,15 +190984,12 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
189666
190984
  const stream = this.streams.get(sessionId);
189667
190985
  const approval = parseApprovalCallback(callback.data);
189668
190986
  if (approval !== undefined) {
189669
- const entry = this.approvals.get(approval.approvalId);
189670
- if (entry !== undefined && entry.sessionId === sessionId && entry.expiresAt > this.now()) {
189671
- this.dropApproval(approval.approvalId);
189672
- const event = { updateId: callback.updateId, approvalId: approval.approvalId, decision: approval.decision };
189673
- const told = stream?.write(encodeSseEvent("approval", event, callback.updateId)) === true;
189674
- if (told) {
189675
- this.noteDecision(sessionId, approval.decision);
189676
- }
189677
- }
190987
+ this.pressApproval(sessionId, callback, approval, stream);
190988
+ return Promise.resolve();
190989
+ }
190990
+ const choice = parseChoiceCallback(callback.data);
190991
+ if (choice !== undefined) {
190992
+ this.pressChoice(sessionId, callback, choice, stream);
189678
190993
  return Promise.resolve();
189679
190994
  }
189680
190995
  const event = {
@@ -189687,8 +191002,181 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
189687
191002
  stream?.write(encodeSseEvent("callback", event, callback.updateId));
189688
191003
  return Promise.resolve();
189689
191004
  }
189690
- async noteDecision(sessionId, decision) {
189691
- await this.hub?.send(sessionId, decision === "allow" ? "Approval granted." : "Approval denied.").catch(() => {
191005
+ pressApproval(sessionId, callback, approval, stream) {
191006
+ const id = approval.approvalId;
191007
+ const entry = this.approvals.get(id);
191008
+ if (entry !== undefined && entry.sessionId !== sessionId) {
191009
+ return;
191010
+ }
191011
+ if (entry !== undefined && entry.expiresAt > this.now()) {
191012
+ this.dropApproval(id);
191013
+ const event = { updateId: callback.updateId, approvalId: id, decision: approval.decision };
191014
+ const told = stream?.write(encodeSseEvent("approval", event, callback.updateId)) === true;
191015
+ const messageId = entry.messageId ?? callback.messageId;
191016
+ const when = clockText(this.now());
191017
+ if (!told) {
191018
+ this.finishApproval(id, sessionId, messageId, entry.text, {
191019
+ result: `Not delivered at ${when}: the shell is not connected, so nothing was approved.`,
191020
+ shortReply: "Approval not delivered: the shell is not connected, so nothing was approved."
191021
+ });
191022
+ return;
191023
+ }
191024
+ const who = `user ${callback.fromId}`;
191025
+ this.finishApproval(id, sessionId, messageId, entry.text, {
191026
+ result: approval.decision === "allow" ? `Allowed by ${who} at ${when}.` : `Denied by ${who} at ${when}.`,
191027
+ shortReply: approval.decision === "allow" ? "Approval granted." : "Approval denied."
191028
+ });
191029
+ return;
191030
+ }
191031
+ if (entry !== undefined) {
191032
+ this.expireApproval(id);
191033
+ return;
191034
+ }
191035
+ const done = this.finished.get(id);
191036
+ if (done !== undefined) {
191037
+ if (done.sessionId === sessionId) {
191038
+ const messageId = callback.messageId ?? done.messageId;
191039
+ if (messageId !== undefined) {
191040
+ this.hub?.settleMessage(sessionId, messageId, done.finalText, done.shortReply).catch(() => {
191041
+ return;
191042
+ });
191043
+ }
191044
+ }
191045
+ return;
191046
+ }
191047
+ const pressed = callback.messageId;
191048
+ if (pressed !== undefined && !this.carriesLivePrompt(pressed)) {
191049
+ this.hub?.settleMessage(sessionId, pressed, "This request is no longer active.", "That request is no longer active.").catch(() => {
191050
+ return;
191051
+ });
191052
+ }
191053
+ }
191054
+ carriesLivePrompt(messageId) {
191055
+ return [...this.approvals.values(), ...this.choices.values()].some((live) => live.messageId === messageId);
191056
+ }
191057
+ pressChoice(sessionId, callback, choice, stream) {
191058
+ const id = choice.promptId;
191059
+ const entry = this.choices.get(id);
191060
+ if (entry !== undefined && entry.sessionId !== sessionId) {
191061
+ return;
191062
+ }
191063
+ if (entry !== undefined && entry.expiresAt > this.now()) {
191064
+ const sameMessage = entry.messageId !== undefined && callback.messageId === entry.messageId;
191065
+ const sameUser = entry.forUserId === undefined || entry.forUserId === callback.fromId;
191066
+ if (!sameMessage || !sameUser || choice.index >= entry.labels.length) {
191067
+ return;
191068
+ }
191069
+ this.dropChoice(id);
191070
+ const event = { updateId: callback.updateId, promptId: id, index: choice.index, fromId: callback.fromId };
191071
+ const told = stream?.write(encodeSseEvent("choice", event, callback.updateId)) === true;
191072
+ const when = clockText(this.now());
191073
+ if (!told) {
191074
+ this.finishApproval(id, sessionId, entry.messageId, entry.text, {
191075
+ result: `Not delivered at ${when}: the shell is not connected, so nothing changed.`,
191076
+ shortReply: "Not delivered: the shell is not connected, so nothing changed."
191077
+ });
191078
+ return;
191079
+ }
191080
+ this.finishApproval(id, sessionId, entry.messageId, entry.text, {
191081
+ result: `Chosen: ${entry.labels[choice.index]} (user ${callback.fromId}, ${when}).`,
191082
+ shortReply: `Chosen: ${entry.labels[choice.index]}.`
191083
+ });
191084
+ return;
191085
+ }
191086
+ if (entry !== undefined) {
191087
+ this.expireChoice(id);
191088
+ return;
191089
+ }
191090
+ const done = this.finished.get(id);
191091
+ if (done !== undefined) {
191092
+ if (done.sessionId === sessionId) {
191093
+ const messageId = callback.messageId ?? done.messageId;
191094
+ if (messageId !== undefined) {
191095
+ this.hub?.settleMessage(sessionId, messageId, done.finalText, done.shortReply).catch(() => {
191096
+ return;
191097
+ });
191098
+ }
191099
+ }
191100
+ return;
191101
+ }
191102
+ const pressed = callback.messageId;
191103
+ if (pressed !== undefined && !this.carriesLivePrompt(pressed)) {
191104
+ this.hub?.settleMessage(sessionId, pressed, "This request is no longer active.", "That request is no longer active.").catch(() => {
191105
+ return;
191106
+ });
191107
+ }
191108
+ }
191109
+ onChoiceSent(promptId, sessionId, messageId, text) {
191110
+ const live = this.choices.get(promptId);
191111
+ if (live !== undefined) {
191112
+ live.messageId = messageId;
191113
+ live.text = text;
191114
+ return;
191115
+ }
191116
+ this.onApprovalSent(promptId, sessionId, messageId, text);
191117
+ }
191118
+ dropChoice(promptId) {
191119
+ const entry = this.choices.get(promptId);
191120
+ if (entry === undefined) {
191121
+ return;
191122
+ }
191123
+ this.timers.clearTimeout(entry.timer);
191124
+ this.choices.delete(promptId);
191125
+ }
191126
+ expireChoice(promptId) {
191127
+ const entry = this.choices.get(promptId);
191128
+ if (entry === undefined) {
191129
+ return;
191130
+ }
191131
+ this.dropChoice(promptId);
191132
+ this.finishApproval(promptId, entry.sessionId, entry.messageId, entry.text, {
191133
+ result: `Expired at ${clockText(this.now())}: no answer, nothing changed.`,
191134
+ shortReply: "That question expired: no answer, nothing changed."
191135
+ });
191136
+ }
191137
+ onApprovalSent(approvalId, sessionId, messageId, text) {
191138
+ const live = this.approvals.get(approvalId);
191139
+ if (live !== undefined) {
191140
+ live.messageId = messageId;
191141
+ live.text = text;
191142
+ return;
191143
+ }
191144
+ const done = this.finished.get(approvalId);
191145
+ if (done !== undefined && done.messageId === undefined) {
191146
+ done.messageId = messageId;
191147
+ done.finalText = settledText(text, done.finalText);
191148
+ this.hub?.settleMessage(sessionId, messageId, done.finalText, done.shortReply).catch(() => {
191149
+ return;
191150
+ });
191151
+ }
191152
+ }
191153
+ finishApproval(approvalId, sessionId, messageId, original, outcome) {
191154
+ const finalText = messageId === undefined ? outcome.result : settledText(original, outcome.result);
191155
+ this.finished.delete(approvalId);
191156
+ this.finished.set(approvalId, {
191157
+ sessionId,
191158
+ ...messageId === undefined ? {} : { messageId },
191159
+ finalText,
191160
+ shortReply: outcome.shortReply
191161
+ });
191162
+ while (this.finished.size > MAX_FINISHED_PROMPTS) {
191163
+ const oldest = this.finished.keys().next().value;
191164
+ if (oldest === undefined) {
191165
+ break;
191166
+ }
191167
+ this.finished.delete(oldest);
191168
+ }
191169
+ const hub = this.hub;
191170
+ if (hub === undefined) {
191171
+ return;
191172
+ }
191173
+ if (messageId === undefined) {
191174
+ hub.send(sessionId, outcome.shortReply).catch(() => {
191175
+ return;
191176
+ });
191177
+ return;
191178
+ }
191179
+ hub.settleMessage(sessionId, messageId, finalText, outcome.shortReply).catch(() => {
189692
191180
  return;
189693
191181
  });
189694
191182
  }
@@ -189705,9 +191193,11 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
189705
191193
  if (entry === undefined) {
189706
191194
  return;
189707
191195
  }
191196
+ this.timers.clearTimeout(entry.timer);
189708
191197
  this.approvals.delete(approvalId);
189709
- this.hub?.send(entry.sessionId, "Approval request expired: no answer, so it was denied.").catch(() => {
189710
- return;
191198
+ this.finishApproval(approvalId, entry.sessionId, entry.messageId, entry.text, {
191199
+ result: `Expired at ${clockText(this.now())}: no answer, so it was denied.`,
191200
+ shortReply: "Approval request expired: no answer, so it was denied."
189711
191201
  });
189712
191202
  }
189713
191203
  onStreamEnded(stream) {
@@ -189726,8 +191216,28 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
189726
191216
  for (const [id, entry] of [...this.approvals.entries()]) {
189727
191217
  if (entry.sessionId === stream.sessionId) {
189728
191218
  this.dropApproval(id);
191219
+ if (!this.closed && entry.messageId !== undefined) {
191220
+ this.finishApproval(id, stream.sessionId, entry.messageId, entry.text, {
191221
+ result: `Cancelled at ${clockText(this.now())}: the shell disconnected, so it was denied.`,
191222
+ shortReply: "Approval cancelled: the shell disconnected, so it was denied."
191223
+ });
191224
+ }
191225
+ }
191226
+ }
191227
+ for (const [id, entry] of [...this.choices.entries()]) {
191228
+ if (entry.sessionId === stream.sessionId) {
191229
+ this.dropChoice(id);
191230
+ if (!this.closed && entry.messageId !== undefined) {
191231
+ this.finishApproval(id, stream.sessionId, entry.messageId, entry.text, {
191232
+ result: `Cancelled at ${clockText(this.now())}: the shell disconnected, nothing changed.`,
191233
+ shortReply: "Question cancelled: the shell disconnected, nothing changed."
191234
+ });
191235
+ }
189729
191236
  }
189730
191237
  }
191238
+ if (stream.endedBecause === "closing") {
191239
+ this.hub?.endActivity(stream.sessionId);
191240
+ }
189731
191241
  }
189732
191242
  }
189733
191243
  function asCodeBlock(text) {
@@ -189739,7 +191249,7 @@ function asCodeBlock(text) {
189739
191249
  ${safe}
189740
191250
  ${fence}`;
189741
191251
  }
189742
- var MAX_REPLY_CHARS = 20000, MAX_PENDING_APPROVALS_PER_SESSION = 8, MAX_REMEMBERED_IDS_PER_SESSION = 512, MAX_PROJECT_NAME_CHARS = 128, CONTROL_CHARS2, JSON_HEADERS2, MAX_BACKTICK_RUN = 8;
191252
+ var MAX_REPLY_CHARS = 20000, MAX_PENDING_APPROVALS_PER_SESSION = 8, MAX_PENDING_CHOICES_PER_SESSION = 8, MAX_REMEMBERED_IDS_PER_SESSION = 512, MAX_PROJECT_NAME_CHARS = 128, CONTROL_CHARS2, JSON_HEADERS2, MAX_FINISHED_PROMPTS = 128, MAX_SETTLED_TEXT_CHARS = 3600, MAX_BACKTICK_RUN = 8;
189743
191253
  var init_http_surface = __esm(() => {
189744
191254
  init_service2();
189745
191255
  init_hub();