@dashup/connector 0.1.8 → 0.1.10

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 +6 -20
  2. package/dist/index.js +495 -603
  3. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ var require_XMLHttpRequest = __commonJS({
42
42
  "use strict";
43
43
  var fs = __require("fs");
44
44
  var Url = __require("url");
45
- var spawn7 = __require("child_process").spawn;
45
+ var spawn6 = __require("child_process").spawn;
46
46
  module.exports = XMLHttpRequest3;
47
47
  XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
48
48
  function XMLHttpRequest3(opts) {
@@ -338,7 +338,7 @@ var require_XMLHttpRequest = __commonJS({
338
338
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
339
339
  fs.writeFileSync(syncFile, "", "utf8");
340
340
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
341
- var syncProc = spawn7(process.argv[0], ["-e", execString]);
341
+ var syncProc = spawn6(process.argv[0], ["-e", execString]);
342
342
  var statusText;
343
343
  while (fs.existsSync(syncFile)) {
344
344
  }
@@ -1849,7 +1849,7 @@ var require_dist = __commonJS({
1849
1849
  var require_eventemitter3 = __commonJS({
1850
1850
  "../../node_modules/.pnpm/eventemitter3@5.0.4/node_modules/eventemitter3/index.js"(exports, module) {
1851
1851
  "use strict";
1852
- var has2 = Object.prototype.hasOwnProperty;
1852
+ var has = Object.prototype.hasOwnProperty;
1853
1853
  var prefix2 = "~";
1854
1854
  function Events() {
1855
1855
  }
@@ -1884,7 +1884,7 @@ var require_eventemitter3 = __commonJS({
1884
1884
  var names = [], events, name;
1885
1885
  if (this._eventsCount === 0) return names;
1886
1886
  for (name in events = this._events) {
1887
- if (has2.call(events, name)) names.push(prefix2 ? name.slice(1) : name);
1887
+ if (has.call(events, name)) names.push(prefix2 ? name.slice(1) : name);
1888
1888
  }
1889
1889
  if (Object.getOwnPropertySymbols) {
1890
1890
  return names.concat(Object.getOwnPropertySymbols(events));
@@ -2726,12 +2726,17 @@ var PRODUCT = {
2726
2726
  var PAIRING_TTL_MS = 5 * 6e4;
2727
2727
  var PROTOCOL_VERSION = 1;
2728
2728
  var MAX_ENVELOPE_BYTES = 8 * 1024 * 1024;
2729
+ var HISTORY_PAGE_ITEMS = 30;
2729
2730
  var HISTORY_TEXT_CHARS = 4e3;
2730
2731
  var HISTORY_PAGE_BYTES = 256 * 1024;
2731
2732
  var HISTORY_BATCH_ITEMS = 50;
2732
2733
  var HISTORY_BATCH_BYTES = HISTORY_PAGE_BYTES;
2733
2734
  var HISTORY_RECORD_BYTES = 32 * 1024;
2734
2735
  var HISTORY_RECORD_CIPHERTEXT_BYTES = HISTORY_RECORD_BYTES + 16;
2736
+ var OUTPUT_PREVIEW = { lines: 12, chars: 2e3 };
2737
+ var HISTORY_ATTACHMENTS = 16;
2738
+ var MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
2739
+ var ATTACHMENT_NAME_CHARS = 100;
2735
2740
  var MESSAGE_ROLES = ["user", "assistant", "system", "tool"];
2736
2741
  var HARNESSES = {
2737
2742
  claude: "Claude Code",
@@ -2874,6 +2879,21 @@ function complexOverlapBytes(input, output) {
2874
2879
  if (overlapBytes(input, output) && input.byteOffset < output.byteOffset)
2875
2880
  throw new Error("complex overlap of input and output is not supported");
2876
2881
  }
2882
+ function concatBytes(...arrays) {
2883
+ let sum = 0;
2884
+ for (let i = 0; i < arrays.length; i++) {
2885
+ const a = arrays[i];
2886
+ abytes(a);
2887
+ sum += a.length;
2888
+ }
2889
+ const res = new Uint8Array(sum);
2890
+ for (let i = 0, pad = 0; i < arrays.length; i++) {
2891
+ const a = arrays[i];
2892
+ res.set(a, pad);
2893
+ pad += a.length;
2894
+ }
2895
+ return res;
2896
+ }
2877
2897
  function checkOpts(defaults, opts) {
2878
2898
  aobject(defaults, "defaults");
2879
2899
  aobject(opts, "opts");
@@ -3588,6 +3608,12 @@ function decryptContent(key, associatedData, sealed) {
3588
3608
  ).decrypt(ciphertextBytes(sealed.ciphertext));
3589
3609
  return JSON.parse(bytesToUtf8(cleartext));
3590
3610
  }
3611
+ var attachmentData = (pairing, id) => utf8ToBytes(JSON.stringify([PROTOCOL_VERSION, "attachment", pairing, id]));
3612
+ function sealAttachment(key, pairing, id, bytes, random) {
3613
+ const nonce = random(NONCE_BYTES);
3614
+ const cipher = xchacha20poly1305(hexToBytes(key), nonce, attachmentData(pairing, id));
3615
+ return concatBytes(nonce, cipher.encrypt(bytes));
3616
+ }
3591
3617
  var PAIRING_CODE = new RegExp(
3592
3618
  `^DASHUP:${PROTOCOL_VERSION}:([0-9A-F]{64}):([0-9A-F]{16}):([0-9A-F]{64}):(.+)$`
3593
3619
  );
@@ -3615,8 +3641,6 @@ function loadConfig() {
3615
3641
  api: PRODUCT.api,
3616
3642
  secret: "",
3617
3643
  pairs: {},
3618
- screen: false,
3619
- control: false,
3620
3644
  outbox: [],
3621
3645
  localSecret: ""
3622
3646
  };
@@ -3642,10 +3666,14 @@ function saveConfig(config2) {
3642
3666
  }
3643
3667
  }
3644
3668
  async function api(config2, path, body2, method = body2 ? "POST" : "GET") {
3669
+ const bytes = body2 instanceof Uint8Array;
3645
3670
  const response = await fetch(`${config2.api}/v1${path}`, {
3646
3671
  method,
3647
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${config2.secret}` },
3648
- body: body2 ? JSON.stringify(body2) : void 0
3672
+ headers: {
3673
+ "Content-Type": bytes ? "application/octet-stream" : "application/json",
3674
+ Authorization: `Bearer ${config2.secret}`
3675
+ },
3676
+ body: bytes ? new Uint8Array(body2) : body2 ? JSON.stringify(body2) : void 0
3649
3677
  });
3650
3678
  const value2 = await response.json();
3651
3679
  if (!response.ok || !value2.success)
@@ -4017,7 +4045,7 @@ import * as ui from "@clack/prompts";
4017
4045
  import { z } from "zod";
4018
4046
 
4019
4047
  // package.json
4020
- var version = "0.1.8";
4048
+ var version = "0.1.10";
4021
4049
 
4022
4050
  // src/pair.ts
4023
4051
  var Invitation = z.object({
@@ -4147,282 +4175,19 @@ async function link(config2, account) {
4147
4175
  return true;
4148
4176
  }
4149
4177
 
4150
- // src/viewer.ts
4151
- import { existsSync as existsSync2 } from "node:fs";
4152
- import { fileURLToPath } from "node:url";
4153
- import { createInterface as createInterface2 } from "node:readline";
4154
- import { spawn as spawn2 } from "node:child_process";
4155
-
4156
- // src/screen.ts
4157
- import { promisify as promisify2 } from "node:util";
4158
- import { execFile as execFile2, spawnSync } from "node:child_process";
4159
- var run = promisify2(execFile2);
4160
- var powershell = (source) => run("powershell.exe", [
4161
- "-NoProfile",
4162
- "-NonInteractive",
4163
- "-EncodedCommand",
4164
- Buffer.from(source, "utf16le").toString("base64")
4165
- ]);
4166
- var psString = (value2) => `[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${Buffer.from(value2).toString("base64")}'))`;
4167
- function has(command2) {
4168
- return spawnSync(process.platform === "win32" ? "where.exe" : "which", [command2], { stdio: "ignore" }).status === 0;
4169
- }
4170
- function controlAvailable() {
4171
- return process.platform === "darwin" || process.platform === "win32" || has(process.env.WAYLAND_DISPLAY ? "ydotool" : "xdotool");
4172
- }
4173
- var KEYS = {
4174
- a: { x11: "a", linux: 30, mac: "a", windows: "a" },
4175
- c: { x11: "c", linux: 46, mac: "c", windows: "c" },
4176
- v: { x11: "v", linux: 47, mac: "v", windows: "v" },
4177
- z: { x11: "z", linux: 44, mac: "z", windows: "z" },
4178
- Tab: { x11: "Tab", linux: 15, mac: 48, windows: "{TAB}" },
4179
- Enter: { x11: "Return", linux: 28, mac: 36, windows: "{ENTER}" },
4180
- Escape: { x11: "Escape", linux: 1, mac: 53, windows: "{ESC}" },
4181
- ArrowUp: { x11: "Up", linux: 103, mac: 126, windows: "{UP}" },
4182
- Backspace: { x11: "BackSpace", linux: 14, mac: 51, windows: "{BACKSPACE}" },
4183
- ArrowDown: { x11: "Down", linux: 108, mac: 125, windows: "{DOWN}" },
4184
- ArrowLeft: { x11: "Left", linux: 105, mac: 123, windows: "{LEFT}" },
4185
- ArrowRight: { x11: "Right", linux: 106, mac: 124, windows: "{RIGHT}" }
4186
- };
4187
- var MODIFIERS = {
4188
- alt: { x11: "alt", linux: 56, mac: "option down", windows: "%" },
4189
- meta: { x11: "super", linux: 125, mac: "command down", windows: "" },
4190
- shift: { x11: "shift", linux: 42, mac: "shift down", windows: "+" },
4191
- control: { x11: "ctrl", linux: 29, mac: "control down", windows: "^" }
4192
- };
4193
- var SCROLL_LIMIT = 10;
4194
- var WINDOWS_INPUT = `Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class DashupInput { [DllImport("user32.dll")] public static extern bool SetCursorPos(int x,int y); [DllImport("user32.dll")] public static extern void mouse_event(uint f,uint x,uint y,int d,UIntPtr e); [DllImport("user32.dll")] public static extern void keybd_event(byte v,byte s,uint f,UIntPtr e); }'; `;
4195
- async function controlScreen(input) {
4196
- if (input.kind === "click" && (!Number.isInteger(input.x) || !Number.isInteger(input.y) || input.x < -32768 || input.y < -32768 || input.x > 32767 || input.y > 32767))
4197
- throw new Error("Invalid screen coordinates");
4198
- if (input.kind === "text" && (typeof input.text !== "string" || input.text.length > 16e3))
4199
- throw new Error("Text is too long");
4200
- if (input.kind === "scroll" && (!Number.isInteger(input.dx) || !Number.isInteger(input.dy) || Math.abs(input.dx) > SCROLL_LIMIT || Math.abs(input.dy) > SCROLL_LIMIT))
4201
- throw new Error("Invalid scroll distance");
4202
- const key = input.kind === "key" && Object.hasOwn(KEYS, input.key) ? KEYS[input.key] : void 0;
4203
- if (input.kind === "key" && !key) throw new Error("This key is not supported");
4204
- const modifiers = input.kind === "key" ? input.modifiers ?? [] : [];
4205
- if (!Array.isArray(modifiers) || modifiers.length > 4 || modifiers.some((modifier) => !Object.hasOwn(MODIFIERS, modifier)))
4206
- throw new Error("This modifier is not supported");
4207
- const chord = [...new Set(modifiers)].map((modifier) => MODIFIERS[modifier]);
4208
- if (process.platform === "darwin") {
4209
- if (input.kind === "scroll") {
4210
- await run("osascript", [
4211
- "-l",
4212
- "JavaScript",
4213
- "-e",
4214
- `ObjC.import('ApplicationServices'); const event = $.CGEventCreateScrollWheelEvent2(null, 0, 2, ${-input.dy}, ${-input.dx}, 0); if (!event) throw Error('Could not create scroll event'); $.CGEventPost(0, event); $.CFRelease(event);`
4215
- ]);
4216
- return;
4217
- }
4218
- const command2 = input.kind === "click" ? `click at {${input.x}, ${input.y}}` : input.kind === "key" ? `${typeof key.mac === "number" ? "key code " + key.mac : "keystroke " + JSON.stringify(key.mac)}${chord.length ? " using {" + chord.map((modifier) => modifier.mac).join(", ") + "}" : ""}` : `keystroke ${JSON.stringify(input.text)}`;
4219
- await run("osascript", ["-e", `tell application "System Events" to ${command2}`]);
4220
- } else if (process.platform === "win32") {
4221
- if (input.kind === "click" || input.kind === "scroll") {
4222
- const mouse = (flags, data = 0) => `[DashupInput]::mouse_event(${flags},0,0,${data},[UIntPtr]::Zero); `;
4223
- await powershell(
4224
- WINDOWS_INPUT + (input.kind === "click" ? `[DashupInput]::SetCursorPos(${input.x},${input.y}); ` + mouse(2) + mouse(4) : mouse(2048, -input.dy * 120) + mouse(4096, input.dx * 120))
4225
- );
4226
- } else {
4227
- const text3 = input.kind === "key" ? chord.map((modifier) => modifier.windows).join("") + key.windows : input.text.replace(/[+^%~(){}\[\]]/g, "{$&}");
4228
- const send = `Add-Type -AssemblyName System.Windows.Forms; [Windows.Forms.SendKeys]::SendWait(${psString(text3)})`;
4229
- await powershell(
4230
- modifiers.includes("meta") ? WINDOWS_INPUT + `[DashupInput]::keybd_event(91,0,0,[UIntPtr]::Zero); try { ${send} } finally { [DashupInput]::keybd_event(91,0,2,[UIntPtr]::Zero) }` : send
4231
- );
4232
- }
4233
- } else if (process.env.WAYLAND_DISPLAY) {
4234
- if (input.kind === "text") await run("ydotool", ["type", "--", input.text]);
4235
- else if (input.kind === "key")
4236
- await run("ydotool", [
4237
- "key",
4238
- ...chord.map((modifier) => `${modifier.linux}:1`),
4239
- `${key.linux}:1`,
4240
- `${key.linux}:0`,
4241
- ...[...chord].reverse().map((modifier) => `${modifier.linux}:0`)
4242
- ]);
4243
- else if (input.kind === "scroll")
4244
- await run("ydotool", [
4245
- "mousemove",
4246
- "--wheel",
4247
- "-x",
4248
- String(input.dx),
4249
- "-y",
4250
- String(-input.dy)
4251
- ]);
4252
- else {
4253
- await run("ydotool", [
4254
- "mousemove",
4255
- "--absolute",
4256
- "-x",
4257
- String(input.x),
4258
- "-y",
4259
- String(input.y)
4260
- ]);
4261
- await run("ydotool", ["click", "0xC0"]);
4262
- }
4263
- } else {
4264
- if (input.kind === "scroll") {
4265
- for (const [distance, negative, positive] of [
4266
- [input.dy, 4, 5],
4267
- [input.dx, 6, 7]
4268
- ])
4269
- if (distance)
4270
- await run("xdotool", [
4271
- "click",
4272
- "--clearmodifiers",
4273
- "--repeat",
4274
- String(Math.abs(distance)),
4275
- "--delay",
4276
- "0",
4277
- String(distance < 0 ? negative : positive)
4278
- ]);
4279
- return;
4280
- }
4281
- await run(
4282
- "xdotool",
4283
- input.kind === "click" ? ["mousemove", String(input.x), String(input.y), "click", "1"] : input.kind === "key" ? [
4284
- "key",
4285
- "--clearmodifiers",
4286
- [...chord.map((modifier) => modifier.x11), key.x11].join("+")
4287
- ] : ["type", "--clearmodifiers", "--", input.text]
4288
- );
4289
- }
4290
- }
4291
-
4292
- // src/viewer.ts
4293
- var DESKTOP_BIN = process.env.DASHUP_DESKTOP_BIN ?? fileURLToPath(
4294
- new URL(
4295
- `../../../packages/desktop/target/release/dashup-desktop${process.platform === "win32" ? ".exe" : ""}`,
4296
- import.meta.url
4297
- )
4298
- );
4299
- var Viewer = class _Viewer {
4300
- constructor(config2, isActive, send) {
4301
- this.config = config2;
4302
- this.isActive = isActive;
4303
- this.send = send;
4304
- }
4305
- config;
4306
- isActive;
4307
- send;
4308
- sessions = /* @__PURE__ */ new Map();
4309
- process = null;
4310
- pending = null;
4311
- static available() {
4312
- return existsSync2(DESKTOP_BIN);
4313
- }
4314
- async start(pair, projectId, session) {
4315
- if (!this.config.screen || !this.isActive(pair))
4316
- throw new Error("Screen access is no longer enabled for this pairing");
4317
- if (!_Viewer.available())
4318
- throw new Error("Build the Dashup desktop viewer helper on this computer first");
4319
- if (this.sessions.has(session) || this.pending?.id === session)
4320
- throw new Error("Screen session already exists");
4321
- const previous = [...this.sessions];
4322
- this.close();
4323
- const pending = { pair, projectId, id: session };
4324
- this.pending = pending;
4325
- try {
4326
- for (const [id, current] of previous) await this.sendClose(id, current);
4327
- if (this.pending !== pending) throw new Error("Screen request was cancelled");
4328
- const { iceServers } = await api(this.config, "/dashup/ice");
4329
- if (this.pending !== pending || !this.config.screen || !this.isActive(pair))
4330
- throw new Error("Screen access is no longer enabled for this pairing");
4331
- const child = spawn2(DESKTOP_BIN, [], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
4332
- this.process = child;
4333
- let events = Promise.resolve();
4334
- createInterface2({ input: child.stdout }).on("error", () => this.failed(child)).on("line", (line) => {
4335
- events = events.then(() => {
4336
- if (this.process === child) return this.receive(line);
4337
- }).catch(() => {
4338
- process.stderr.write("Desktop viewer event failed\n");
4339
- });
4340
- });
4341
- child.stderr.resume();
4342
- child.stdin.on("error", () => this.failed(child));
4343
- child.once("error", () => this.failed(child));
4344
- child.once("exit", () => this.failed(child));
4345
- this.sessions.set(session, { pair, projectId });
4346
- this.write({ kind: "start", session, iceServers });
4347
- } finally {
4348
- if (this.pending === pending) this.pending = null;
4349
- }
4350
- }
4351
- signal(pair, projectId, session, signal) {
4352
- const current = this.sessions.get(session);
4353
- const pending = this.pending?.id === session ? this.pending : null;
4354
- const owner = current ?? pending;
4355
- if (!owner || owner.pair.id !== pair.id || owner.projectId !== projectId || !this.config.screen || !this.isActive(pair))
4356
- throw new Error("Screen session is no longer active");
4357
- if (signal.kind === "close") this.stop(pair, session);
4358
- else if (!current) throw new Error("Screen session is not ready");
4359
- else this.write({ kind: "signal", session, signal });
4360
- }
4361
- stop(pair, session) {
4362
- if (this.pending?.id === session && this.pending.pair.id === pair.id) this.pending = null;
4363
- if (this.sessions.get(session)?.pair.id !== pair.id) return;
4364
- this.write({ kind: "stop", session });
4365
- this.sessions.delete(session);
4366
- if (!this.sessions.size) this.close();
4367
- }
4368
- revoke(pairings) {
4369
- if (this.pending && !pairings.has(this.pending.pair.id)) this.pending = null;
4370
- for (const [session, current] of this.sessions)
4371
- if (!pairings.has(current.pair.id)) this.stop(current.pair, session);
4372
- }
4373
- close() {
4374
- const child = this.process;
4375
- this.pending = null;
4376
- this.process = null;
4377
- this.sessions.clear();
4378
- child?.kill();
4379
- }
4380
- sendClose(sessionId, { pair, projectId }) {
4381
- return this.send(pair, { type: "signal", projectId, sessionId, signal: { kind: "close" } });
4382
- }
4383
- write(value2) {
4384
- this.process?.stdin.write(JSON.stringify(value2) + "\n");
4385
- }
4386
- failed(child) {
4387
- if (this.process !== child) return;
4388
- const sessions = [...this.sessions];
4389
- this.close();
4390
- for (const [sessionId, current] of sessions)
4391
- void this.sendClose(sessionId, current).catch(() => process.stderr.write("Desktop viewer closed\n"));
4392
- }
4393
- async receive(line) {
4394
- const event = JSON.parse(line);
4395
- const current = this.sessions.get(event.session);
4396
- if (!current || !this.config.screen || !this.isActive(current.pair)) return;
4397
- if (event.kind === "signal" && event.signal)
4398
- await this.send(current.pair, {
4399
- type: "signal",
4400
- sessionId: event.session,
4401
- projectId: current.projectId,
4402
- signal: event.signal
4403
- });
4404
- else if (event.kind === "input" && event.input && this.config.control)
4405
- await controlScreen(event.input);
4406
- else if (event.kind === "error" || event.state === "failed" || event.state === "closed" || event.state === "disconnected") {
4407
- this.stop(current.pair, event.session);
4408
- await this.sendClose(event.session, current);
4409
- }
4410
- }
4411
- };
4412
-
4413
4178
  // src/discovery.ts
4414
4179
  import { homedir as homedir3 } from "node:os";
4415
4180
  import { createHmac } from "node:crypto";
4416
4181
  import { basename, dirname as dirname3, join as join3 } from "node:path";
4417
4182
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
4418
4183
  import { zstdDecompressSync } from "node:zlib";
4419
- import { watch, existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
4184
+ import { watch, existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
4420
4185
 
4421
4186
  // src/harness/history.ts
4422
4187
  import { homedir as homedir2 } from "node:os";
4423
4188
  import { DatabaseSync } from "node:sqlite";
4424
4189
  import { join as join2, dirname as dirname2, resolve as resolve2, isAbsolute } from "node:path";
4425
- import { closeSync as closeSync2, existsSync as existsSync3, openSync as openSync2, readSync, realpathSync, readdirSync, statSync } from "node:fs";
4190
+ import { closeSync as closeSync2, existsSync as existsSync2, openSync as openSync2, readSync, realpathSync, readdirSync, statSync } from "node:fs";
4426
4191
 
4427
4192
  // src/harness/session.ts
4428
4193
  var EARLIER = {
@@ -4437,8 +4202,15 @@ function agentHarness(name) {
4437
4202
  if (/opencode/i.test(name)) return "opencode";
4438
4203
  return void 0;
4439
4204
  }
4205
+ var attached = (id, mime, data, name) => ({
4206
+ id,
4207
+ mime,
4208
+ bytes: Buffer.byteLength(data, "base64"),
4209
+ ...name ? { name: name.slice(-ATTACHMENT_NAME_CHARS) } : {},
4210
+ read: () => Buffer.from(data, "base64")
4211
+ });
4440
4212
  var MAX_PREVIEW = 64 * 1024;
4441
- var SCREEN_PREVIEW = "Open Screen to review the full request before approving.";
4213
+ var NO_PREVIEW = "Review the full request on your computer before approving.";
4442
4214
 
4443
4215
  // src/harness/history.ts
4444
4216
  var SAMPLE_BYTES = 128 * 1024;
@@ -4482,10 +4254,10 @@ function readRange(path, start, length) {
4482
4254
  closeSync2(fd);
4483
4255
  }
4484
4256
  }
4485
- function sample(path, until) {
4257
+ function described(path, until, describe2) {
4486
4258
  const stamp = statSync(path);
4487
4259
  const saved = metadata.get(path);
4488
- if (saved && unchanged(saved.stamp, stamp)) return saved.rows;
4260
+ if (saved && unchanged(saved.stamp, stamp)) return saved.value;
4489
4261
  let head = readRange(path, 0, Math.min(stamp.size, SAMPLE_BYTES));
4490
4262
  if (head.length < stamp.size && head.at(-1) !== 10) {
4491
4263
  const chunks = [head];
@@ -4508,9 +4280,9 @@ function sample(path, until) {
4508
4280
  );
4509
4281
  if (start === head.length || tail.some(until)) break;
4510
4282
  }
4511
- const rows = [...records(head), ...tail];
4512
- metadata.set(path, { stamp, rows });
4513
- return rows;
4283
+ const value2 = describe2([...records(head), ...tail], stamp);
4284
+ metadata.set(path, { stamp, value: value2 });
4285
+ return value2;
4514
4286
  }
4515
4287
  var windowed = (path, messages5) => journals.get(path)?.start ? [EARLIER, ...messages5] : messages5;
4516
4288
  function journal(path, latest = false) {
@@ -4552,7 +4324,7 @@ function journal(path, latest = false) {
4552
4324
  return tail ? [...saved.rows, tail] : saved.rows;
4553
4325
  }
4554
4326
  function files(root, scan, depth = Infinity) {
4555
- if (!existsSync3(root)) {
4327
+ if (!existsSync2(root)) {
4556
4328
  watchParent(root, scan);
4557
4329
  return [];
4558
4330
  }
@@ -4569,8 +4341,8 @@ function files(root, scan, depth = Infinity) {
4569
4341
  }
4570
4342
  }
4571
4343
  function watchParent(path, scan) {
4572
- while (!existsSync3(path) && dirname2(path) !== path) path = dirname2(path);
4573
- if (existsSync3(path)) scan.paths.add(path);
4344
+ while (!existsSync2(path) && dirname2(path) !== path) path = dirname2(path);
4345
+ if (existsSync2(path)) scan.paths.add(path);
4574
4346
  }
4575
4347
  function canonical(cwd) {
4576
4348
  if (typeof cwd !== "string" || !isAbsolute(cwd)) return void 0;
@@ -4596,13 +4368,27 @@ function text(value2) {
4596
4368
  const part = object(value2);
4597
4369
  return part ? typeof part.text === "string" ? part.text : text(part.content) : "";
4598
4370
  }
4371
+ var DATA_URL = /^data:([^;,]+);base64,/;
4372
+ function attachments(id, content) {
4373
+ return list(content).flatMap((part, index) => {
4374
+ const key = `${id}:${index}`;
4375
+ const name = [part?.title, part?.filename].find((value2) => typeof value2 === "string");
4376
+ const source = object(part?.source);
4377
+ if (source?.type === "base64" && typeof source.data === "string")
4378
+ return [attached(key, String(source.media_type), source.data, name)];
4379
+ const url2 = String(part?.image_url ?? part?.url ?? "");
4380
+ const data = DATA_URL.exec(url2);
4381
+ return data ? [attached(key, data[1], url2.slice(data[0].length), name)] : [];
4382
+ });
4383
+ }
4599
4384
  function prose(id, role, content) {
4600
4385
  const value2 = text(
4601
4386
  Array.isArray(content) ? content.filter(
4602
4387
  (part) => typeof part === "string" || ["text", "input_text", "output_text"].includes(part?.type)
4603
4388
  ) : content
4604
4389
  );
4605
- return value2 ? [{ id, role, text: value2 }] : [];
4390
+ const files2 = attachments(id, content);
4391
+ return value2 || files2.length ? [{ id, role, text: value2, ...files2.length ? { attachments: files2 } : {} }] : [];
4606
4392
  }
4607
4393
  var SUMMARY_LIMIT = 200;
4608
4394
  var SUMMARY_FIELDS = [
@@ -4621,10 +4407,10 @@ var SUMMARY_FIELDS = [
4621
4407
  var PATCH_FILE = /^\*\*\* (Add|Update|Delete) File: (.+)$/gm;
4622
4408
  function describe(input) {
4623
4409
  const fields = object(input);
4624
- const named = SUMMARY_FIELDS.map((key) => fields?.[key]).find(
4410
+ const named2 = SUMMARY_FIELDS.map((key) => fields?.[key]).find(
4625
4411
  (value3) => typeof value3 === "string" && value3.trim() !== ""
4626
4412
  );
4627
- const value2 = typeof input === "string" ? input : named ?? (fields && Object.keys(fields).length ? JSON.stringify(fields) : "");
4413
+ const value2 = typeof input === "string" ? input : named2 ?? (fields && Object.keys(fields).length ? JSON.stringify(fields) : "");
4628
4414
  const paths = [...value2.matchAll(PATCH_FILE)].map((match) => match[2]).join(", ");
4629
4415
  const first = value2.split("\n").find((line) => line.trim());
4630
4416
  return (paths || first?.trim())?.slice(0, SUMMARY_LIMIT);
@@ -4642,9 +4428,10 @@ function added(path, content) {
4642
4428
  ...lines.map((line) => `+${line}`)
4643
4429
  ].join("\n");
4644
4430
  }
4431
+ var binary = (diff) => diff.slice(0, 8e3).includes("\0");
4645
4432
  function step(id, name, input, result) {
4646
4433
  const summary = describe(input);
4647
- const diff = result?.status === "ok" ? result.diff : void 0;
4434
+ const diff = result?.status === "ok" && result.diff && !binary(result.diff) ? result.diff : void 0;
4648
4435
  return {
4649
4436
  id,
4650
4437
  role: "tool",
@@ -4655,7 +4442,8 @@ function step(id, name, input, result) {
4655
4442
  ...result?.status ? { status: result.status } : {},
4656
4443
  ...result?.status && result.ms !== void 0 ? { ms: result.ms } : {},
4657
4444
  ...diff ? { diff: true } : {}
4658
- }
4445
+ },
4446
+ ...result?.attachments?.length ? { attachments: result.attachments } : {}
4659
4447
  };
4660
4448
  }
4661
4449
  var claudeHome = () => process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
@@ -4711,16 +4499,18 @@ function claudeConversation(conversation, rows) {
4711
4499
  ...list(content).flatMap((part, position) => {
4712
4500
  if (part?.type !== "tool_use") return [];
4713
4501
  const result = results.get(part.id);
4502
+ const call = String(part.id ?? `${id}:${position}`);
4714
4503
  return [
4715
4504
  step(
4716
- String(part.id ?? `${id}:${position}`),
4505
+ call,
4717
4506
  part.name,
4718
4507
  part.input,
4719
4508
  result && {
4720
4509
  text: text(result.part.content),
4721
4510
  status: result.part.is_error ? "error" : "ok",
4722
4511
  ms: elapsed(row.timestamp, result.row.timestamp),
4723
- diff: claudeChange(object(result.row.toolUseResult))
4512
+ diff: claudeChange(object(result.row.toolUseResult)),
4513
+ attachments: attachments(call, result.part.content)
4724
4514
  }
4725
4515
  )
4726
4516
  ];
@@ -4731,26 +4521,31 @@ function claudeConversation(conversation, rows) {
4731
4521
  function claude(root, scan) {
4732
4522
  for (const path of files(join2(root, "projects"), scan, 1).filter((file) => file.endsWith(".jsonl"))) {
4733
4523
  try {
4734
- const rows = sample(path, claudeMessage);
4735
- const latest = [...rows].reverse();
4736
- const owner = rows.find((row) => typeof row.sessionId === "string" && canonical(row.cwd));
4737
- if (!owner) {
4524
+ const session = described(path, claudeMessage, (rows, stamp) => {
4525
+ const latest = [...rows].reverse();
4526
+ const owner = rows.find((row) => typeof row.sessionId === "string" && canonical(row.cwd));
4527
+ if (!owner) return void 0;
4528
+ const named2 = latest.find((row) => row.type === "custom-title" || row.type === "ai-title");
4529
+ const first = rows.find(
4530
+ (row) => row.type === "user" && !row.isMeta && !row.isSidechain && !injected(row.message?.content)
4531
+ );
4532
+ return {
4533
+ id: String(owner.sessionId),
4534
+ cwd: canonical(owner.cwd),
4535
+ title: title(
4536
+ named2?.customTitle ?? named2?.aiTitle ?? text(first?.message?.content),
4537
+ "Claude Code thread"
4538
+ ),
4539
+ updatedAt: date(latest.find(claudeMessage)?.timestamp, stamp.mtimeMs)
4540
+ };
4541
+ });
4542
+ if (!session) {
4738
4543
  scan.warnings.push(`Unrecognized Claude Code session: ${path}`);
4739
4544
  continue;
4740
4545
  }
4741
- const named = latest.find((row) => row.type === "custom-title" || row.type === "ai-title");
4742
- const first = rows.find(
4743
- (row) => row.type === "user" && !row.isMeta && !row.isSidechain && !injected(row.message?.content)
4744
- );
4745
4546
  scan.sessions.push({
4746
4547
  harness: "claude",
4747
- id: owner.sessionId,
4748
- cwd: canonical(owner.cwd),
4749
- title: title(
4750
- named?.customTitle ?? named?.aiTitle ?? text(first?.message?.content),
4751
- "Claude Code thread"
4752
- ),
4753
- updatedAt: date(latest.find(claudeMessage)?.timestamp, statSync(path).mtimeMs),
4548
+ ...session,
4754
4549
  files: [path],
4755
4550
  read: () => claudeMessages(path)
4756
4551
  });
@@ -4794,15 +4589,17 @@ function codexMessages(path) {
4794
4589
  const input = item.type === "function_call" ? parse(String(item.arguments)) ?? item.arguments : item.input;
4795
4590
  const output = outputs.get(item.call_id);
4796
4591
  const patch = item.name === "apply_patch" && typeof input === "string" ? input : "";
4592
+ const call = String(item.call_id ?? id);
4797
4593
  return [
4798
4594
  step(
4799
- String(item.call_id ?? id),
4595
+ call,
4800
4596
  item.name,
4801
4597
  input,
4802
4598
  output && {
4803
4599
  ...codexResult(text(output.payload.output)),
4804
4600
  ms: elapsed(row.timestamp, output.timestamp),
4805
- diff: patch && applyPatch(patch)
4601
+ diff: patch && applyPatch(patch),
4602
+ attachments: attachments(call, output.payload.output)
4806
4603
  }
4807
4604
  )
4808
4605
  ];
@@ -4822,7 +4619,7 @@ function codexMessages(path) {
4822
4619
  function codex(root, scan) {
4823
4620
  const index = join2(root, "session_index.jsonl");
4824
4621
  const names = /* @__PURE__ */ new Map();
4825
- if (existsSync3(index)) {
4622
+ if (existsSync2(index)) {
4826
4623
  scan.paths.add(root);
4827
4624
  try {
4828
4625
  for (const row of journal(index)) if (typeof row.id === "string") names.set(row.id, row);
@@ -4831,34 +4628,41 @@ function codex(root, scan) {
4831
4628
  }
4832
4629
  }
4833
4630
  const titles = codexTitles(root);
4631
+ const indexed = existsSync2(index);
4834
4632
  for (const path of [
4835
4633
  ...files(join2(root, "sessions"), scan),
4836
4634
  ...files(join2(root, "archived_sessions"), scan)
4837
4635
  ].filter((file) => file.endsWith(".jsonl"))) {
4838
4636
  try {
4839
- const rows = sample(path, codexItem);
4840
- const latest = [...rows].reverse();
4841
- const owner = rows.find((row) => row.type === "session_meta")?.payload;
4842
- const cwd = canonical(owner?.cwd);
4843
- if (typeof owner?.id !== "string" || !cwd) {
4637
+ const session = described(path, codexItem, (rows, stamp) => {
4638
+ const owner = rows.find((row) => row.type === "session_meta")?.payload;
4639
+ const cwd = canonical(owner?.cwd);
4640
+ if (typeof owner?.id !== "string" || !cwd) return void 0;
4641
+ const first = rows.find(
4642
+ (row) => row.type === "response_item" && row.payload?.type === "message" && row.payload.role === "user" && !injected(row.payload.content)
4643
+ );
4644
+ const parent = owner.parent_thread_id;
4645
+ return {
4646
+ id: owner.id,
4647
+ cwd,
4648
+ asked: text(first?.payload?.content),
4649
+ parent: typeof parent === "string" ? parent : void 0,
4650
+ updatedAt: date([...rows].reverse().find(codexItem)?.timestamp, stamp.mtimeMs)
4651
+ };
4652
+ });
4653
+ if (!session) {
4844
4654
  scan.warnings.push(`Unrecognized Codex session: ${path}`);
4845
4655
  continue;
4846
4656
  }
4847
- const named = names.get(owner.id);
4848
- const first = rows.find(
4849
- (row) => row.type === "response_item" && row.payload?.type === "message" && row.payload.role === "user" && !injected(row.payload.content)
4850
- );
4657
+ const { asked, ...found } = session;
4851
4658
  scan.sessions.push({
4852
4659
  harness: "codex",
4853
- id: owner.id,
4854
- cwd,
4660
+ ...found,
4855
4661
  title: title(
4856
- named?.thread_name ?? titles.get(owner.id) ?? text(first?.payload?.content),
4662
+ names.get(found.id)?.thread_name ?? titles.get(found.id) ?? asked,
4857
4663
  "Codex thread"
4858
4664
  ),
4859
- parent: typeof owner.parent_thread_id === "string" ? owner.parent_thread_id : void 0,
4860
- updatedAt: date(latest.find(codexItem)?.timestamp, statSync(path).mtimeMs),
4861
- files: [path, ...existsSync3(index) ? [index] : []],
4665
+ files: [path, ...indexed ? [index] : []],
4862
4666
  read: () => codexMessages(path)
4863
4667
  });
4864
4668
  } catch {
@@ -4877,6 +4681,10 @@ function openCodeParts(id, role, parts2) {
4877
4681
  const key = String(part.id ?? `${id}:${index}`);
4878
4682
  if (part.type === "text" && typeof part.text === "string")
4879
4683
  return [{ id: key, role, text: part.text }];
4684
+ if (part.type === "file") {
4685
+ const files2 = attachments(key, [part]);
4686
+ return files2.length ? [{ id: key, role, text: "", attachments: files2 }] : [];
4687
+ }
4880
4688
  if (part.type !== "tool") return [];
4881
4689
  const state = object(part.state) ?? {};
4882
4690
  const status2 = state.status === "completed" ? "ok" : state.status === "error" ? "error" : void 0;
@@ -4889,7 +4697,8 @@ function openCodeParts(id, role, parts2) {
4889
4697
  text: text(state.output ?? state.error),
4890
4698
  status: status2,
4891
4699
  ms: elapsed(state.time?.start, state.time?.end),
4892
- diff: openCodeChange(state)
4700
+ diff: openCodeChange(state),
4701
+ attachments: attachments(key, state.attachments)
4893
4702
  }
4894
4703
  )
4895
4704
  ];
@@ -4925,17 +4734,22 @@ function databaseMessages(path, session) {
4925
4734
  }
4926
4735
  }
4927
4736
  function codexSubagent(row) {
4928
- const spawn7 = object(object(parse(String(row.source ?? "")))?.subagent)?.thread_spawn;
4929
- const task = typeof spawn7?.agent_path === "string" ? spawn7.agent_path.split("/").pop() : "";
4737
+ const spawn6 = object(object(parse(String(row.source ?? "")))?.subagent)?.thread_spawn;
4738
+ const task = typeof spawn6?.agent_path === "string" ? spawn6.agent_path.split("/").pop() : "";
4930
4739
  return [row.agent_nickname, task?.replace(/_/g, " ")].filter(Boolean).join(" \xB7 ");
4931
4740
  }
4741
+ var codexState;
4932
4742
  function codexTitles(root) {
4933
4743
  const titles = /* @__PURE__ */ new Map();
4934
- const state = (existsSync3(root) ? readdirSync(root) : []).filter((name) => /^state_\d+\.sqlite$/.test(name)).sort((a, b) => Number(b.match(/\d+/)[0]) - Number(a.match(/\d+/)[0]))[0];
4744
+ const state = (existsSync2(root) ? readdirSync(root) : []).filter((name) => /^state_\d+\.sqlite$/.test(name)).sort((a, b) => Number(b.match(/\d+/)[0]) - Number(a.match(/\d+/)[0]))[0];
4935
4745
  if (!state) return titles;
4746
+ const path = join2(root, state);
4747
+ const stamp = [path, `${path}-wal`].map((file) => statSync(file, { throwIfNoEntry: false })).map((found) => found ? `${found.ino}:${found.size}:${found.mtimeMs}` : "").join("|");
4748
+ if (codexState?.stamp === stamp) return codexState.titles;
4749
+ codexState = { stamp, titles };
4936
4750
  let db;
4937
4751
  try {
4938
- db = openDatabase(join2(root, state));
4752
+ db = openDatabase(path);
4939
4753
  for (const row of db.prepare("SELECT id, title, first_user_message, source, agent_nickname FROM threads").all()) {
4940
4754
  const name = [row.title, row.first_user_message].find(
4941
4755
  (value2) => typeof value2 === "string" && !!value2.trim() && !injected(value2)
@@ -4949,7 +4763,7 @@ function codexTitles(root) {
4949
4763
  return titles;
4950
4764
  }
4951
4765
  function openCodeDatabase(path, scan) {
4952
- if (!existsSync3(path)) {
4766
+ if (!existsSync2(path)) {
4953
4767
  watchParent(path, scan);
4954
4768
  return;
4955
4769
  }
@@ -5011,7 +4825,7 @@ function legacyMessages(root, session) {
5011
4825
  }
5012
4826
  function openCodeLegacy(root, scan) {
5013
4827
  const parts2 = join2(root, "part");
5014
- if (existsSync3(parts2)) files(parts2, scan, 1);
4828
+ if (existsSync2(parts2)) files(parts2, scan, 1);
5015
4829
  for (const path of files(join2(root, "session"), scan).filter((file) => file.endsWith(".json"))) {
5016
4830
  try {
5017
4831
  const row = jsonFile(path);
@@ -5064,7 +4878,22 @@ function scanHistories() {
5064
4878
  }
5065
4879
 
5066
4880
  // src/discovery.ts
5067
- var opaqueId = (value2, secret) => createHmac("sha256", secret).update(value2).digest("hex").slice(0, 32);
4881
+ var READ_ONLY = {
4882
+ read: true,
4883
+ send: false,
4884
+ queue: false,
4885
+ stop: false,
4886
+ approve: false
4887
+ };
4888
+ var opaqueIds = /* @__PURE__ */ new Map();
4889
+ var opaqueId = (value2, secret) => {
4890
+ const key = `${secret}\0${value2}`;
4891
+ const known = opaqueIds.get(key);
4892
+ if (known) return known;
4893
+ const id = createHmac("sha256", secret).update(value2).digest("hex").slice(0, 32);
4894
+ opaqueIds.set(key, id);
4895
+ return id;
4896
+ };
5068
4897
  var agentThread = (harness, session, secret) => opaqueId(`agent\0${harness}\0${session}`, secret);
5069
4898
  var zedHost = (value2) => typeof value2 === "string" ? value2 : value2 && typeof value2 === "object" ? String(Object.values(value2)[0] ?? "") : "";
5070
4899
  function vscodeHost(authority) {
@@ -5150,13 +4979,11 @@ function contentText(content) {
5150
4979
  return typeof part.text === "string" ? part.text : typeof part.Text === "string" ? part.Text : typeof part.value === "string" ? part.value : contentText(part.content);
5151
4980
  }
5152
4981
  var Discovery = class {
5153
- constructor(deviceId, capabilities, secret) {
4982
+ constructor(deviceId, secret) {
5154
4983
  this.deviceId = deviceId;
5155
- this.capabilities = capabilities;
5156
4984
  this.secret = secret;
5157
4985
  }
5158
4986
  deviceId;
5159
- capabilities;
5160
4987
  secret;
5161
4988
  projects = /* @__PURE__ */ new Map();
5162
4989
  locations = /* @__PURE__ */ new Map();
@@ -5192,7 +5019,7 @@ var Discovery = class {
5192
5019
  editor: "cli",
5193
5020
  status: "unknown",
5194
5021
  updatedAt: session.updatedAt,
5195
- capabilities: { ...this.capabilities },
5022
+ capabilities: { ...READ_ONLY },
5196
5023
  agent: { harness: session.harness, sessionId: session.id }
5197
5024
  };
5198
5025
  project.threads.push(thread);
@@ -5216,7 +5043,7 @@ var Discovery = class {
5216
5043
  const parent = dirname3(project.path);
5217
5044
  counts.set(parent, (counts.get(parent) ?? 0) + 1);
5218
5045
  }
5219
- const folders = [...counts].sort((a, b) => b[1] - a[1]).map(([folder]) => folder).filter((folder) => existsSync4(folder)).slice(0, 3);
5046
+ const folders = [...counts].sort((a, b) => b[1] - a[1]).map(([folder]) => folder).filter((folder) => existsSync3(folder)).slice(0, 3);
5220
5047
  return folders.length ? folders : [homedir3()];
5221
5048
  }
5222
5049
  observe(session) {
@@ -5253,7 +5080,7 @@ var Discovery = class {
5253
5080
  editor: "acp",
5254
5081
  status: "idle",
5255
5082
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5256
- capabilities: this.capabilities
5083
+ capabilities: READ_ONLY
5257
5084
  };
5258
5085
  project.threads.push(thread);
5259
5086
  this.locations.set(id, { editor: "acp", path: "", session });
@@ -5281,11 +5108,11 @@ var Discovery = class {
5281
5108
  zed(root) {
5282
5109
  const database = join3(root, "db");
5283
5110
  const store = join3(root, "threads/threads.db");
5284
- if (!existsSync4(database)) return;
5111
+ if (!existsSync3(database)) return;
5285
5112
  this.paths.add(database);
5286
5113
  for (const entry of readdirSync2(database)) {
5287
5114
  const path = join3(database, entry, "db.sqlite");
5288
- if (!existsSync4(path)) continue;
5115
+ if (!existsSync3(path)) continue;
5289
5116
  this.paths.add(join3(database, entry));
5290
5117
  try {
5291
5118
  if (!sqlite(
@@ -5294,7 +5121,7 @@ var Discovery = class {
5294
5121
  ).length)
5295
5122
  continue;
5296
5123
  const stamps = new Map(
5297
- existsSync4(store) ? sqlite(store, "SELECT id, updated_at FROM threads").map((row) => [
5124
+ existsSync3(store) ? sqlite(store, "SELECT id, updated_at FROM threads").map((row) => [
5298
5125
  row.id,
5299
5126
  row.updated_at
5300
5127
  ]) : []
@@ -5327,7 +5154,7 @@ var Discovery = class {
5327
5154
  editor: "zed",
5328
5155
  status: "unknown",
5329
5156
  updatedAt,
5330
- capabilities: this.capabilities,
5157
+ capabilities: READ_ONLY,
5331
5158
  ...harness ? { agent: { harness, sessionId: session } } : {}
5332
5159
  };
5333
5160
  project.threads.push(thread);
@@ -5342,17 +5169,17 @@ var Discovery = class {
5342
5169
  }
5343
5170
  }
5344
5171
  const threads = join3(root, "threads");
5345
- if (existsSync4(threads)) this.paths.add(threads);
5172
+ if (existsSync3(threads)) this.paths.add(threads);
5346
5173
  }
5347
5174
  vscode(root) {
5348
5175
  const workspaces = join3(root, "workspaceStorage");
5349
- if (!existsSync4(workspaces)) return;
5176
+ if (!existsSync3(workspaces)) return;
5350
5177
  this.paths.add(workspaces);
5351
5178
  for (const entry of readdirSync2(workspaces)) {
5352
5179
  const directory2 = join3(workspaces, entry);
5353
5180
  const workspace = join3(directory2, "workspace.json");
5354
5181
  const sessions = join3(directory2, "chatSessions");
5355
- if (!existsSync4(workspace)) continue;
5182
+ if (!existsSync3(workspace)) continue;
5356
5183
  this.paths.add(directory2);
5357
5184
  try {
5358
5185
  const data = JSON.parse(readFileSync2(workspace, "utf8"));
@@ -5361,7 +5188,7 @@ var Discovery = class {
5361
5188
  const path = decodeURIComponent(uri.pathname).replace(/^\/([A-Za-z]:\/)/, "$1");
5362
5189
  const host = uri.protocol === "vscode-remote:" ? vscodeHost(uri.host) : "";
5363
5190
  const project = this.project(path, host ? { host, path } : void 0);
5364
- if (!existsSync4(sessions)) continue;
5191
+ if (!existsSync3(sessions)) continue;
5365
5192
  this.paths.add(sessions);
5366
5193
  for (const filename of readdirSync2(sessions).filter((name) => /\.jsonl?$/.test(name))) {
5367
5194
  const file = join3(sessions, filename);
@@ -5373,7 +5200,7 @@ var Discovery = class {
5373
5200
  editor: "vscode",
5374
5201
  status: "unknown",
5375
5202
  updatedAt: new Date(state.lastMessageDate ?? state.creationDate ?? 0).toISOString(),
5376
- capabilities: this.capabilities
5203
+ capabilities: READ_ONLY
5377
5204
  });
5378
5205
  this.locations.set(id, { editor: "vscode", path: file });
5379
5206
  }
@@ -5382,12 +5209,12 @@ var Discovery = class {
5382
5209
  }
5383
5210
  }
5384
5211
  }
5385
- read(thread, attached) {
5212
+ read(thread, attached2) {
5386
5213
  const location2 = this.locations.get(thread.id);
5387
5214
  if (!location2) throw new Error("Thread is no longer available");
5388
5215
  if (location2.history) return location2.history.read();
5389
5216
  if (location2.live) return location2.live.messages;
5390
- if (attached) return attached;
5217
+ if (attached2) return attached2;
5391
5218
  if (location2.editor === "vscode") {
5392
5219
  const state = chatSession(location2.path);
5393
5220
  return (state.requests ?? []).flatMap((request2, index) => [
@@ -5399,16 +5226,16 @@ var Discovery = class {
5399
5226
  }
5400
5227
  ]).filter((message) => message.text);
5401
5228
  }
5402
- if (!existsSync4(location2.path))
5229
+ if (!existsSync3(location2.path))
5403
5230
  throw new Error(
5404
- "This external agent keeps its conversation in its own storage. Use Screen to view the open thread."
5231
+ "This external agent keeps its conversation in its own storage. View the open thread on your computer."
5405
5232
  );
5406
5233
  const db = new DatabaseSync2(location2.path, { readOnly: true });
5407
5234
  try {
5408
5235
  const row = db.prepare("SELECT data_type, data FROM threads WHERE id = ?").get(location2.session);
5409
5236
  if (!row)
5410
5237
  throw new Error(
5411
- "This agent history is not available on this computer. Open Screen to view the existing session."
5238
+ "This agent history is not available on this computer. View the existing session in Zed."
5412
5239
  );
5413
5240
  const data = row.data_type === "zstd" ? zstdDecompressSync(row.data) : row.data;
5414
5241
  const state = JSON.parse(Buffer.from(data).toString("utf8"));
@@ -10636,20 +10463,20 @@ var deflate$2 = (strm, flush) => {
10636
10463
  let beg = s.pending;
10637
10464
  let left = (s.gzhead.extra.length & 65535) - s.gzindex;
10638
10465
  while (s.pending + left > s.pending_buf_size) {
10639
- let copy = s.pending_buf_size - s.pending;
10640
- s.pending_buf.set(s.gzhead.extra.subarray(s.gzindex, s.gzindex + copy), s.pending);
10466
+ let copy2 = s.pending_buf_size - s.pending;
10467
+ s.pending_buf.set(s.gzhead.extra.subarray(s.gzindex, s.gzindex + copy2), s.pending);
10641
10468
  s.pending = s.pending_buf_size;
10642
10469
  if (s.gzhead.hcrc && s.pending > beg) {
10643
10470
  strm.adler = crc32_1(strm.adler, s.pending_buf, s.pending - beg, beg);
10644
10471
  }
10645
- s.gzindex += copy;
10472
+ s.gzindex += copy2;
10646
10473
  flush_pending(strm);
10647
10474
  if (s.pending !== 0) {
10648
10475
  s.last_flush = -1;
10649
10476
  return Z_OK$3;
10650
10477
  }
10651
10478
  beg = 0;
10652
- left -= copy;
10479
+ left -= copy2;
10653
10480
  }
10654
10481
  let gzhead_extra = new Uint8Array(s.gzhead.extra);
10655
10482
  s.pending_buf.set(gzhead_extra.subarray(s.gzindex, s.gzindex + left), s.pending);
@@ -11959,7 +11786,7 @@ var fixedtables = (state) => {
11959
11786
  state.distcode = distfix;
11960
11787
  state.distbits = 5;
11961
11788
  };
11962
- var updatewindow = (strm, src, end, copy) => {
11789
+ var updatewindow = (strm, src, end, copy2) => {
11963
11790
  let dist;
11964
11791
  const state = strm.state;
11965
11792
  if (state.window === null) {
@@ -11970,20 +11797,20 @@ var updatewindow = (strm, src, end, copy) => {
11970
11797
  state.wnext = 0;
11971
11798
  state.whave = 0;
11972
11799
  }
11973
- if (copy >= state.wsize) {
11800
+ if (copy2 >= state.wsize) {
11974
11801
  state.window.set(src.subarray(end - state.wsize, end), 0);
11975
11802
  state.wnext = 0;
11976
11803
  state.whave = state.wsize;
11977
11804
  } else {
11978
11805
  dist = state.wsize - state.wnext;
11979
- if (dist > copy) {
11980
- dist = copy;
11981
- }
11982
- state.window.set(src.subarray(end - copy, end - copy + dist), state.wnext);
11983
- copy -= dist;
11984
- if (copy) {
11985
- state.window.set(src.subarray(end - copy, end), 0);
11986
- state.wnext = copy;
11806
+ if (dist > copy2) {
11807
+ dist = copy2;
11808
+ }
11809
+ state.window.set(src.subarray(end - copy2, end - copy2 + dist), state.wnext);
11810
+ copy2 -= dist;
11811
+ if (copy2) {
11812
+ state.window.set(src.subarray(end - copy2, end), 0);
11813
+ state.wnext = copy2;
11987
11814
  state.whave = state.wsize;
11988
11815
  } else {
11989
11816
  state.wnext += dist;
@@ -12006,7 +11833,7 @@ var inflate$2 = (strm, flush) => {
12006
11833
  let hold;
12007
11834
  let bits;
12008
11835
  let _in, _out;
12009
- let copy;
11836
+ let copy2;
12010
11837
  let from;
12011
11838
  let from_source;
12012
11839
  let here = 0;
@@ -12205,11 +12032,11 @@ var inflate$2 = (strm, flush) => {
12205
12032
  /* falls through */
12206
12033
  case EXTRA:
12207
12034
  if (state.flags & 1024) {
12208
- copy = state.length;
12209
- if (copy > have) {
12210
- copy = have;
12035
+ copy2 = state.length;
12036
+ if (copy2 > have) {
12037
+ copy2 = have;
12211
12038
  }
12212
- if (copy) {
12039
+ if (copy2) {
12213
12040
  if (state.head) {
12214
12041
  len = state.head.extra_len - state.length;
12215
12042
  if (!state.head.extra) {
@@ -12220,18 +12047,18 @@ var inflate$2 = (strm, flush) => {
12220
12047
  next,
12221
12048
  // extra field is limited to 65536 bytes
12222
12049
  // - no need for additional size check
12223
- next + copy
12050
+ next + copy2
12224
12051
  ),
12225
12052
  /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/
12226
12053
  len
12227
12054
  );
12228
12055
  }
12229
12056
  if (state.flags & 512 && state.wrap & 4) {
12230
- state.check = crc32_1(state.check, input, copy, next);
12057
+ state.check = crc32_1(state.check, input, copy2, next);
12231
12058
  }
12232
- have -= copy;
12233
- next += copy;
12234
- state.length -= copy;
12059
+ have -= copy2;
12060
+ next += copy2;
12061
+ state.length -= copy2;
12235
12062
  }
12236
12063
  if (state.length) {
12237
12064
  break inf_leave;
@@ -12245,18 +12072,18 @@ var inflate$2 = (strm, flush) => {
12245
12072
  if (have === 0) {
12246
12073
  break inf_leave;
12247
12074
  }
12248
- copy = 0;
12075
+ copy2 = 0;
12249
12076
  do {
12250
- len = input[next + copy++];
12077
+ len = input[next + copy2++];
12251
12078
  if (state.head && len && state.length < 65536) {
12252
12079
  state.head.name += String.fromCharCode(len);
12253
12080
  }
12254
- } while (len && copy < have);
12081
+ } while (len && copy2 < have);
12255
12082
  if (state.flags & 512 && state.wrap & 4) {
12256
- state.check = crc32_1(state.check, input, copy, next);
12083
+ state.check = crc32_1(state.check, input, copy2, next);
12257
12084
  }
12258
- have -= copy;
12259
- next += copy;
12085
+ have -= copy2;
12086
+ next += copy2;
12260
12087
  if (len) {
12261
12088
  break inf_leave;
12262
12089
  }
@@ -12271,18 +12098,18 @@ var inflate$2 = (strm, flush) => {
12271
12098
  if (have === 0) {
12272
12099
  break inf_leave;
12273
12100
  }
12274
- copy = 0;
12101
+ copy2 = 0;
12275
12102
  do {
12276
- len = input[next + copy++];
12103
+ len = input[next + copy2++];
12277
12104
  if (state.head && len && state.length < 65536) {
12278
12105
  state.head.comment += String.fromCharCode(len);
12279
12106
  }
12280
- } while (len && copy < have);
12107
+ } while (len && copy2 < have);
12281
12108
  if (state.flags & 512 && state.wrap & 4) {
12282
- state.check = crc32_1(state.check, input, copy, next);
12109
+ state.check = crc32_1(state.check, input, copy2, next);
12283
12110
  }
12284
- have -= copy;
12285
- next += copy;
12111
+ have -= copy2;
12112
+ next += copy2;
12286
12113
  if (len) {
12287
12114
  break inf_leave;
12288
12115
  }
@@ -12417,23 +12244,23 @@ var inflate$2 = (strm, flush) => {
12417
12244
  state.mode = COPY;
12418
12245
  /* falls through */
12419
12246
  case COPY:
12420
- copy = state.length;
12421
- if (copy) {
12422
- if (copy > have) {
12423
- copy = have;
12247
+ copy2 = state.length;
12248
+ if (copy2) {
12249
+ if (copy2 > have) {
12250
+ copy2 = have;
12424
12251
  }
12425
- if (copy > left) {
12426
- copy = left;
12252
+ if (copy2 > left) {
12253
+ copy2 = left;
12427
12254
  }
12428
- if (copy === 0) {
12255
+ if (copy2 === 0) {
12429
12256
  break inf_leave;
12430
12257
  }
12431
- output.set(input.subarray(next, next + copy), put);
12432
- have -= copy;
12433
- next += copy;
12434
- left -= copy;
12435
- put += copy;
12436
- state.length -= copy;
12258
+ output.set(input.subarray(next, next + copy2), put);
12259
+ have -= copy2;
12260
+ next += copy2;
12261
+ left -= copy2;
12262
+ put += copy2;
12263
+ state.length -= copy2;
12437
12264
  break;
12438
12265
  }
12439
12266
  state.mode = TYPE;
@@ -12534,7 +12361,7 @@ var inflate$2 = (strm, flush) => {
12534
12361
  break;
12535
12362
  }
12536
12363
  len = state.lens[state.have - 1];
12537
- copy = 3 + (hold & 3);
12364
+ copy2 = 3 + (hold & 3);
12538
12365
  hold >>>= 2;
12539
12366
  bits -= 2;
12540
12367
  } else if (here_val === 17) {
@@ -12550,7 +12377,7 @@ var inflate$2 = (strm, flush) => {
12550
12377
  hold >>>= here_bits;
12551
12378
  bits -= here_bits;
12552
12379
  len = 0;
12553
- copy = 3 + (hold & 7);
12380
+ copy2 = 3 + (hold & 7);
12554
12381
  hold >>>= 3;
12555
12382
  bits -= 3;
12556
12383
  } else {
@@ -12566,16 +12393,16 @@ var inflate$2 = (strm, flush) => {
12566
12393
  hold >>>= here_bits;
12567
12394
  bits -= here_bits;
12568
12395
  len = 0;
12569
- copy = 11 + (hold & 127);
12396
+ copy2 = 11 + (hold & 127);
12570
12397
  hold >>>= 7;
12571
12398
  bits -= 7;
12572
12399
  }
12573
- if (state.have + copy > state.nlen + state.ndist) {
12400
+ if (state.have + copy2 > state.nlen + state.ndist) {
12574
12401
  strm.msg = "invalid bit length repeat";
12575
12402
  state.mode = BAD;
12576
12403
  break;
12577
12404
  }
12578
- while (copy--) {
12405
+ while (copy2--) {
12579
12406
  state.lens[state.have++] = len;
12580
12407
  }
12581
12408
  }
@@ -12794,39 +12621,39 @@ var inflate$2 = (strm, flush) => {
12794
12621
  if (left === 0) {
12795
12622
  break inf_leave;
12796
12623
  }
12797
- copy = _out - left;
12798
- if (state.offset > copy) {
12799
- copy = state.offset - copy;
12800
- if (copy > state.whave) {
12624
+ copy2 = _out - left;
12625
+ if (state.offset > copy2) {
12626
+ copy2 = state.offset - copy2;
12627
+ if (copy2 > state.whave) {
12801
12628
  if (state.sane) {
12802
12629
  strm.msg = "invalid distance too far back";
12803
12630
  state.mode = BAD;
12804
12631
  break;
12805
12632
  }
12806
12633
  }
12807
- if (copy > state.wnext) {
12808
- copy -= state.wnext;
12809
- from = state.wsize - copy;
12634
+ if (copy2 > state.wnext) {
12635
+ copy2 -= state.wnext;
12636
+ from = state.wsize - copy2;
12810
12637
  } else {
12811
- from = state.wnext - copy;
12638
+ from = state.wnext - copy2;
12812
12639
  }
12813
- if (copy > state.length) {
12814
- copy = state.length;
12640
+ if (copy2 > state.length) {
12641
+ copy2 = state.length;
12815
12642
  }
12816
12643
  from_source = state.window;
12817
12644
  } else {
12818
12645
  from_source = output;
12819
12646
  from = put - state.offset;
12820
- copy = state.length;
12647
+ copy2 = state.length;
12821
12648
  }
12822
- if (copy > left) {
12823
- copy = left;
12649
+ if (copy2 > left) {
12650
+ copy2 = left;
12824
12651
  }
12825
- left -= copy;
12826
- state.length -= copy;
12652
+ left -= copy2;
12653
+ state.length -= copy2;
12827
12654
  do {
12828
12655
  output[put++] = from_source[from++];
12829
- } while (--copy);
12656
+ } while (--copy2);
12830
12657
  if (state.length === 0) {
12831
12658
  state.mode = LEN;
12832
12659
  }
@@ -14717,12 +14544,12 @@ var Model = class _Model extends import_index2.default {
14717
14544
  });
14718
14545
  return value2;
14719
14546
  } catch {
14720
- const copy = { ...value2 };
14721
- Object.defineProperty(copy, SYM_EXPANDED_REF, {
14547
+ const copy2 = { ...value2 };
14548
+ Object.defineProperty(copy2, SYM_EXPANDED_REF, {
14722
14549
  enumerable: true,
14723
14550
  value: true
14724
14551
  });
14725
- return copy;
14552
+ return copy2;
14726
14553
  }
14727
14554
  }
14728
14555
  /**
@@ -16344,13 +16171,18 @@ import { createHmac as createHmac2, randomBytes as randomBytes3 } from "node:cry
16344
16171
  var ID = /^[a-f0-9]{64}$/;
16345
16172
  var ROLES = new Set(MESSAGE_ROLES);
16346
16173
  var ROUTING_FIELDS = ["pairing", "sender", "recipient", "project", "thread"];
16174
+ var MIME = /^[\w.+-]{1,64}\/[\w.+-]{1,64}$/;
16347
16175
  var TOOL_TEXT = 500;
16176
+ function validAttachment(value2) {
16177
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.id === "string" && ID.test(value2.id) && typeof value2.mime === "string" && MIME.test(value2.mime) && Number.isSafeInteger(value2.bytes) && value2.bytes >= 0 && (value2.name === void 0 || typeof value2.name === "string" && value2.name.length <= ATTACHMENT_NAME_CHARS);
16178
+ }
16348
16179
  function validTool(value2) {
16349
16180
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.name === "string" && value2.name.length <= TOOL_TEXT && (value2.summary === void 0 || typeof value2.summary === "string" && value2.summary.length <= TOOL_TEXT) && (value2.status === void 0 || value2.status === "ok" || value2.status === "error") && (value2.ms === void 0 || Number.isSafeInteger(value2.ms) && value2.ms >= 0) && (value2.diff === void 0 || typeof value2.diff === "boolean");
16350
16181
  }
16351
16182
  function validMessage(value2) {
16352
- return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.id === "string" && ID.test(value2.id) && ROLES.has(value2.role) && typeof value2.text === "string" && value2.text.length <= HISTORY_TEXT_CHARS && Number.isSafeInteger(value2.part) && Number.isSafeInteger(value2.parts) && value2.part >= 0 && value2.parts > value2.part && (value2.tool === void 0 || value2.part === 0 && validTool(value2.tool));
16183
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.id === "string" && ID.test(value2.id) && ROLES.has(value2.role) && typeof value2.text === "string" && value2.text.length <= HISTORY_TEXT_CHARS && Number.isSafeInteger(value2.part) && Number.isSafeInteger(value2.parts) && value2.part >= 0 && value2.parts > value2.part && (value2.tool === void 0 || value2.part === 0 && validTool(value2.tool)) && (value2.full === void 0 || value2.part === 0 && validAttachment(value2.full)) && (value2.attachments === void 0 || value2.part === 0 && Array.isArray(value2.attachments) && value2.attachments.length <= HISTORY_ATTACHMENTS && value2.attachments.every(validAttachment));
16353
16184
  }
16185
+ var preview = (text3) => text3.split("\n", OUTPUT_PREVIEW.lines).join("\n").slice(0, OUTPUT_PREVIEW.chars);
16354
16186
  function splitHistoryMessages(messages5, opaqueId2) {
16355
16187
  return messages5.flatMap((message) => {
16356
16188
  const text3 = [];
@@ -16369,7 +16201,9 @@ function splitHistoryMessages(messages5, opaqueId2) {
16369
16201
  text: part,
16370
16202
  part: index,
16371
16203
  parts: text3.length,
16372
- ...index === 0 && message.tool ? { tool: message.tool } : {}
16204
+ ...index === 0 && message.tool ? { tool: message.tool } : {},
16205
+ ...index === 0 && message.full ? { full: message.full } : {},
16206
+ ...index === 0 && message.attachments?.length ? { attachments: message.attachments } : {}
16373
16207
  }));
16374
16208
  });
16375
16209
  }
@@ -16410,6 +16244,37 @@ function sealHistory(key, routing, message, random) {
16410
16244
  // src/history.ts
16411
16245
  var HISTORY_PATH = "/dashup/history";
16412
16246
  var HISTORY_KEPT = 300;
16247
+ var opaque = (key, value2) => createHmac2("sha256", Buffer.from(key, "hex")).update(JSON.stringify(value2)).digest("hex");
16248
+ function historyMessages(key, project, thread, messages5) {
16249
+ const name = (attachment) => named(key, project, thread, attachment);
16250
+ return splitHistoryMessages(
16251
+ messages5.map((message) => {
16252
+ const { full, attachments: attachments2, ...rest } = kept(message);
16253
+ const files2 = attachments2?.map(name).filter(validAttachment).slice(0, HISTORY_ATTACHMENTS);
16254
+ return {
16255
+ ...rest,
16256
+ ...full ? { full: name(full) } : {},
16257
+ ...files2?.length ? { attachments: files2 } : {}
16258
+ };
16259
+ }),
16260
+ (id) => opaque(key, ["history-message", project, thread, id])
16261
+ );
16262
+ }
16263
+ var copy = (messages5) => new Map(
16264
+ messages5.slice(-HISTORY_PAGE_ITEMS).map((message) => [message.id, JSON.stringify(message)])
16265
+ );
16266
+ function changes(copy2, messages5) {
16267
+ const latest = messages5.slice(-HISTORY_PAGE_ITEMS);
16268
+ let known = latest.length;
16269
+ while (known > 0 && !copy2.has(latest[known - 1].id)) known--;
16270
+ if (!known && copy2.size) return [];
16271
+ return latest.filter((message, at) => {
16272
+ const json = JSON.stringify(message);
16273
+ if (copy2.get(message.id) === json || at < known && !copy2.has(message.id)) return false;
16274
+ copy2.set(message.id, json);
16275
+ return true;
16276
+ });
16277
+ }
16413
16278
  async function upload(config2, batch) {
16414
16279
  if (batch.records.length > HISTORY_BATCH_ITEMS || Buffer.byteLength(JSON.stringify(batch)) > HISTORY_BATCH_BYTES)
16415
16280
  throw new Error("The encrypted history upload exceeds the batch limit");
@@ -16424,15 +16289,10 @@ async function publishHistory(config2, pair, project, thread, messages5) {
16424
16289
  const local = config2.pairs[pair.id];
16425
16290
  const generation = ++local.sequence;
16426
16291
  saveConfig(config2);
16427
- const opaque = (value2) => createHmac2("sha256", Buffer.from(local.key, "hex")).update(JSON.stringify(value2)).digest("hex");
16428
- const named = (id) => opaque(["history-message", project, thread.id, id]);
16429
- const all = splitHistoryMessages(messages5, named);
16292
+ const all = historyMessages(local.key, project, thread.id, messages5);
16430
16293
  let start = Math.max(0, all.length - HISTORY_KEPT);
16431
16294
  while (start < all.length && (all[start].part ?? 0) > 0) start++;
16432
- const fragments = start ? [
16433
- ...splitHistoryMessages([EARLIER], named),
16434
- ...all.slice(start)
16435
- ] : all;
16295
+ const fragments = start ? [...historyMessages(local.key, project, thread.id, [EARLIER]), ...all.slice(start)] : all;
16436
16296
  const summary = { generation, total: fragments.length, updatedAt: thread.updatedAt };
16437
16297
  const batch = {
16438
16298
  pairing: pair.id,
@@ -16448,7 +16308,7 @@ async function publishHistory(config2, pair, project, thread, messages5) {
16448
16308
  const record = sealHistory(
16449
16309
  local.key,
16450
16310
  {
16451
- id: opaque(["history-record", project, thread.id, generation, position]),
16311
+ id: opaque(local.key, ["history-record", project, thread.id, generation, position]),
16452
16312
  pairing: pair.id,
16453
16313
  sender: pair.device,
16454
16314
  recipient: pair.mobile,
@@ -16475,18 +16335,63 @@ async function publishHistory(config2, pair, project, thread, messages5) {
16475
16335
  throw new Error("The encrypted history snapshot was not published");
16476
16336
  return summary;
16477
16337
  }
16338
+ var ATTACHMENTS_PATH = "/dashup/attachments";
16339
+ var PASTED = new RegExp(
16340
+ `data:([\\w.+-]+/[\\w.+-]+);base64,([A-Za-z0-9+/]{${OUTPUT_PREVIEW.chars},}={0,2})`,
16341
+ "g"
16342
+ );
16343
+ function kept(message) {
16344
+ const files2 = [...message.attachments ?? []];
16345
+ const text3 = message.role === "tool" ? message.text : message.text.replace(PASTED, (_, mime, data, at) => {
16346
+ files2.push(attached(`${message.id}@${at}`, mime, data));
16347
+ return "";
16348
+ });
16349
+ const start = message.role === "tool" && !message.tool?.diff ? preview(text3) : text3;
16350
+ const full = start.length < text3.length ? {
16351
+ id: `${message.id}:full`,
16352
+ mime: "text/plain",
16353
+ bytes: Buffer.byteLength(text3),
16354
+ read: () => Buffer.from(text3)
16355
+ } : void 0;
16356
+ return {
16357
+ ...message,
16358
+ text: start,
16359
+ ...full ? { full } : {},
16360
+ ...files2.length ? { attachments: files2 } : {}
16361
+ };
16362
+ }
16363
+ var named = (key, project, thread, attachment) => ({
16364
+ id: opaque(key, ["history-attachment", project, thread, attachment.id, attachment.bytes]),
16365
+ mime: attachment.mime,
16366
+ bytes: attachment.bytes,
16367
+ ...attachment.name ? { name: attachment.name } : {}
16368
+ });
16369
+ var readable = (attachment) => "read" in attachment;
16370
+ async function shareAttachment(config2, pair, project, thread, messages5, id) {
16371
+ if (!activePair(config2, pair)) throw new Error("Pairing is no longer available");
16372
+ const key = config2.pairs[pair.id].key;
16373
+ const found = messages5.flatMap((message) => {
16374
+ const { full, attachments: attachments2 = [] } = kept(message);
16375
+ return full ? [full, ...attachments2] : attachments2;
16376
+ }).find((attachment) => named(key, project, thread, attachment).id === id);
16377
+ if (!found || !readable(found)) throw new Error("This attachment is no longer on your computer");
16378
+ if (found.bytes > MAX_ATTACHMENT_BYTES)
16379
+ throw new Error("This attachment is too large to open on your phone");
16380
+ const sealed = sealAttachment(key, pair.id, id, found.read(), randomBytes3);
16381
+ await api(config2, `${ATTACHMENTS_PATH}/${pair.id}/${id}`, sealed);
16382
+ }
16478
16383
 
16479
16384
  // src/harness/live.ts
16480
- import { promisify as promisify6 } from "node:util";
16481
- import { execFile as execFile6 } from "node:child_process";
16385
+ import { promisify as promisify5 } from "node:util";
16386
+ import { execFile as execFile5 } from "node:child_process";
16482
16387
 
16483
16388
  // src/harness/codex.ts
16484
16389
  import WebSocket2 from "ws";
16485
16390
  import { homedir as homedir4 } from "node:os";
16486
16391
  import { connect } from "node:net";
16487
- import { promisify as promisify3 } from "node:util";
16392
+ import { promisify as promisify2 } from "node:util";
16488
16393
  import { randomUUID as randomUUID2 } from "node:crypto";
16489
- import { execFile as execFile3 } from "node:child_process";
16394
+ import { execFile as execFile2 } from "node:child_process";
16490
16395
  import { join as join4, isAbsolute as isAbsolute2 } from "node:path";
16491
16396
  import { stat, readdir, readFile } from "node:fs/promises";
16492
16397
  var REQUEST_MS = 4e3;
@@ -16510,7 +16415,7 @@ function approval(id, method, params) {
16510
16415
  const oversized = Buffer.byteLength(detail) > MAX_PREVIEW;
16511
16416
  const unavailable = !command2 || params.kind !== void 0 && params.kind !== "command" || typeof params.command !== "string" || !params.command.trim() || oversized;
16512
16417
  if (oversized) detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
16513
- if (unavailable) detail = [detail, SCREEN_PREVIEW].filter(Boolean).join("\n\n");
16418
+ if (unavailable) detail = [detail, NO_PREVIEW].filter(Boolean).join("\n\n");
16514
16419
  return {
16515
16420
  id,
16516
16421
  title: command2 ? "Allow command?" : "Allow file changes?",
@@ -16806,7 +16711,7 @@ var Codex = class {
16806
16711
  // Codex's own daemon runs the threads Dashup starts or resumes, so they outlive this connector.
16807
16712
  async start(cwd, text3, saved) {
16808
16713
  const path = process.env.DASHUP_CODEX_SOCKET || join4(codexHome(), ...SOCKET_PARTS);
16809
- await promisify3(execFile3)("codex", ["app-server", "daemon", "start"], {
16714
+ await promisify2(execFile2)("codex", ["app-server", "daemon", "start"], {
16810
16715
  timeout: 6e4,
16811
16716
  shell: process.platform === "win32"
16812
16717
  });
@@ -16849,8 +16754,8 @@ var Codex = class {
16849
16754
  // src/harness/headless.ts
16850
16755
  import { randomUUID as randomUUID3 } from "node:crypto";
16851
16756
  import { readFileSync as readFileSync3 } from "node:fs";
16852
- import { createInterface as createInterface3 } from "node:readline";
16853
- import { spawn as spawn3 } from "node:child_process";
16757
+ import { createInterface as createInterface2 } from "node:readline";
16758
+ import { spawn as spawn2 } from "node:child_process";
16854
16759
  var CHOICES = [
16855
16760
  { id: "accept", label: "Allow once" },
16856
16761
  { id: "decline", label: "Reject" }
@@ -16973,7 +16878,7 @@ var Session = class {
16973
16878
  this.changed();
16974
16879
  return Promise.resolve();
16975
16880
  }
16976
- const turn = this.turn = spawn3(
16881
+ const turn = this.turn = spawn2(
16977
16882
  "claude",
16978
16883
  [
16979
16884
  "-p",
@@ -16996,7 +16901,7 @@ var Session = class {
16996
16901
  errors = (errors + String(chunk)).slice(-2e3);
16997
16902
  });
16998
16903
  const started = new Promise((resolve3, reject) => {
16999
- createInterface3({ input: turn.stdout }).on("line", (line) => {
16904
+ createInterface2({ input: turn.stdout }).on("line", (line) => {
17000
16905
  if (this.receive(line)) resolve3();
17001
16906
  });
17002
16907
  const end = (reason) => {
@@ -17028,7 +16933,7 @@ var Session = class {
17028
16933
  else if (message.type === "assistant") {
17029
16934
  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");
17030
16935
  if (!text3) return false;
17031
- this.messages.push({ id: String(message.message?.id ?? randomUUID3()), role: "assistant", text: text3 });
16936
+ this.messages.push({ id: String(message.uuid ?? randomUUID3()), role: "assistant", text: text3 });
17032
16937
  } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
17033
16938
  const id = randomUUID3();
17034
16939
  this.approvals.set(id, {
@@ -17250,9 +17155,9 @@ var Claude = class {
17250
17155
  import Socket3 from "ws";
17251
17156
  import { homedir as homedir5 } from "node:os";
17252
17157
  import { connect as connect3 } from "node:net";
17253
- import { promisify as promisify4 } from "node:util";
17254
- import { fileURLToPath as fileURLToPath2 } from "node:url";
17255
- import { execFile as execFile4 } from "node:child_process";
17158
+ import { promisify as promisify3 } from "node:util";
17159
+ import { fileURLToPath } from "node:url";
17160
+ import { execFile as execFile3 } from "node:child_process";
17256
17161
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
17257
17162
  import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17258
17163
  import { join as join6, dirname as dirname4, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
@@ -17265,7 +17170,7 @@ var MAX_METADATA = 64 * 1024;
17265
17170
  var MAX_HOSTS = 16;
17266
17171
  var MAX_SESSIONS3 = 100;
17267
17172
  var ROOT_CHANNEL = "ahp-root://";
17268
- var run2 = promisify4(execFile4);
17173
+ var run = promisify3(execFile3);
17269
17174
  var CONFIRMATION_CHOICES = [
17270
17175
  { id: "approve", label: "Allow once", kind: "approve" },
17271
17176
  { id: "deny", label: "Reject", kind: "deny" }
@@ -17285,7 +17190,7 @@ async function privateFile(path, limit) {
17285
17190
  const encodedPath = Buffer.from(path).toString("base64");
17286
17191
  const script = `$p=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedPath}')); $a=Get-Acl -LiteralPath $p -ErrorAction Stop; $sid=[Security.Principal.SecurityIdentifier]; $me=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value; $ok=$a.GetOwner($sid).Value -eq $me; foreach($r in $a.Access){if($r.AccessControlType -eq 'Allow' -and $r.IdentityReference.Translate($sid).Value -notin @($me,'S-1-5-18','S-1-5-32-544')){$ok=$false}}; $ok`;
17287
17192
  try {
17288
- const { stdout } = await run2(
17193
+ const { stdout } = await run(
17289
17194
  "powershell.exe",
17290
17195
  [
17291
17196
  "-NoProfile",
@@ -17354,7 +17259,7 @@ function directory(state) {
17354
17259
  const uri = state.workingDirectories?.[0];
17355
17260
  if (typeof uri !== "string" || !uri.startsWith("file:")) return;
17356
17261
  try {
17357
- return fileURLToPath2(uri);
17262
+ return fileURLToPath(uri);
17358
17263
  } catch {
17359
17264
  return;
17360
17265
  }
@@ -17375,7 +17280,7 @@ function confirmation(toolCall) {
17375
17280
  const paths = [before, after].filter((uri) => typeof uri === "string");
17376
17281
  const names = [...new Set(paths)].map((uri) => {
17377
17282
  try {
17378
- return fileURLToPath2(uri);
17283
+ return fileURLToPath(uri);
17379
17284
  } catch {
17380
17285
  return uri;
17381
17286
  }
@@ -17402,8 +17307,7 @@ function confirmation(toolCall) {
17402
17307
  })) : [];
17403
17308
  if (unavailable) choices = choices.filter((choice) => choice.kind === "deny");
17404
17309
  if (oversized) detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
17405
- if (unavailable || !choices.length)
17406
- detail = [detail, SCREEN_PREVIEW].filter(Boolean).join("\n\n");
17310
+ if (unavailable || !choices.length) detail = [detail, NO_PREVIEW].filter(Boolean).join("\n\n");
17407
17311
  return { detail, choices };
17408
17312
  }
17409
17313
  function messages3(chat) {
@@ -17628,12 +17532,12 @@ var Host = class {
17628
17532
  for (const pending of Array.isArray(session.inputNeeded) ? session.inputNeeded : []) {
17629
17533
  if (pending.kind !== "toolConfirmation" || pending.chat !== item.resource || pending.toolCall?.status !== "pending-confirmation")
17630
17534
  continue;
17631
- const preview = confirmation(pending.toolCall);
17535
+ const preview2 = confirmation(pending.toolCall);
17632
17536
  approvals.push({
17633
17537
  id: pending.id,
17634
17538
  title: text2(pending.toolCall.confirmationTitle) || pending.toolCall.displayName || "Native agent approval",
17635
- detail: preview.detail,
17636
- choices: preview.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17539
+ detail: preview2.detail,
17540
+ choices: preview2.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17637
17541
  });
17638
17542
  }
17639
17543
  const id = `vscode-ahp:${createHash3("sha256").update(this.entry.root).digest("hex").slice(0, 16)}:${item.resource}`;
@@ -17747,11 +17651,11 @@ var VSCode = class {
17747
17651
 
17748
17652
  // src/harness/opencode.ts
17749
17653
  import { join as join7 } from "node:path";
17750
- import { promisify as promisify5 } from "node:util";
17654
+ import { promisify as promisify4 } from "node:util";
17751
17655
  import { createHash as createHash4 } from "node:crypto";
17752
- import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
17656
+ import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
17753
17657
  import { stat as stat3, readlink, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17754
- var run3 = promisify5(execFile5);
17658
+ var run2 = promisify4(execFile4);
17755
17659
  var REQUEST_MS4 = 4e3;
17756
17660
  var MAX_BYTES3 = 8 * 1024 * 1024;
17757
17661
  var MAX_SESSIONS4 = 100;
@@ -17831,7 +17735,7 @@ async function endpoints() {
17831
17735
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17832
17736
  if (process.platform === "linux") return linuxEndpoints();
17833
17737
  if (process.platform === "darwin") {
17834
- const { stdout } = await run3(
17738
+ const { stdout } = await run2(
17835
17739
  "lsof",
17836
17740
  ["-nP", "-a", "-c", "opencode", "-iTCP", "-sTCP:LISTEN", "-Fn"],
17837
17741
  { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
@@ -17848,7 +17752,7 @@ async function endpoints() {
17848
17752
  }
17849
17753
  if (process.platform === "win32") {
17850
17754
  const script = "$ids = @(Get-Process opencode -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id); Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $ids -contains $_.OwningProcess } | Select-Object -ExpandProperty LocalPort";
17851
- const { stdout } = await run3(
17755
+ const { stdout } = await run2(
17852
17756
  "powershell.exe",
17853
17757
  ["-NoProfile", "-NonInteractive", "-Command", script],
17854
17758
  { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
@@ -17929,7 +17833,7 @@ ${always.join("\n")}` : ""
17929
17833
  }
17930
17834
  if (unavailable) detail += `
17931
17835
 
17932
- ${SCREEN_PREVIEW}`;
17836
+ ${NO_PREVIEW}`;
17933
17837
  return {
17934
17838
  id: permission.id,
17935
17839
  title: `Allow ${String(permission.permission ?? "tool")}?`,
@@ -18130,7 +18034,7 @@ var Server = class {
18130
18034
  if (!permissionApproval(permission, history2, cwd).choices.some(
18131
18035
  (value2) => value2.id === choice
18132
18036
  ))
18133
- throw new Error(SCREEN_PREVIEW);
18037
+ throw new Error(NO_PREVIEW);
18134
18038
  }
18135
18039
  await this.request(`/permission/${encodeURIComponent(approvalId)}/reply`, cwd, {
18136
18040
  reply: choice
@@ -18217,7 +18121,7 @@ var Server = class {
18217
18121
  }
18218
18122
  };
18219
18123
  function serve(cwd) {
18220
- const child = spawn4("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18124
+ const child = spawn3("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18221
18125
  cwd,
18222
18126
  stdio: ["ignore", "pipe", "ignore"],
18223
18127
  shell: process.platform === "win32"
@@ -18342,7 +18246,7 @@ var LiveHarnesses = class {
18342
18246
  async agents() {
18343
18247
  const harnesses = ["claude", "codex", "opencode"];
18344
18248
  const runs = await Promise.all(
18345
- harnesses.map((harness) => promisify6(execFile6)(harness, ["--version"], {
18249
+ harnesses.map((harness) => promisify5(execFile5)(harness, ["--version"], {
18346
18250
  timeout: 1e4,
18347
18251
  shell: process.platform === "win32"
18348
18252
  }).then(
@@ -18395,10 +18299,10 @@ var LiveHarnesses = class {
18395
18299
 
18396
18300
  // src/harness/queue.ts
18397
18301
  import { randomUUID as randomUUID5 } from "node:crypto";
18398
- import { spawn as spawn5 } from "node:child_process";
18302
+ import { spawn as spawn4 } from "node:child_process";
18399
18303
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18400
18304
  import { join as join8, dirname as dirname5, basename as basename3 } from "node:path";
18401
- import { statSync as statSync2, existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18305
+ import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18402
18306
  var CODEX_VERSION = "0.160.1";
18403
18307
  var REQUEST_MS5 = 1e4;
18404
18308
  var MAX_BYTES4 = 1024 * 1024;
@@ -18419,7 +18323,7 @@ function packageVersion(executable) {
18419
18323
  let directory2 = dirname5(executable);
18420
18324
  for (let depth = 0; depth < 8; depth++, directory2 = dirname5(directory2)) {
18421
18325
  const path = join8(directory2, "package.json");
18422
- if (!existsSync5(path)) continue;
18326
+ if (!existsSync4(path)) continue;
18423
18327
  const value2 = JSON.parse(readFileSync4(path, "utf8"));
18424
18328
  if (value2.name?.startsWith("@openai/codex-")) return value2.version;
18425
18329
  }
@@ -18520,7 +18424,7 @@ function validateEvidence(owner) {
18520
18424
  if (realpathSync2.native(owner.home) !== owner.home || process.getuid && statSync2(owner.home).uid !== process.getuid() || !sameFile(identity(owner.queue.path), owner.queue) || !sameFile(identity(owner.rollout.path), owner.rollout))
18521
18425
  throw new Error("The original native history storage changed");
18522
18426
  const path = join8(owner.home, "thread_history_1.sqlite");
18523
- if (existsSync5(path) && identity(path).path !== path)
18427
+ if (existsSync4(path) && identity(path).path !== path)
18524
18428
  throw new Error("Native history is outside the original home");
18525
18429
  }
18526
18430
  function nativeReceipt(owner, clientId) {
@@ -18537,7 +18441,7 @@ function nativeReceipt(owner, clientId) {
18537
18441
  queue.close();
18538
18442
  }
18539
18443
  const path = join8(owner.home, "thread_history_1.sqlite");
18540
- if (!existsSync5(path)) return { queueId };
18444
+ if (!existsSync4(path)) return { queueId };
18541
18445
  const history = new DatabaseSync3(path, { readOnly: true });
18542
18446
  try {
18543
18447
  const rows = history.prepare(
@@ -18592,7 +18496,7 @@ var Writer = class {
18592
18496
  tail = "";
18593
18497
  pending = /* @__PURE__ */ new Map();
18594
18498
  constructor(owner) {
18595
- this.process = spawn5(owner.executable, ["app-server", "--listen", "stdio://"], {
18499
+ this.process = spawn4(owner.executable, ["app-server", "--listen", "stdio://"], {
18596
18500
  cwd: owner.cwd,
18597
18501
  env: { ...process.env, CODEX_HOME: owner.home },
18598
18502
  stdio: ["pipe", "pipe", "ignore"]
@@ -18751,6 +18655,8 @@ var NativeQueue = class {
18751
18655
  // src/connector.ts
18752
18656
  var EPHEMERAL_TTL_MS = 6e4;
18753
18657
  var HOME_RECENT = 20;
18658
+ var FOLLOW_MS = 5 * 6e4;
18659
+ var RESCAN_MS = 1e3;
18754
18660
  var Command = z2.object({
18755
18661
  type: z2.literal("command"),
18756
18662
  requestId: z2.string().min(1),
@@ -18763,8 +18669,7 @@ var Command = z2.object({
18763
18669
  z2.object({ kind: z2.literal("thread.queue"), text: z2.string().min(1).max(1e5) }).strict(),
18764
18670
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18765
18671
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
18766
- z2.object({ kind: z2.literal("screen.start"), sessionId: z2.string().min(1).max(128) }).strict(),
18767
- z2.object({ kind: z2.literal("screen.stop"), sessionId: z2.string().min(1).max(128) }).strict(),
18672
+ z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
18768
18673
  z2.object({
18769
18674
  kind: z2.literal("session.start"),
18770
18675
  harness: z2.enum(["claude", "codex", "opencode"]),
@@ -18790,31 +18695,13 @@ var Connector = class {
18790
18695
  autoConnect: false
18791
18696
  });
18792
18697
  provideClient(() => this.client);
18793
- this.viewer = new Viewer(
18794
- config2,
18795
- (pair) => this.isActive(pair.id),
18796
- (pair, content) => this.send(pair, content)
18797
- );
18798
- this.discovery = new Discovery(
18799
- config2.device.id,
18800
- {
18801
- read: true,
18802
- send: false,
18803
- queue: false,
18804
- stop: false,
18805
- approve: false,
18806
- screen: config2.screen && Viewer.available(),
18807
- control: config2.control && controlAvailable()
18808
- },
18809
- config2.localSecret
18810
- );
18698
+ this.discovery = new Discovery(config2.device.id, config2.localSecret);
18811
18699
  this.pairings = createLiveQuery(() => this.client.bind(Pairing).where({ device: config2.device.id }).whereNull("revokedAt").clearLimit());
18812
18700
  this.relays = createLiveQuery(() => this.client.bind(Relay).where({ recipient: config2.device.id }).orderBy("createdAt", "desc").orderBy("id", "desc").clearLimit());
18813
18701
  }
18814
18702
  config;
18815
18703
  client;
18816
18704
  discovery;
18817
- viewer;
18818
18705
  pairings;
18819
18706
  relays;
18820
18707
  nativeQueue;
@@ -18825,8 +18712,17 @@ var Connector = class {
18825
18712
  retry;
18826
18713
  stopWatching = () => {
18827
18714
  };
18828
- rescan = () => void this.queueChange().catch(() => {
18829
- });
18715
+ rescanning;
18716
+ stopped = false;
18717
+ rescan = () => {
18718
+ if (this.rescanning) return;
18719
+ this.rescanning = setTimeout(() => {
18720
+ this.rescanning = void 0;
18721
+ void this.queueChange().catch(() => {
18722
+ });
18723
+ }, RESCAN_MS);
18724
+ this.rescanning.unref();
18725
+ };
18830
18726
  releases = [];
18831
18727
  announced = /* @__PURE__ */ new Set();
18832
18728
  listed = /* @__PURE__ */ new Map();
@@ -18838,6 +18734,8 @@ var Connector = class {
18838
18734
  snapshots = /* @__PURE__ */ new Set();
18839
18735
  nativeSnapshots = /* @__PURE__ */ new Map();
18840
18736
  queueReceipts = /* @__PURE__ */ new Map();
18737
+ // The conversations phones are reading, by pairing and thread.
18738
+ follows = /* @__PURE__ */ new Map();
18841
18739
  // The content key behind each pairing code on show, until a phone pairs with it.
18842
18740
  invitations = /* @__PURE__ */ new Map();
18843
18741
  async start() {
@@ -18854,8 +18752,6 @@ var Connector = class {
18854
18752
  this.releases.push(this.harnesses.watch(this.rescan));
18855
18753
  this.releases.push(
18856
18754
  this.pairings.retain(() => {
18857
- if (this.pairings.snapshot().status !== "ready") this.viewer.close();
18858
- else this.viewer.revoke(new Set(this.activePairs().map((pair) => pair.id)));
18859
18755
  this.work = this.work.then(async () => {
18860
18756
  if (this.pairings.snapshot().status !== "ready") return;
18861
18757
  for (const pair of this.activePairs()) {
@@ -18935,7 +18831,6 @@ var Connector = class {
18935
18831
  this.client.send("dashup:presence", { device: this.config.device.id });
18936
18832
  });
18937
18833
  this.client.on("error", () => this.report(new Error("Connection interrupted. Dashup will reconnect.")));
18938
- this.client.on("disconnected", () => this.viewer.close());
18939
18834
  await this.client.reconnect();
18940
18835
  process.stdout.write(
18941
18836
  `Dashup is connected. ${this.discovery.projects.size} existing projects found.
@@ -18949,29 +18844,16 @@ var Connector = class {
18949
18844
  return invite;
18950
18845
  }
18951
18846
  stop() {
18847
+ this.stopped = true;
18952
18848
  this.stopWatching();
18953
18849
  for (const release of this.releases.splice(0)) release();
18954
18850
  clearTimeout(this.retry);
18851
+ clearTimeout(this.rescanning);
18955
18852
  this.client.dispose();
18956
- this.viewer.close();
18957
18853
  this.nativeQueue.stop();
18958
18854
  this.harnesses.stop();
18959
18855
  for (const agent of this.agents.values()) agent.socket.destroy();
18960
18856
  }
18961
- async permissions(screen, control) {
18962
- this.config.screen = screen;
18963
- this.config.control = screen && control;
18964
- this.discovery.capabilities = {
18965
- ...this.discovery.capabilities,
18966
- screen: screen && Viewer.available(),
18967
- control: this.config.control && controlAvailable()
18968
- };
18969
- const sessions = screen ? [] : [...this.viewer.sessions];
18970
- if (!screen) this.viewer.close();
18971
- saveConfig(this.config);
18972
- for (const [id, current] of sessions) await this.viewer.sendClose(id, current);
18973
- await this.queueChange();
18974
- }
18975
18857
  activePairs() {
18976
18858
  return this.pairings.snapshot().items;
18977
18859
  }
@@ -19000,7 +18882,7 @@ var Connector = class {
19000
18882
  );
19001
18883
  if (envelope.ciphertext.length > MAX_ENVELOPE_BYTES * 2)
19002
18884
  throw new Error(
19003
- "This update exceeds the encrypted message limit. Open Screen to view the full conversation."
18885
+ "This update exceeds the encrypted message limit. View the full conversation on your computer."
19004
18886
  );
19005
18887
  this.config.outbox.push(envelope);
19006
18888
  saveConfig(this.config);
@@ -19013,7 +18895,7 @@ var Connector = class {
19013
18895
  saveConfig(this.config);
19014
18896
  }
19015
18897
  flush() {
19016
- const run5 = this.drain.catch(() => {
18898
+ const run4 = this.drain.catch(() => {
19017
18899
  }).then(async () => {
19018
18900
  this.discardLegacySnapshots();
19019
18901
  while (this.config.outbox.length && this.client.isConnected) {
@@ -19023,8 +18905,8 @@ var Connector = class {
19023
18905
  saveConfig(this.config);
19024
18906
  }
19025
18907
  });
19026
- this.drain = run5;
19027
- return run5.catch((error) => {
18908
+ this.drain = run4;
18909
+ return run4.catch((error) => {
19028
18910
  this.report(error);
19029
18911
  clearTimeout(this.retry);
19030
18912
  this.retry = setTimeout(() => {
@@ -19065,7 +18947,6 @@ var Connector = class {
19065
18947
  return {
19066
18948
  type: "projects",
19067
18949
  version,
19068
- screen: this.discovery.capabilities.screen,
19069
18950
  agents: this.startable,
19070
18951
  folders: this.discovery.folders(),
19071
18952
  projects: [...this.listedProjects.values()],
@@ -19099,15 +18980,16 @@ var Connector = class {
19099
18980
  // transcript costs one rescan rather than one each.
19100
18981
  queueChange() {
19101
18982
  if (this.pendingChange) return this.pendingChange;
19102
- const run5 = this.work.then(() => {
18983
+ const run4 = this.work.then(() => {
19103
18984
  this.pendingChange = void 0;
19104
18985
  return this.changed();
19105
18986
  });
19106
- this.pendingChange = run5;
19107
- this.work = run5.catch((error) => this.report(error));
19108
- return run5;
18987
+ this.pendingChange = run4;
18988
+ this.work = run4.catch((error) => this.report(error));
18989
+ return run4;
19109
18990
  }
19110
18991
  async changed() {
18992
+ if (this.stopped) return;
19111
18993
  const previous = new Map(
19112
18994
  [...this.discovery.projects.values()].map((project) => [project.id, project])
19113
18995
  );
@@ -19142,9 +19024,19 @@ var Connector = class {
19142
19024
  const projects = [...this.discovery.projects.values()];
19143
19025
  this.stopWatching();
19144
19026
  this.stopWatching = this.discovery.watch(this.rescan);
19027
+ await this.stream();
19145
19028
  for (const project of projects) {
19146
- if (JSON.stringify(previous.get(project.id)) === JSON.stringify(project)) continue;
19147
- for (const pair of this.activePairs()) await this.send(pair, { type: "project", project });
19029
+ const before = previous.get(project.id);
19030
+ const sent = new Map(before?.threads.map((thread) => [thread.id, JSON.stringify(thread)]));
19031
+ const changed = project.threads.filter(
19032
+ (thread) => sent.get(thread.id) !== JSON.stringify(thread)
19033
+ );
19034
+ const whole = !before || project.threads.filter((thread) => sent.has(thread.id)).length < sent.size || JSON.stringify({ ...before, threads: [] }) !== JSON.stringify({ ...project, threads: [] });
19035
+ for (const pair of this.activePairs())
19036
+ if (whole) await this.send(pair, { type: "project", project });
19037
+ else
19038
+ for (const thread of changed)
19039
+ if (!current.has(thread.id)) await this.thread(pair, project, thread);
19148
19040
  }
19149
19041
  for (const { project, thread, session } of live) {
19150
19042
  const snapshot = this.threadState(project, thread);
@@ -19167,6 +19059,40 @@ var Connector = class {
19167
19059
  for (const pair of this.activePairs()) await this.listProjects(pair);
19168
19060
  await this.publishQueueReceipts();
19169
19061
  }
19062
+ // A phone reading a conversation gets its messages as they change, before anything that says the
19063
+ // conversation changed, so it never asks for a copy it already has.
19064
+ async stream() {
19065
+ for (const [key, follow] of this.follows) {
19066
+ const thread = this.discovery.projects.get(follow.project)?.threads.find((item) => item.id === follow.thread);
19067
+ let messages5;
19068
+ try {
19069
+ if (thread && follow.until > Date.now() && activePair(this.config, follow.pair))
19070
+ messages5 = changes(
19071
+ follow.copy,
19072
+ this.discovery.read(thread, this.attached(thread)?.messages)
19073
+ );
19074
+ } catch {
19075
+ }
19076
+ if (!thread || !messages5) {
19077
+ this.follows.delete(key);
19078
+ continue;
19079
+ }
19080
+ if (!messages5.length && thread.updatedAt === follow.updatedAt) continue;
19081
+ follow.updatedAt = thread.updatedAt;
19082
+ await this.send(follow.pair, {
19083
+ type: "thread-tail",
19084
+ projectId: follow.project,
19085
+ threadId: follow.thread,
19086
+ updatedAt: follow.updatedAt,
19087
+ messages: historyMessages(
19088
+ this.config.pairs[follow.pair.id].key,
19089
+ follow.project,
19090
+ follow.thread,
19091
+ messages5
19092
+ )
19093
+ });
19094
+ }
19095
+ }
19170
19096
  // A saved session nothing else has open continues headless when its agent runs here. One a
19171
19097
  // native process owns takes queued messages instead.
19172
19098
  refreshResumes() {
@@ -19218,38 +19144,23 @@ var Connector = class {
19218
19144
  }
19219
19145
  async receive(envelope) {
19220
19146
  const pair = this.activePairs().find((item) => item.id === envelope.pairing);
19221
- if (!pair || !activePair(this.config, pair) || envelope.sender !== pair.mobile || envelope.recipient !== pair.device || !["command", "signal"].includes(envelope.channel))
19147
+ if (!pair || !activePair(this.config, pair) || envelope.sender !== pair.mobile || envelope.recipient !== pair.device || envelope.channel !== "command")
19222
19148
  return;
19223
19149
  const key = this.config.pairs[pair.id];
19224
19150
  if (envelope.sequence <= key.received) return;
19225
19151
  const content = open(key.key, envelope);
19226
- if (content.type !== "command" && content.type !== "signal" || content.projectId !== envelope.project)
19152
+ if (content.type !== "command" || content.projectId !== envelope.project || content.requestId !== envelope.id)
19227
19153
  return;
19228
- if (content.type === "command" && content.requestId !== envelope.id) return;
19229
- if (content.type === "command") Command.parse(content);
19154
+ Command.parse(content);
19230
19155
  key.received = envelope.sequence;
19231
- if (content.type === "command")
19232
- this.config.unconfirmed[content.requestId] = {
19233
- pairing: pair.id,
19234
- project: content.projectId
19235
- };
19156
+ this.config.unconfirmed[content.requestId] = { pairing: pair.id, project: content.projectId };
19236
19157
  saveConfig(this.config);
19237
- if (content.type === "signal") {
19238
- this.viewer.signal(pair, content.projectId, content.sessionId, content.signal);
19239
- return;
19240
- }
19241
- if (content.action.kind === "screen.start") {
19242
- void this.execute(pair, content, envelope).catch((error) => this.report(error));
19243
- return;
19244
- }
19245
19158
  await this.execute(pair, content, envelope);
19246
19159
  }
19247
19160
  async execute(pair, content, envelope) {
19248
19161
  let queue = void 0;
19249
19162
  try {
19250
- if (["screen.start", "approval.respond", "thread.stop", "session.start"].includes(
19251
- content.action.kind
19252
- ) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19163
+ if (["approval.respond", "thread.stop", "session.start"].includes(content.action.kind) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19253
19164
  throw new Error("Command expired. Try again while this computer is online.");
19254
19165
  const outcome = await this.command(pair, content);
19255
19166
  delete this.config.unconfirmed[content.requestId];
@@ -19402,12 +19313,32 @@ var Connector = class {
19402
19313
  if (!thread) throw new Error("Thread not found");
19403
19314
  const messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
19404
19315
  const history = await publishHistory(this.config, pair, project.id, thread, messages5);
19316
+ this.follows.set(`${pair.id}:${thread.id}`, {
19317
+ pair,
19318
+ project: project.id,
19319
+ thread: thread.id,
19320
+ until: Date.now() + FOLLOW_MS,
19321
+ copy: copy(messages5),
19322
+ updatedAt: thread.updatedAt
19323
+ });
19405
19324
  return this.thread(pair, project, thread, history);
19406
19325
  }
19326
+ case "attachment.read": {
19327
+ if (!thread) throw new Error("Thread not found");
19328
+ const messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
19329
+ return shareAttachment(
19330
+ this.config,
19331
+ pair,
19332
+ project.id,
19333
+ thread.id,
19334
+ messages5,
19335
+ command2.action.id
19336
+ );
19337
+ }
19407
19338
  case "thread.send": {
19408
19339
  if (!thread || !thread.capabilities.send)
19409
19340
  throw new Error(
19410
- "This session has no live reply connection. Open Screen to use the existing agent."
19341
+ "This session has no live reply connection. Use the existing agent on your computer."
19411
19342
  );
19412
19343
  const location2 = this.discovery.locations.get(thread.id);
19413
19344
  if (location2?.live?.send) return location2.live.send(command2.requestId, command2.action.text);
@@ -19428,7 +19359,7 @@ var Connector = class {
19428
19359
  const agent = thread && this.attached(thread);
19429
19360
  if (!agent || agent.socket.destroyed)
19430
19361
  throw new Error(
19431
- "This session has no live connection that can stop it. Open Screen to stop the agent."
19362
+ "This session has no live connection that can stop it. Stop the agent on your computer."
19432
19363
  );
19433
19364
  return commandAcp(agent, command2.requestId, { kind: "cancel" });
19434
19365
  }
@@ -19462,7 +19393,8 @@ var Connector = class {
19462
19393
  return native.approve(command2.requestId, approval4.id, action.choice);
19463
19394
  }
19464
19395
  const agent = this.attached(thread);
19465
- if (!agent) throw new Error("Approval requires an attached ACP session. Use Screen.");
19396
+ if (!agent)
19397
+ throw new Error("Approval requires an attached ACP session. Answer it on your computer.");
19466
19398
  const [request2, approval3] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
19467
19399
  if (!approval3?.choices.some((choice) => choice.id === action.choice))
19468
19400
  throw new Error("Approval is no longer available");
@@ -19472,12 +19404,6 @@ var Connector = class {
19472
19404
  choice: action.choice
19473
19405
  });
19474
19406
  }
19475
- case "screen.start":
19476
- if (!this.config.screen) throw new Error("Enable screen access on this computer first");
19477
- return this.viewer.start(pair, project.id, command2.action.sessionId);
19478
- case "screen.stop":
19479
- this.viewer.stop(pair, command2.action.sessionId);
19480
- return;
19481
19407
  }
19482
19408
  }
19483
19409
  // Starts an agent in a project, or in a new folder inside one this computer offers. Commands run
@@ -19518,24 +19444,24 @@ var Connector = class {
19518
19444
  };
19519
19445
 
19520
19446
  // src/install.ts
19521
- import { existsSync as existsSync6 } from "node:fs";
19447
+ import { existsSync as existsSync5 } from "node:fs";
19522
19448
  import { homedir as homedir6, userInfo } from "node:os";
19523
- import { promisify as promisify7 } from "node:util";
19524
- import { fileURLToPath as fileURLToPath3 } from "node:url";
19525
- import { execFile as execFile7, spawn as spawn6 } from "node:child_process";
19449
+ import { promisify as promisify6 } from "node:util";
19450
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
19451
+ import { execFile as execFile6, spawn as spawn5 } from "node:child_process";
19526
19452
  import { cp, mkdir as mkdir2, rm, writeFile } from "node:fs/promises";
19527
19453
  import { dirname as dirname6, join as join10, relative, sep } from "node:path";
19528
- var run4 = promisify7(execFile7);
19454
+ var run3 = promisify6(execFile6);
19529
19455
  var xml = (value2) => value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
19530
19456
  var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
19531
- var systemctl = (...args2) => run4("systemctl", ["--user", ...args2], {
19457
+ var systemctl = (...args2) => run3("systemctl", ["--user", ...args2], {
19532
19458
  env: { ...process.env, XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${userInfo().uid}` }
19533
19459
  });
19534
19460
  var serviceFile = () => process.platform === "darwin" ? join10(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join10(
19535
19461
  process.env.APPDATA ?? join10(homedir6(), "AppData/Roaming"),
19536
19462
  "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
19537
19463
  ) : join10(homedir6(), ".config/systemd/user/dashup.service");
19538
- var startupInstalled = () => existsSync6(serviceFile());
19464
+ var startupInstalled = () => existsSync5(serviceFile());
19539
19465
  function service(platform, node, entry, config2) {
19540
19466
  const command2 = [node, ...NODE_FLAGS, entry];
19541
19467
  if (platform === "darwin")
@@ -19563,7 +19489,7 @@ WantedBy=default.target
19563
19489
  `;
19564
19490
  }
19565
19491
  async function durableEntry() {
19566
- const entry = fileURLToPath3(import.meta.url);
19492
+ const entry = fileURLToPath2(import.meta.url);
19567
19493
  if (!entry.endsWith(`${sep}dist${sep}index.js`))
19568
19494
  throw new Error(
19569
19495
  "Build the connector before installing startup: pnpm --filter @dashup/connector build"
@@ -19586,13 +19512,13 @@ async function installStartup() {
19586
19512
  service(process.platform, process.execPath, await durableEntry(), configPath())
19587
19513
  );
19588
19514
  if (process.platform === "darwin") {
19589
- await run4("launchctl", ["unload", path]).catch(() => {
19515
+ await run3("launchctl", ["unload", path]).catch(() => {
19590
19516
  });
19591
- await run4("launchctl", ["load", path]);
19517
+ await run3("launchctl", ["load", path]);
19592
19518
  } else if (process.platform === "win32") {
19593
- spawn6("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19519
+ spawn5("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19594
19520
  } else {
19595
- const note = process.env.DISPLAY || process.env.WAYLAND_DISPLAY ? void 0 : await run4("loginctl", ["enable-linger"]).then(
19521
+ const note = process.env.DISPLAY || process.env.WAYLAND_DISPLAY ? void 0 : await run3("loginctl", ["enable-linger"]).then(
19596
19522
  () => void 0,
19597
19523
  () => `It stops when you log out. To keep it running, run sudo loginctl enable-linger ${userInfo().username}.`
19598
19524
  );
@@ -19604,7 +19530,7 @@ async function installStartup() {
19604
19530
  }
19605
19531
  async function uninstallStartup() {
19606
19532
  const path = serviceFile();
19607
- if (process.platform === "darwin") await run4("launchctl", ["unload", path]).catch(() => {
19533
+ if (process.platform === "darwin") await run3("launchctl", ["unload", path]).catch(() => {
19608
19534
  });
19609
19535
  else if (process.platform !== "win32")
19610
19536
  await systemctl("disable", "dashup.service").catch(() => {
@@ -19709,22 +19635,6 @@ async function main() {
19709
19635
  if (!response.ok) throw new Error("Could not stop Dashup");
19710
19636
  return;
19711
19637
  }
19712
- if (command === "permissions") {
19713
- config.screen = args.includes("--screen");
19714
- config.control = config.screen && args.includes("--control");
19715
- const remote = await running("/permissions", {
19716
- method: "POST",
19717
- body: JSON.stringify({ screen: config.screen, control: config.control }),
19718
- signal: AbortSignal.timeout(5e3)
19719
- }).catch(refused);
19720
- if (remote && !remote.ok) throw new Error("Could not change connector permissions");
19721
- if (!remote) saveConfig({ ...loadConfig(), screen: config.screen, control: config.control });
19722
- process.stdout.write(
19723
- `Screen ${config.screen ? "enabled" : "disabled"}, control ${config.control ? "enabled" : "disabled"}.
19724
- `
19725
- );
19726
- return;
19727
- }
19728
19638
  if (command === "acp") {
19729
19639
  const native = args[1] === "--" ? args.slice(2) : args.slice(1);
19730
19640
  const [executable, ...agentArgs] = native;
@@ -19733,18 +19643,7 @@ async function main() {
19733
19643
  return proxyAcp(config, executable, agentArgs);
19734
19644
  }
19735
19645
  if (command === "doctor" || command === "projects") {
19736
- const support = { screen: Viewer.available(), control: controlAvailable() };
19737
- const discovery = new Discovery(
19738
- config.device?.id ?? "local",
19739
- {
19740
- read: true,
19741
- send: false,
19742
- approve: false,
19743
- screen: config.screen && support.screen,
19744
- control: config.control && support.control
19745
- },
19746
- config.localSecret
19747
- );
19646
+ const discovery = new Discovery(config.device?.id ?? "local", config.localSecret);
19748
19647
  discovery.scan();
19749
19648
  const live = new LiveHarnesses();
19750
19649
  try {
@@ -19759,7 +19658,6 @@ async function main() {
19759
19658
  node: process.version,
19760
19659
  paired: Boolean(config.secret),
19761
19660
  platform: process.platform,
19762
- screen: support,
19763
19661
  projectCount: projects.length,
19764
19662
  harnesses: Object.fromEntries(
19765
19663
  Object.keys(HARNESSES).filter((harness) => harness !== "other").map((harness) => [
@@ -19793,7 +19691,7 @@ async function main() {
19793
19691
  }
19794
19692
  if (!["pair", "watch"].includes(command)) {
19795
19693
  process.stdout.write(
19796
- "Dashup: pair [--account <email>] | watch | projects | alert --project <id> | doctor | permissions --screen --control | stop | acp -- <original-agent-command> | install | uninstall\n"
19694
+ "Dashup: pair [--account <email>] | watch | projects | alert --project <id> | doctor | stop | acp -- <original-agent-command> | install | uninstall\n"
19797
19695
  );
19798
19696
  return;
19799
19697
  }
@@ -19866,7 +19764,7 @@ async function main() {
19866
19764
  response.end(JSON.stringify(connector.invite()));
19867
19765
  return;
19868
19766
  }
19869
- if (!["/attention", "/permissions"].includes(request2.url ?? "") || request2.method !== "POST") {
19767
+ if (request2.url !== "/attention" || request2.method !== "POST") {
19870
19768
  response.writeHead(404).end();
19871
19769
  return;
19872
19770
  }
@@ -19875,12 +19773,6 @@ async function main() {
19875
19773
  value2 += String(chunk);
19876
19774
  if (value2.length > 2e4) throw new Error("Alert too large");
19877
19775
  }
19878
- if (request2.url === "/permissions") {
19879
- const input2 = z3.object({ screen: z3.boolean(), control: z3.boolean() }).strict().parse(JSON.parse(value2));
19880
- await connector.permissions(input2.screen, input2.control);
19881
- response.end("{}");
19882
- return;
19883
- }
19884
19776
  const input = Attention.parse(JSON.parse(value2));
19885
19777
  await connector.attention(
19886
19778
  input.projectId,