@dashup/connector 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,10 +34,10 @@ Each history read stages a new encrypted database snapshot and publishes it only
34
34
 
35
35
  Dashup discovers existing local OpenCode listeners, Codex control sockets, and VS Code Agent Host endpoints. These connections remain local; their tokens are never sent to Parcae or the phone. Native endpoints are checked for local ownership or loopback access, and redirects and remote endpoint URLs are rejected.
36
36
 
37
- - **OpenCode:** the existing TUI/server supplies conversations, live events, prompts, native tool permissions, and supported single-choice questions. A reply targets the original session ID and preserves its latest native agent, model, and variant. For an existing session, Dashup does not run `opencode serve` or create another one.
37
+ - **OpenCode:** the existing TUI/server supplies conversations, live events, prompts, native tool permissions, and questions, which the phone answers or skips. A reply targets the original session ID and preserves its latest native agent, model, and variant. For an existing session, Dashup does not run `opencode serve` or create another one.
38
38
  - **Codex:** the existing control socket supplies already-loaded threads. Replies require a recognized native server and a thread that is still loaded and idle immediately before sending. For an existing thread, Dashup never calls `thread/start`, `thread/resume`, or `session/load`. Approval controls appear only for native requests routed to this connection; approvals owned by another client may remain available only in the original UI.
39
39
  - **VS Code:** Microsoft's official AHP client subscribes to existing host sessions and chats. Native choices are retained, and delivery is acknowledged by the host. Older extension-host chats without an accessible Agent Host remain saved history. AHP chat IDs are namespaced rather than guessed to equal CLI thread IDs. Non-file remote AHP workspace URIs are not presented as local execution environments.
40
- - **Claude Code:** existing transcripts are readable without starting Claude. `dashup install` adds Dashup's plugin to Claude Code (`dashup@dashup`, read in place from this package's `claude/` folder), and `dashup uninstall` removes it. A session that starts with the plugin holds a stream open to `~/.dashup/claude.sock` through `curl`, and takes each reply as the person's own words; a reply that starts with `/` runs that command. Claude Code's inbox socket is not used, because Claude presents anything posted there as another session's message, without the person's authority. Sessions opened before the plugin was installed, and sessions on Windows, stay read-only.
40
+ - **Claude Code:** existing transcripts are readable without starting Claude. `dashup install` adds Dashup's plugin to Claude Code (`dashup@dashup`, read in place from this package's `claude/` folder), and `dashup uninstall` removes it. A session that starts with the plugin holds a stream open to `~/.dashup/claude.sock` through `curl`, and takes each reply as the person's own words; a reply that starts with `/` runs that command. The plugin also puts each permission request the session waits on to the phone: a tool to allow once or under the rule Claude Code suggests, questions, or a plan. The dialog on the computer, or the editor hosting the session, stays open, and the first answer wins. Claude Code lets only the person there or its host approve a plan, so the phone can only send a running session's plan back with what to change. Claude Code's inbox socket is not used, because Claude presents anything posted there as another session's message, without the person's authority. Sessions opened before the plugin was installed, and sessions on Windows, stay read-only.
41
41
 
42
42
  Native listener discovery supports the host's available process/socket interfaces. File and socket formats are versioned implementation boundaries; unavailable or unfamiliar interfaces leave history readable and controls disabled. Existing editor and agent processes continue independently when the Dashup connector disconnects. The PC's saved SSH relationship is retained; reading it does not grant access to a remote machine's CLI store.
43
43
 
@@ -55,7 +55,7 @@ Live discovery is bounded to 100 sessions per endpoint. OpenCode supplies the la
55
55
 
56
56
  ## Starting sessions from the phone
57
57
 
58
- The phone starts a session in a local project, or in a new folder inside one of the folders that already hold this computer's projects, with Claude Code, Codex or OpenCode: whichever of `claude`, `codex` and `opencode` runs here. The agent works with its own settings and permission rules, and asks the phone before anything they don't allow. Claude Code runs headless (`claude -p`) for each turn and resumes the session for the next; Codex threads start in Codex's own app-server daemon (`codex app-server daemon start`) and outlive the connector; OpenCode sessions run on an `opencode serve` the connector starts for that folder. Claude Code's permission requests arrive with the command, or the diff an edit or write would make.
58
+ The phone starts a session in a local project, or in a new folder inside one of the folders that already hold this computer's projects, with Claude Code, Codex or OpenCode: whichever of `claude`, `codex` and `opencode` runs here. The agent works with its own settings and permission rules, and asks the phone before anything they don't allow. Claude Code runs headless (`claude -p`) for each turn and resumes the session for the next; Codex threads start in Codex's own app-server daemon (`codex app-server daemon start`) and outlive the connector; OpenCode sessions run on an `opencode serve` the connector starts for that folder. Claude Code's permission requests arrive with the command, or the diff an edit or write would make, and the rule that would stop it asking again; the phone answers its questions and plans as Claude Code's host.
59
59
 
60
60
  A saved conversation is answered the same way when its agent runs here, its project is local, and nothing else has it open: Claude Code resumes it (`claude -p --resume`), Codex loads it in its daemon (`thread/resume`), and OpenCode serves its folder. One a native Codex process owns takes queued messages instead. A start expires with other commands after 60 seconds.
61
61
 
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "name": "dashup",
3
- "description": "Takes what you send this session from the Dashup app as your own words"
3
+ "description": "Takes what you send this session from the Dashup app as your own words, and lets you answer its approvals, questions and plans there"
4
4
  }
@@ -13,9 +13,85 @@ type Delivery = { text?: string; now?: true };
13
13
 
14
14
  const RETRY_MS = 10_000;
15
15
 
16
+ // =============================================================================
17
+ // Permission requests
18
+ // =============================================================================
19
+
20
+ // The connector holds each request this session waits on open until the phone answers it. The
21
+ // dialog here, the editor hosting the session or the person's own hooks may answer first. No hook
22
+ // hears the dialog's answer, so the call it settles closes the request: both hooks see the call,
23
+ // with its fields in different orders.
24
+ function call(agent: string | undefined, tool: string, input: unknown): string {
25
+ const sorted = (value: unknown): unknown =>
26
+ Array.isArray(value)
27
+ ? value.map(sorted)
28
+ : value && typeof value === 'object'
29
+ ? Object.fromEntries(
30
+ Object.keys(value)
31
+ .sort()
32
+ .map((key) => [key, sorted((value as Record<string, unknown>)[key])]),
33
+ )
34
+ : value;
35
+ return JSON.stringify([agent ?? '', tool, sorted(input)]);
36
+ }
37
+
16
38
  export const register: Register = (on) => {
17
39
  let turnId: string | undefined;
18
40
  let stream: AsyncGenerator<unknown, unknown> | undefined;
41
+ // The requests the phone is asked, by the call each waits on.
42
+ const asks = new Map<string, AsyncGenerator<unknown, unknown>>();
43
+ on('classic.PermissionRequest', async ($, e, next) => {
44
+ const key = call(e.agent_id, e.tool_name, e.tool_input);
45
+ const ask = $.process.spawn({
46
+ argv: [
47
+ 'curl',
48
+ '-sfN',
49
+ '--unix-socket',
50
+ `${await $.env.get('HOME')}/.dashup/claude.sock`,
51
+ '--data-binary',
52
+ '@-',
53
+ `http://dashup/sessions/${await $.session.id()}/prompts`,
54
+ ],
55
+ input: JSON.stringify({
56
+ tool : e.tool_name,
57
+ input : e.tool_input,
58
+ suggestions : e.permission_suggestions,
59
+ }),
60
+ });
61
+ const open: AsyncGenerator<unknown, unknown> = ask;
62
+ asks.set(key, open);
63
+ const beneath = next(e).then((result) => {
64
+ if (result.decision) void open.return(undefined);
65
+ return result;
66
+ });
67
+ let answer = '';
68
+ try {
69
+ for await (const { stream: name, text } of ask) if (name === 'stdout') answer += text;
70
+ } catch {
71
+ // curl is missing.
72
+ } finally {
73
+ if (asks.get(key) === open) asks.delete(key);
74
+ }
75
+ try {
76
+ return answer ? { decision: JSON.parse(answer) } : beneath;
77
+ } catch {
78
+ // The connector stopped mid-answer.
79
+ return beneath;
80
+ }
81
+ });
82
+ on('tool.call', async ($, e, next) => {
83
+ try {
84
+ return await next(e);
85
+ } finally {
86
+ if (asks.size) {
87
+ const { tool, tool_use_id, consent, agentId, requestMeta, ...input } = e as Record<
88
+ string,
89
+ unknown
90
+ >;
91
+ void asks.get(call(agentId as string | undefined, String(tool), input))?.return(undefined);
92
+ }
93
+ }
94
+ });
19
95
  on('turn.start', ($, e, next) => {
20
96
  if (e.agentId === undefined) turnId = e.turnId;
21
97
  return next(e);
@@ -23,6 +99,7 @@ export const register: Register = (on) => {
23
99
  // `/clear` starts another conversation in this process, which streams under its own id.
24
100
  on('session.end', ($, e, next) => {
25
101
  void stream?.return(undefined);
102
+ for (const ask of asks.values()) void ask.return(undefined);
26
103
  return next(e);
27
104
  });
28
105
  on('session.start', async ($, e, next) => {
package/dist/index.js CHANGED
@@ -3802,7 +3802,7 @@ function bridgeAcp(socket, onUpdate) {
3802
3802
  if (session && direction === "agent" && message.method === methods.client.session.requestPermission) {
3803
3803
  const tool = message.params.toolCall ?? {};
3804
3804
  const diff = (tool.content ?? []).find((part) => part.type === "diff");
3805
- const approval3 = {
3805
+ const approval2 = {
3806
3806
  id: randomUUID(),
3807
3807
  title: String(message.params.title ?? tool.title ?? "Agent needs approval"),
3808
3808
  detail: String(message.params.description ?? ""),
@@ -3820,10 +3820,10 @@ ${String(diff.newText).split("\n").map((line) => "+" + line).join("\n")}`
3820
3820
  }
3821
3821
  } : {}
3822
3822
  };
3823
- session.approvals.set(String(message.id), approval3);
3823
+ session.approvals.set(String(message.id), approval2);
3824
3824
  permissions.set(String(message.id), session);
3825
3825
  session.status = "attention";
3826
- publish(session, approval3);
3826
+ publish(session, approval2);
3827
3827
  }
3828
3828
  if (direction === "editor" && !message.method) {
3829
3829
  const pending = permissions.get(String(message.id));
@@ -4048,7 +4048,7 @@ import * as ui from "@clack/prompts";
4048
4048
  import { z } from "zod";
4049
4049
 
4050
4050
  // package.json
4051
- var version = "0.1.15";
4051
+ var version = "0.1.16";
4052
4052
 
4053
4053
  // src/pair.ts
4054
4054
  var Invitation = z.object({
@@ -4215,6 +4215,24 @@ var attached = (id, mime, data, name) => ({
4215
4215
  });
4216
4216
  var MAX_PREVIEW = 64 * 1024;
4217
4217
  var NO_PREVIEW = "Review the full request on your computer before approving.";
4218
+ function questionApproval(id, items, multiple) {
4219
+ const asked = items.map((item) => ({
4220
+ ...typeof item?.header === "string" ? { header: item.header } : {},
4221
+ question: String(item?.question ?? ""),
4222
+ options: (Array.isArray(item?.options) ? item.options : []).filter((option2) => typeof option2?.label === "string").map((option2) => ({
4223
+ label: String(option2.label),
4224
+ ...typeof option2.description === "string" ? { description: option2.description } : {}
4225
+ })),
4226
+ ...multiple(item) ? { multiple: true } : {}
4227
+ }));
4228
+ return {
4229
+ id,
4230
+ title: asked.length === 1 ? asked[0].question : `${asked.length} questions`,
4231
+ detail: "",
4232
+ choices: [{ id: "skip", label: "Skip" }],
4233
+ questions: asked
4234
+ };
4235
+ }
4218
4236
 
4219
4237
  // src/harness/history.ts
4220
4238
  var SAMPLE_BYTES = 128 * 1024;
@@ -16728,13 +16746,14 @@ var Client = class {
16728
16746
  });
16729
16747
  this.changed();
16730
16748
  }) : void 0,
16731
- approve: approvals.length ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16732
- const approval3 = this.approvals.get(approvalId);
16749
+ approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
16750
+ const approval2 = this.approvals.get(approvalId);
16751
+ const choice = "choice" in reply ? reply.choice : void 0;
16733
16752
  const socket = this.socket;
16734
- if (!approval3 || approval3.threadId !== id || !approval3.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16753
+ if (!approval2 || approval2.threadId !== id || !approval2.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16735
16754
  throw new Error("Codex approval is no longer pending");
16736
16755
  await new Promise((resolve3, reject) => socket.send(
16737
- JSON.stringify({ id: approval3.requestId, result: { decision: choice } }),
16756
+ JSON.stringify({ id: approval2.requestId, result: { decision: choice } }),
16738
16757
  (error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
16739
16758
  ));
16740
16759
  this.approvals.delete(approvalId);
@@ -16810,17 +16829,17 @@ var Codex = class {
16810
16829
  };
16811
16830
 
16812
16831
  // src/harness/headless.ts
16813
- import { randomUUID as randomUUID3 } from "node:crypto";
16814
- import { readFileSync as readFileSync3 } from "node:fs";
16832
+ import { randomUUID as randomUUID4 } from "node:crypto";
16815
16833
  import { createInterface as createInterface3 } from "node:readline";
16816
16834
  import { spawn as spawn3 } from "node:child_process";
16817
16835
 
16818
16836
  // src/harness/claude.ts
16819
16837
  import { homedir as homedir5 } from "node:os";
16820
- import { dirname as dirname4, join as join5 } from "node:path";
16838
+ import { randomUUID as randomUUID3 } from "node:crypto";
16839
+ import { dirname as dirname4, join as join5, basename as basename2 } from "node:path";
16821
16840
  import { spawn as spawn2 } from "node:child_process";
16822
16841
  import { createInterface as createInterface2 } from "node:readline";
16823
- import { watch as watch2 } from "node:fs";
16842
+ import { watch as watch2, readFileSync as readFileSync3 } from "node:fs";
16824
16843
  import { mkdir, readdir as readdir2, readFile as readFile2, readlink, rm } from "node:fs/promises";
16825
16844
  import { createServer as createServer2 } from "node:http";
16826
16845
  var MODELS_MS = 3e4;
@@ -16893,9 +16912,148 @@ function claudeOptions(models, recorded) {
16893
16912
  ...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
16894
16913
  ];
16895
16914
  }
16915
+ var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16916
+ var SCOPE = 32;
16917
+ var preview2 = (text3) => Buffer.byteLength(text3) > MAX_PREVIEW ? Buffer.from(text3).subarray(0, MAX_PREVIEW).toString("utf8") : text3;
16918
+ function hunk(before, after, offset = 0) {
16919
+ const old = before ? before.replace(/\n$/, "").split("\n") : [];
16920
+ const next = after ? after.replace(/\n$/, "").split("\n") : [];
16921
+ let start2 = 0;
16922
+ while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
16923
+ let end = 0;
16924
+ while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
16925
+ end++;
16926
+ const removed = old.slice(start2, old.length - end);
16927
+ const added2 = next.slice(start2, next.length - end);
16928
+ return {
16929
+ lines: [
16930
+ `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
16931
+ ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
16932
+ ...removed.map((line) => `-${line}`),
16933
+ ...added2.map((line) => `+${line}`),
16934
+ ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
16935
+ ],
16936
+ added: added2.length,
16937
+ deleted: removed.length
16938
+ };
16939
+ }
16940
+ function change(tool, input) {
16941
+ const path = input.file_path;
16942
+ if (typeof path !== "string") return void 0;
16943
+ let current = "";
16944
+ try {
16945
+ current = readFileSync3(path, "utf8");
16946
+ } catch {
16947
+ }
16948
+ const edit = (value2) => {
16949
+ const before = String(value2?.old_string ?? "");
16950
+ const at = before ? current.indexOf(before) : -1;
16951
+ return hunk(
16952
+ before,
16953
+ String(value2?.new_string ?? ""),
16954
+ at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
16955
+ );
16956
+ };
16957
+ const hunks = tool === "Write" && typeof input.content === "string" ? [hunk(current, input.content)] : tool === "Edit" ? [edit(input)] : tool === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(edit) : [];
16958
+ if (!hunks.length) return void 0;
16959
+ const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
16960
+ if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
16961
+ return {
16962
+ path,
16963
+ diff,
16964
+ added: hunks.reduce((sum, item) => sum + item.added, 0),
16965
+ deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
16966
+ };
16967
+ }
16968
+ function remember(suggestions = []) {
16969
+ const updates = suggestions.filter(
16970
+ (update) => update?.type === "addRules" && update.behavior === "allow" || update?.type === "addDirectories"
16971
+ );
16972
+ const rule = updates.flatMap((update) => update.rules ?? [])[0];
16973
+ const folder = updates.flatMap((update) => update.directories ?? [])[0];
16974
+ const scope2 = rule ? String(rule.ruleContent ?? rule.toolName) : folder ? `access to ${basename2(String(folder))}` : "";
16975
+ if (scope2)
16976
+ return {
16977
+ label: `Always allow ${scope2.length > SCOPE ? `${scope2.slice(0, SCOPE - 1)}\u2026` : scope2}`,
16978
+ updates
16979
+ };
16980
+ const edits = suggestions.find(
16981
+ (update) => update?.type === "setMode" && update.mode === "acceptEdits"
16982
+ );
16983
+ return edits ? { label: "Allow all edits", updates: [edits] } : void 0;
16984
+ }
16985
+ function ask(id, request2, host) {
16986
+ const { tool, input } = request2;
16987
+ if (tool === "AskUserQuestion" && Array.isArray(input.questions))
16988
+ return questionApproval(id, input.questions, (item) => item.multiSelect === true);
16989
+ if (tool === "ExitPlanMode")
16990
+ return {
16991
+ id,
16992
+ title: "Approve this plan?",
16993
+ detail: host ? "" : "Approve it on your computer, or tell Claude what to change.",
16994
+ choices: [
16995
+ ...host ? [{ id: "accept", label: "Approve" }] : [],
16996
+ { id: "revise", label: "Keep planning", text: true }
16997
+ ],
16998
+ plan: preview2(String(input.plan ?? ""))
16999
+ };
17000
+ const diff = change(tool, input);
17001
+ const always = remember(request2.suggestions);
17002
+ return {
17003
+ id,
17004
+ title: tool === "Bash" ? "Allow command?" : EDITS.has(tool) ? "Allow file changes?" : `Allow ${request2.label ?? tool}?`,
17005
+ detail: preview2(
17006
+ [
17007
+ request2.description ?? (typeof input.description === "string" ? input.description : ""),
17008
+ typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
17009
+ ].filter(Boolean).join("\n\n")
17010
+ ),
17011
+ ...diff ? { change: diff } : {},
17012
+ choices: [
17013
+ { id: "accept", label: "Allow once" },
17014
+ ...always ? [{ id: "always", label: always.label }] : [],
17015
+ { id: "decline", label: "Reject" }
17016
+ ]
17017
+ };
17018
+ }
17019
+ function decide(request2, approval2, reply) {
17020
+ const input = request2.input;
17021
+ if ("answers" in reply)
17022
+ return {
17023
+ behavior: "allow",
17024
+ updatedInput: {
17025
+ ...input,
17026
+ answers: Object.fromEntries(
17027
+ (approval2.questions ?? []).map((item, index) => [
17028
+ item.question,
17029
+ reply.answers[index].join(", ")
17030
+ ])
17031
+ )
17032
+ }
17033
+ };
17034
+ if (reply.choice === "accept")
17035
+ return {
17036
+ behavior: "allow",
17037
+ updatedInput: input,
17038
+ ...approval2.plan === void 0 ? {} : { updatedPermissions: [{ type: "setMode", mode: "default", destination: "session" }] }
17039
+ };
17040
+ if (reply.choice === "always")
17041
+ return {
17042
+ behavior: "allow",
17043
+ updatedInput: input,
17044
+ updatedPermissions: remember(request2.suggestions)?.updates ?? []
17045
+ };
17046
+ if (reply.choice === "revise")
17047
+ return { behavior: "deny", message: reply.text?.trim() || "Keep planning." };
17048
+ return {
17049
+ behavior: "deny",
17050
+ message: reply.choice === "skip" ? "The person skipped these questions." : "The person rejected this from their phone."
17051
+ };
17052
+ }
16896
17053
  var MAX_SESSIONS2 = 100;
16897
17054
  var MAX_REQUESTS = 1e3;
16898
- var SESSION = /^\/sessions\/([\w-]{1,100})$/;
17055
+ var MAX_PROMPT = 4 * 1024 * 1024;
17056
+ var SESSION = /^\/sessions\/([\w-]{1,100})(\/prompts)?$/;
16899
17057
  var STATUS = {
16900
17058
  busy: "working",
16901
17059
  idle: "idle",
@@ -16932,20 +17090,28 @@ var Claude = class {
16932
17090
  notify;
16933
17091
  server;
16934
17092
  streams = /* @__PURE__ */ new Map();
17093
+ prompts = /* @__PURE__ */ new Map();
16935
17094
  requests = /* @__PURE__ */ new Map();
16936
17095
  async refresh() {
16937
17096
  this.watch();
16938
17097
  return (await registrations()).map((session) => {
16939
17098
  const plugin = this.streams.has(session.sessionId);
16940
17099
  const models = claudeModels(this.changed, session.command, session.version);
17100
+ const approvals = [...this.prompts.values()].filter((prompt) => prompt.session === session.sessionId).map((prompt) => prompt.approval);
16941
17101
  return {
16942
17102
  id: session.sessionId,
16943
17103
  harness: "claude",
16944
17104
  cwd: session.cwd,
16945
17105
  updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
16946
- status: STATUS[session.status ?? ""] ?? "unknown",
17106
+ status: approvals.length ? "attention" : STATUS[session.status ?? ""] ?? "unknown",
16947
17107
  messages: [],
16948
- approvals: [],
17108
+ approvals,
17109
+ approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17110
+ const prompt = this.prompts.get(approvalId);
17111
+ if (prompt?.session !== session.sessionId)
17112
+ throw new Error("Claude Code approval is no longer pending");
17113
+ prompt.answer(decide(prompt.request, prompt.approval, reply));
17114
+ }) : void 0,
16949
17115
  send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
16950
17116
  stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
16951
17117
  now: plugin,
@@ -16958,8 +17124,9 @@ var Claude = class {
16958
17124
  async listen() {
16959
17125
  if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16960
17126
  const server = this.server = createServer2((request2, response) => {
16961
- const id = request2.method === "GET" ? SESSION.exec(request2.url ?? "")?.[1] : void 0;
16962
- if (!id) return void response.writeHead(404).end();
17127
+ const [, id, prompt] = SESSION.exec(request2.url ?? "") ?? [];
17128
+ if (id && prompt && request2.method === "POST") return this.prompt(id, request2, response);
17129
+ if (!id || prompt || request2.method !== "GET") return void response.writeHead(404).end();
16963
17130
  this.streams.get(id)?.end();
16964
17131
  this.streams.set(id, response);
16965
17132
  response.once("close", () => {
@@ -16977,6 +17144,46 @@ var Claude = class {
16977
17144
  server.listen(this.socket, resolve3);
16978
17145
  });
16979
17146
  }
17147
+ // Asks the phone what the session's permission request asks the person.
17148
+ prompt(session, request2, response) {
17149
+ const chunks = [];
17150
+ let bytes = 0;
17151
+ request2.on("data", (chunk) => {
17152
+ bytes += chunk.length;
17153
+ if (bytes > MAX_PROMPT) request2.destroy();
17154
+ else chunks.push(chunk);
17155
+ });
17156
+ request2.once("end", () => {
17157
+ let asked;
17158
+ try {
17159
+ asked = JSON.parse(Buffer.concat(chunks).toString("utf8"));
17160
+ } catch {
17161
+ }
17162
+ if (typeof asked?.tool !== "string" || !asked.input || typeof asked.input !== "object")
17163
+ return void response.writeHead(400).end();
17164
+ const id = randomUUID3();
17165
+ const prompted = {
17166
+ tool: asked.tool,
17167
+ input: asked.input,
17168
+ ...Array.isArray(asked.suggestions) ? { suggestions: asked.suggestions } : {}
17169
+ };
17170
+ this.prompts.set(id, {
17171
+ session,
17172
+ request: prompted,
17173
+ approval: ask(id, prompted, false),
17174
+ answer: (decision) => {
17175
+ this.prompts.delete(id);
17176
+ response.end(JSON.stringify(decision));
17177
+ this.changed();
17178
+ }
17179
+ });
17180
+ response.once("close", () => {
17181
+ if (this.prompts.delete(id)) this.changed();
17182
+ });
17183
+ response.writeHead(200, { "Content-Type": "application/json" }).flushHeaders();
17184
+ this.changed();
17185
+ });
17186
+ }
16980
17187
  deliver(id, delivery) {
16981
17188
  const stream = this.streams.get(id);
16982
17189
  if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
@@ -17015,6 +17222,7 @@ var Claude = class {
17015
17222
  clearTimeout(this.notify);
17016
17223
  this.watcher?.close();
17017
17224
  this.watcher = void 0;
17225
+ this.prompts.clear();
17018
17226
  this.server?.closeAllConnections();
17019
17227
  this.server?.close();
17020
17228
  this.server = void 0;
@@ -17022,82 +17230,9 @@ var Claude = class {
17022
17230
  };
17023
17231
 
17024
17232
  // src/harness/headless.ts
17025
- var CHOICES = [
17026
- { id: "accept", label: "Allow once" },
17027
- { id: "decline", label: "Reject" }
17028
- ];
17029
- var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
17030
17233
  var MAX_REQUESTS2 = 1e3;
17031
17234
  var NOTIFY_MS = 100;
17032
17235
  var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
17033
- function hunk(before, after, offset = 0) {
17034
- const old = before ? before.replace(/\n$/, "").split("\n") : [];
17035
- const next = after ? after.replace(/\n$/, "").split("\n") : [];
17036
- let start2 = 0;
17037
- while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
17038
- let end = 0;
17039
- while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
17040
- end++;
17041
- const removed = old.slice(start2, old.length - end);
17042
- const added2 = next.slice(start2, next.length - end);
17043
- return {
17044
- lines: [
17045
- `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
17046
- ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
17047
- ...removed.map((line) => `-${line}`),
17048
- ...added2.map((line) => `+${line}`),
17049
- ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
17050
- ],
17051
- added: added2.length,
17052
- deleted: removed.length
17053
- };
17054
- }
17055
- function change(tool, input) {
17056
- const path = input.file_path;
17057
- if (typeof path !== "string") return void 0;
17058
- let current = "";
17059
- try {
17060
- current = readFileSync3(path, "utf8");
17061
- } catch {
17062
- }
17063
- const edit = (value2) => {
17064
- const before = String(value2?.old_string ?? "");
17065
- const at = before ? current.indexOf(before) : -1;
17066
- return hunk(
17067
- before,
17068
- String(value2?.new_string ?? ""),
17069
- at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
17070
- );
17071
- };
17072
- const hunks = tool === "Write" && typeof input.content === "string" ? [hunk(current, input.content)] : tool === "Edit" ? [edit(input)] : tool === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(edit) : [];
17073
- if (!hunks.length) return void 0;
17074
- const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
17075
- if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
17076
- return {
17077
- path,
17078
- diff,
17079
- added: hunks.reduce((sum, item) => sum + item.added, 0),
17080
- deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
17081
- };
17082
- }
17083
- function approval2(id, request2) {
17084
- const input = request2.input ?? {};
17085
- const tool = String(request2.display_name ?? request2.tool_name ?? "a tool");
17086
- const diff = change(request2.tool_name, input);
17087
- let detail = [
17088
- typeof request2.description === "string" ? request2.description : "",
17089
- typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
17090
- ].filter(Boolean).join("\n\n");
17091
- if (Buffer.byteLength(detail) > MAX_PREVIEW)
17092
- detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
17093
- return {
17094
- id,
17095
- title: request2.tool_name === "Bash" ? "Allow command?" : EDITS.has(request2.tool_name) ? "Allow file changes?" : `Allow ${tool}?`,
17096
- detail,
17097
- ...diff ? { change: diff } : {},
17098
- choices: CHOICES.map((choice) => ({ ...choice }))
17099
- };
17100
- }
17101
17236
  var Session = class {
17102
17237
  // A resumed session is `saved` and keeps the title its history gives.
17103
17238
  constructor(id, cwd, title2, changed, saved = false) {
@@ -17131,14 +17266,14 @@ var Session = class {
17131
17266
  interrupt() {
17132
17267
  this.write({
17133
17268
  type: "control_request",
17134
- request_id: randomUUID3(),
17269
+ request_id: randomUUID4(),
17135
17270
  request: { subtype: "interrupt" }
17136
17271
  });
17137
17272
  }
17138
17273
  apply(option2, value2) {
17139
17274
  this.write({
17140
17275
  type: "control_request",
17141
- request_id: randomUUID3(),
17276
+ request_id: randomUUID4(),
17142
17277
  request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
17143
17278
  });
17144
17279
  }
@@ -17154,7 +17289,7 @@ var Session = class {
17154
17289
  // Resolves once Claude Code takes the message, or says why it could not start. `now` stops the
17155
17290
  // turn under way, so Claude Code takes the message at once.
17156
17291
  say(text3, now = false) {
17157
- this.messages.push({ id: randomUUID3(), role: "user", text: text3 });
17292
+ this.messages.push({ id: randomUUID4(), role: "user", text: text3 });
17158
17293
  this.typing = "";
17159
17294
  this.status = "working";
17160
17295
  this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -17223,15 +17358,27 @@ var Session = class {
17223
17358
  else if (message.type === "assistant") {
17224
17359
  const text3 = (Array.isArray(message.message?.content) ? message.message.content : []).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n\n");
17225
17360
  if (!text3) return false;
17226
- this.messages.push({ id: String(message.uuid ?? randomUUID3()), role: "assistant", text: text3 });
17361
+ this.messages.push({ id: String(message.uuid ?? randomUUID4()), role: "assistant", text: text3 });
17227
17362
  } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
17228
- const id = randomUUID3();
17363
+ const id = randomUUID4();
17364
+ const asked = message.request;
17365
+ const request2 = {
17366
+ tool: String(asked.tool_name),
17367
+ input: asked.input ?? {},
17368
+ label: typeof asked.display_name === "string" ? asked.display_name : void 0,
17369
+ description: typeof asked.description === "string" ? asked.description : void 0,
17370
+ suggestions: Array.isArray(asked.permission_suggestions) ? asked.permission_suggestions : void 0
17371
+ };
17229
17372
  this.approvals.set(id, {
17230
- request: String(message.request_id),
17231
- input: message.request.input ?? {},
17232
- approval: approval2(id, message.request)
17373
+ control: String(message.request_id),
17374
+ request: request2,
17375
+ approval: ask(id, request2, true)
17233
17376
  });
17234
17377
  this.status = "attention";
17378
+ } else if (message.type === "control_cancel_request") {
17379
+ for (const [id, pending] of this.approvals)
17380
+ if (pending.control === message.request_id) this.approvals.delete(id);
17381
+ if (this.status === "attention" && !this.approvals.size) this.status = "working";
17235
17382
  } else if (message.type === "result") {
17236
17383
  if (--this.open <= 0) this.turn?.stdin.end();
17237
17384
  } else if (message.type === "stream_event" && !message.parent_tool_use_id) {
@@ -17269,16 +17416,16 @@ var Session = class {
17269
17416
  if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17270
17417
  this.interrupt();
17271
17418
  }) : void 0,
17272
- approve: this.approvals.size ? (requestId, approvalId, choice) => this.once(requestId, async () => {
17419
+ approve: this.approvals.size ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17273
17420
  const pending = this.approvals.get(approvalId);
17274
- if (!pending || !this.turn || !CHOICES.some((value2) => value2.id === choice))
17421
+ if (!pending || !this.turn)
17275
17422
  throw new Error("Claude Code approval is no longer pending");
17276
17423
  this.write({
17277
17424
  type: "control_response",
17278
17425
  response: {
17279
17426
  subtype: "success",
17280
- request_id: pending.request,
17281
- response: choice === "accept" ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The person rejected this from their phone." }
17427
+ request_id: pending.control,
17428
+ response: decide(pending.request, pending.approval, reply)
17282
17429
  }
17283
17430
  });
17284
17431
  this.approvals.delete(approvalId);
@@ -17309,7 +17456,7 @@ var Headless = class {
17309
17456
  // Starts a session in `cwd` with its first message, and resolves with the session's id.
17310
17457
  async start(cwd, text3) {
17311
17458
  const title2 = text3.trim().split("\n")[0].slice(0, 80);
17312
- const session = new Session(randomUUID3(), cwd, title2, this.soon);
17459
+ const session = new Session(randomUUID4(), cwd, title2, this.soon);
17313
17460
  this.sessions.set(session.id, session);
17314
17461
  try {
17315
17462
  await session.say(text3);
@@ -17348,9 +17495,9 @@ import { connect as connect2 } from "node:net";
17348
17495
  import { promisify as promisify3 } from "node:util";
17349
17496
  import { fileURLToPath } from "node:url";
17350
17497
  import { execFile as execFile3 } from "node:child_process";
17351
- import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
17498
+ import { createHash as createHash3, randomUUID as randomUUID5 } from "node:crypto";
17352
17499
  import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17353
- import { join as join6, dirname as dirname5, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
17500
+ import { join as join6, dirname as dirname5, basename as basename3, isAbsolute as isAbsolute3 } from "node:path";
17354
17501
  import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
17355
17502
  import { AhpClient } from "@microsoft/agent-host-protocol/client";
17356
17503
  import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
@@ -17368,7 +17515,7 @@ var CONFIRMATION_CHOICES = [
17368
17515
  function roots() {
17369
17516
  const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
17370
17517
  if (override !== void 0)
17371
- return override ? [basename2(override) === "User" ? dirname5(override) : override] : [];
17518
+ return override ? [basename3(override) === "User" ? dirname5(override) : override] : [];
17372
17519
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17373
17520
  const base = process.platform === "darwin" ? join6(homedir6(), "Library/Application Support") : process.platform === "win32" ? process.env.APPDATA ?? join6(homedir6(), "AppData/Roaming") : process.env.XDG_CONFIG_HOME ?? join6(homedir6(), ".config");
17374
17521
  return ["Code", "Code - Insiders"].map((name) => join6(base, name));
@@ -17410,12 +17557,12 @@ async function entries() {
17410
17557
  try {
17411
17558
  if (!await privateFile(path, MAX_METADATA)) continue;
17412
17559
  const parsed = JSON.parse(await readFile3(path, "utf8"));
17413
- const rows = basename2(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
17560
+ const rows = basename3(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
17414
17561
  for (const row of rows.slice(0, MAX_HOSTS)) {
17415
17562
  if (row?.schemaVersion !== 2 || !["editor", "standalone"].includes(row.type) || !Number.isSafeInteger(row.pid) || row.pid <= 0 || typeof row.instanceId !== "string" || !row.instanceId || typeof row.connectionToken !== "string" || !row.connectionToken || row.connectionToken.length > 1024 || row.endpoint?.type !== "socket" || typeof row.endpoint.path !== "string" || !isAbsolute3(row.endpoint.path))
17416
17563
  continue;
17417
17564
  const identity2 = createHash3("sha256").update(`${row.type}\0${row.pid}\0${row.instanceId}`).digest("hex");
17418
- if (basename2(path) !== "metadata.json" && basename2(path) !== `${identity2}.json`) continue;
17565
+ if (basename3(path) !== "metadata.json" && basename3(path) !== `${identity2}.json`) continue;
17419
17566
  try {
17420
17567
  process.kill(row.pid, 0);
17421
17568
  } catch {
@@ -17541,7 +17688,7 @@ var Host = class {
17541
17688
  client;
17542
17689
  connecting;
17543
17690
  stopped = false;
17544
- clientId = `dashup-${randomUUID4()}`;
17691
+ clientId = `dashup-${randomUUID5()}`;
17545
17692
  resources = /* @__PURE__ */ new Map();
17546
17693
  subscriptions = /* @__PURE__ */ new Set();
17547
17694
  receipts = /* @__PURE__ */ new Map();
@@ -17722,12 +17869,12 @@ var Host = class {
17722
17869
  for (const pending of Array.isArray(session.inputNeeded) ? session.inputNeeded : []) {
17723
17870
  if (pending.kind !== "toolConfirmation" || pending.chat !== item.resource || pending.toolCall?.status !== "pending-confirmation")
17724
17871
  continue;
17725
- const preview2 = confirmation(pending.toolCall);
17872
+ const preview3 = confirmation(pending.toolCall);
17726
17873
  approvals.push({
17727
17874
  id: pending.id,
17728
17875
  title: text2(pending.toolCall.confirmationTitle) || pending.toolCall.displayName || "Native agent approval",
17729
- detail: preview2.detail,
17730
- choices: preview2.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17876
+ detail: preview3.detail,
17877
+ choices: preview3.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17731
17878
  });
17732
17879
  }
17733
17880
  const id = `vscode-ahp:${createHash3("sha256").update(this.entry.root).digest("hex").slice(0, 16)}:${item.resource}`;
@@ -17769,7 +17916,8 @@ var Host = class {
17769
17916
  message: { text: value2, origin: { kind: "user" } }
17770
17917
  });
17771
17918
  } : void 0,
17772
- approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => {
17919
+ approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => {
17920
+ const choice = "choice" in reply ? reply.choice : void 0;
17773
17921
  const prior = this.requests.get(requestId);
17774
17922
  if (prior) return prior;
17775
17923
  const current = this.resources.get(summary.resource)?.state;
@@ -18142,21 +18290,14 @@ var Server = class {
18142
18290
  (question) => question?.sessionID === id && typeof question.id === "string"
18143
18291
  ) : [];
18144
18292
  const approvals = pending.map((permission) => permissionApproval(permission, history, cwd));
18145
- for (const question of asks) {
18146
- const first = question.questions?.[0];
18147
- const options = first?.multiple !== true && question.questions?.length === 1 && Array.isArray(first?.options) ? first.options.filter(
18148
- (option2) => typeof option2?.label === "string"
18149
- ) : [];
18150
- approvals.push({
18151
- id: `question:${question.id}`,
18152
- title: typeof first?.header === "string" ? first.header : "OpenCode needs an answer",
18153
- detail: typeof first?.question === "string" ? first.question : "Open this question in the computer\u2019s native interface.",
18154
- choices: options.map((option2, index) => ({
18155
- id: `answer:${index}`,
18156
- label: option2.label
18157
- }))
18158
- });
18159
- }
18293
+ for (const question of asks)
18294
+ approvals.push(
18295
+ questionApproval(
18296
+ `question:${question.id}`,
18297
+ Array.isArray(question.questions) ? question.questions : [],
18298
+ (item) => item.multiple === true
18299
+ )
18300
+ );
18160
18301
  result.push({
18161
18302
  id,
18162
18303
  cwd,
@@ -18192,23 +18333,21 @@ var Server = class {
18192
18333
  });
18193
18334
  this.changed();
18194
18335
  }),
18195
- approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => this.once(requestId, async () => {
18336
+ approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => this.once(requestId, async () => {
18196
18337
  if (this.stopped) throw new Error("OpenCode is disconnected");
18338
+ const choice = "choice" in reply ? reply.choice : void 0;
18197
18339
  if (approvalId.startsWith("question:")) {
18198
18340
  const requestID = approvalId.slice("question:".length);
18199
18341
  const current = await this.request("/question", cwd);
18200
- const question = Array.isArray(current) ? current.find(
18342
+ if (!Array.isArray(current) || !current.some(
18201
18343
  (value2) => value2?.id === requestID && value2.sessionID === id
18202
- ) : void 0;
18203
- const index = /^answer:(\d+)$/.exec(choice)?.[1];
18204
- const option2 = index !== void 0 ? question?.questions?.[0]?.options?.[Number(index)] : void 0;
18205
- if (question?.questions?.length !== 1 || question.questions[0]?.multiple === true || typeof option2?.label !== "string")
18206
- throw new Error(
18207
- "Question is no longer pending or requires the native interface"
18208
- );
18209
- await this.request(`/question/${encodeURIComponent(requestID)}/reply`, cwd, {
18210
- answers: [[option2.label]]
18211
- });
18344
+ ))
18345
+ throw new Error("Question is no longer pending");
18346
+ await this.request(
18347
+ `/question/${encodeURIComponent(requestID)}/${"answers" in reply ? "reply" : "reject"}`,
18348
+ cwd,
18349
+ "answers" in reply ? { answers: reply.answers } : {}
18350
+ );
18212
18351
  } else {
18213
18352
  const current = await this.request("/permission", cwd);
18214
18353
  const permission = Array.isArray(current) ? current.find(
@@ -18493,10 +18632,10 @@ var LiveHarnesses = class {
18493
18632
  };
18494
18633
 
18495
18634
  // src/harness/queue.ts
18496
- import { randomUUID as randomUUID5 } from "node:crypto";
18635
+ import { randomUUID as randomUUID6 } from "node:crypto";
18497
18636
  import { spawn as spawn5 } from "node:child_process";
18498
18637
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18499
- import { join as join8, dirname as dirname6, basename as basename3 } from "node:path";
18638
+ import { join as join8, dirname as dirname6, basename as basename4 } from "node:path";
18500
18639
  import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18501
18640
  var CODEX_VERSION = "0.160.1";
18502
18641
  var REQUEST_MS4 = 1e4;
@@ -18528,7 +18667,7 @@ function processOwners(pid) {
18528
18667
  const root = `/proc/${pid}`;
18529
18668
  if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
18530
18669
  const executable = realpathSync2.native(join8(root, "exe"));
18531
- if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18670
+ if (basename4(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18532
18671
  const status2 = readFileSync4(join8(root, "stat"), "utf8");
18533
18672
  const start2 = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
18534
18673
  if (!start2 || !/^\d+$/.test(start2)) return [];
@@ -18541,8 +18680,8 @@ function processOwners(pid) {
18541
18680
  }
18542
18681
  });
18543
18682
  return files2.flatMap(({ fd, path }) => {
18544
- if (basename3(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18545
- const thread = basename3(path, ".lock");
18683
+ if (basename4(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18684
+ const thread = basename4(path, ".lock");
18546
18685
  if (!THREAD_ID.test(thread)) return [];
18547
18686
  try {
18548
18687
  const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
@@ -18778,7 +18917,7 @@ var NativeQueue = class {
18778
18917
  if (existing) return existing;
18779
18918
  if (this.stopped) throw new Error("The connector is stopping");
18780
18919
  validateOwner(scope2.owner);
18781
- const clientUserMessageId = randomUUID5();
18920
+ const clientUserMessageId = randomUUID6();
18782
18921
  const request2 = { ...scope2, receipt: { state: "unknown", clientUserMessageId } };
18783
18922
  let writer;
18784
18923
  try {
@@ -18852,6 +18991,7 @@ var EPHEMERAL_TTL_MS = 6e4;
18852
18991
  var HOME_RECENT = 20;
18853
18992
  var FOLLOW_MS = 2 * 6e4;
18854
18993
  var RESCAN_MS = 1e3;
18994
+ var answers = (approval2, reply) => "answers" in reply ? reply.answers.length === approval2.questions?.length : approval2.choices.some((choice) => choice.id === reply.choice);
18855
18995
  var Command = z2.object({
18856
18996
  type: z2.literal("command"),
18857
18997
  requestId: z2.string().min(1),
@@ -18873,7 +19013,15 @@ var Command = z2.object({
18873
19013
  option: z2.string().min(1).max(100),
18874
19014
  value: z2.union([z2.string().min(1).max(200), z2.boolean()])
18875
19015
  }).strict(),
18876
- z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
19016
+ z2.object({
19017
+ kind: z2.literal("approval.respond"),
19018
+ approvalId: z2.string(),
19019
+ choice: z2.string().optional(),
19020
+ text: z2.string().max(1e5).optional(),
19021
+ answers: z2.array(z2.array(z2.string().min(1).max(1e5)).min(1)).min(1).optional()
19022
+ }).strict().refine(
19023
+ (action) => action.choice === void 0 !== (action.answers === void 0) && (action.text === void 0 || action.choice !== void 0)
19024
+ ),
18877
19025
  z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
18878
19026
  z2.object({
18879
19027
  kind: z2.literal("session.start"),
@@ -18989,15 +19137,15 @@ var Connector = class {
18989
19137
  for (const project of this.discovery.projects.values())
18990
19138
  for (const thread of project.threads) {
18991
19139
  const native = this.discovery.locations.get(thread.id)?.live;
18992
- const approval3 = native?.approvals[0];
18993
- if (!approval3) continue;
19140
+ const approval2 = native?.approvals[0];
19141
+ if (!approval2) continue;
18994
19142
  await this.thread(pair, project, thread);
18995
19143
  await this.send(pair, {
18996
19144
  type: "attention",
18997
19145
  projectId: project.id,
18998
19146
  threadId: thread.id,
18999
- title: approval3.title,
19000
- body: approval3.detail,
19147
+ title: approval2.title,
19148
+ body: approval2.detail,
19001
19149
  reason: "approval"
19002
19150
  });
19003
19151
  }
@@ -19251,9 +19399,9 @@ var Connector = class {
19251
19399
  if (JSON.stringify(prior) === JSON.stringify(snapshot)) continue;
19252
19400
  this.nativeSnapshots.set(thread.id, structuredClone(snapshot));
19253
19401
  for (const pair of this.activePairs()) await this.thread(pair, project, thread);
19254
- for (const approval3 of session.approvals)
19255
- if (!prior?.approvals.some((item) => item.id === approval3.id))
19256
- await this.attention(project.id, thread.id, approval3.title, approval3.detail, "approval");
19402
+ for (const approval2 of session.approvals)
19403
+ if (!prior?.approvals.some((item) => item.id === approval2.id))
19404
+ await this.attention(project.id, thread.id, approval2.title, approval2.detail, "approval");
19257
19405
  if (prior?.thread.status === "working" && thread.status === "idle")
19258
19406
  await this.attention(
19259
19407
  project.id,
@@ -19441,7 +19589,7 @@ var Connector = class {
19441
19589
  }
19442
19590
  }
19443
19591
  attach(socket) {
19444
- const sessions = bridgeAcp(socket, (session, approval3) => {
19592
+ const sessions = bridgeAcp(socket, (session, approval2) => {
19445
19593
  this.agents.set(session.id, session);
19446
19594
  const { project, thread } = this.adopt(session);
19447
19595
  if (!this.snapshots.has(session.id)) {
@@ -19456,15 +19604,15 @@ var Connector = class {
19456
19604
  }
19457
19605
  }).catch((error) => this.report(error));
19458
19606
  }
19459
- if (approval3)
19607
+ if (approval2)
19460
19608
  this.work = this.work.then(async () => {
19461
- if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval3))
19609
+ if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval2))
19462
19610
  return;
19463
19611
  await this.attention(
19464
19612
  project.id,
19465
19613
  thread.id,
19466
- approval3.title,
19467
- approval3.detail,
19614
+ approval2.title,
19615
+ approval2.detail,
19468
19616
  "approval"
19469
19617
  );
19470
19618
  }).catch((error) => this.report(error));
@@ -19630,24 +19778,24 @@ var Connector = class {
19630
19778
  }
19631
19779
  case "approval.respond": {
19632
19780
  if (!thread) throw new Error("Thread not found");
19633
- const action = command2.action;
19781
+ const { kind, approvalId, ...reply } = command2.action;
19634
19782
  const native = this.discovery.locations.get(thread.id)?.live;
19635
19783
  if (native?.approve) {
19636
- const approval4 = native.approvals.find((item) => item.id === action.approvalId);
19637
- if (!approval4?.choices.some((choice) => choice.id === action.choice))
19784
+ const approval3 = native.approvals.find((item) => item.id === approvalId);
19785
+ if (!approval3 || !answers(approval3, reply))
19638
19786
  throw new Error("Approval is no longer available");
19639
- return native.approve(command2.requestId, approval4.id, action.choice);
19787
+ return native.approve(command2.requestId, approval3.id, reply);
19640
19788
  }
19641
19789
  const agent = this.attached(thread);
19642
19790
  if (!agent)
19643
19791
  throw new Error("Approval requires an attached ACP session. Answer it on your computer.");
19644
- const [request2, approval3] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
19645
- if (!approval3?.choices.some((choice) => choice.id === action.choice))
19792
+ const [request2, approval2] = [...agent.approvals].find(([, item]) => item.id === approvalId) ?? [];
19793
+ if (!approval2 || !("choice" in reply) || !answers(approval2, reply))
19646
19794
  throw new Error("Approval is no longer available");
19647
19795
  return commandAcp(agent, command2.requestId, {
19648
19796
  kind: "approval",
19649
19797
  id: request2,
19650
- choice: action.choice
19798
+ choice: reply.choice
19651
19799
  });
19652
19800
  }
19653
19801
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dashup": "dist/index.js"
@@ -30,11 +30,11 @@
30
30
  "tsup": "^8.5.1",
31
31
  "tsx": "^4.22.4",
32
32
  "typescript": "^5.9.3",
33
+ "@dashup/models": "0.1.0",
33
34
  "@dashup/protocol": "0.1.0",
34
- "@parcae/lynx": "0.1.0",
35
- "@parcae/sdk": "0.8.1",
36
35
  "@dashup/typescript-config": "0.0.0",
37
- "@dashup/models": "0.1.0"
36
+ "@parcae/lynx": "0.1.0",
37
+ "@parcae/sdk": "0.8.1"
38
38
  },
39
39
  "scripts": {
40
40
  "dashup": "tsx src/index.ts",