@dashup/connector 0.1.9 → 0.1.11

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 (2) hide show
  1. package/dist/index.js +277 -65
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2728,11 +2728,16 @@ var PROTOCOL_VERSION = 1;
2728
2728
  var MAX_ENVELOPE_BYTES = 8 * 1024 * 1024;
2729
2729
  var HISTORY_PAGE_ITEMS = 30;
2730
2730
  var HISTORY_TEXT_CHARS = 4e3;
2731
+ var HISTORY_TOOL_CHARS = 500;
2731
2732
  var HISTORY_PAGE_BYTES = 256 * 1024;
2732
2733
  var HISTORY_BATCH_ITEMS = 50;
2733
2734
  var HISTORY_BATCH_BYTES = HISTORY_PAGE_BYTES;
2734
2735
  var HISTORY_RECORD_BYTES = 32 * 1024;
2735
2736
  var HISTORY_RECORD_CIPHERTEXT_BYTES = HISTORY_RECORD_BYTES + 16;
2737
+ var OUTPUT_PREVIEW = { lines: 12, chars: 2e3 };
2738
+ var HISTORY_ATTACHMENTS = 16;
2739
+ var MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
2740
+ var ATTACHMENT_NAME_CHARS = 100;
2736
2741
  var MESSAGE_ROLES = ["user", "assistant", "system", "tool"];
2737
2742
  var HARNESSES = {
2738
2743
  claude: "Claude Code",
@@ -2875,6 +2880,21 @@ function complexOverlapBytes(input, output) {
2875
2880
  if (overlapBytes(input, output) && input.byteOffset < output.byteOffset)
2876
2881
  throw new Error("complex overlap of input and output is not supported");
2877
2882
  }
2883
+ function concatBytes(...arrays) {
2884
+ let sum = 0;
2885
+ for (let i = 0; i < arrays.length; i++) {
2886
+ const a = arrays[i];
2887
+ abytes(a);
2888
+ sum += a.length;
2889
+ }
2890
+ const res = new Uint8Array(sum);
2891
+ for (let i = 0, pad = 0; i < arrays.length; i++) {
2892
+ const a = arrays[i];
2893
+ res.set(a, pad);
2894
+ pad += a.length;
2895
+ }
2896
+ return res;
2897
+ }
2878
2898
  function checkOpts(defaults, opts) {
2879
2899
  aobject(defaults, "defaults");
2880
2900
  aobject(opts, "opts");
@@ -3589,6 +3609,12 @@ function decryptContent(key, associatedData, sealed) {
3589
3609
  ).decrypt(ciphertextBytes(sealed.ciphertext));
3590
3610
  return JSON.parse(bytesToUtf8(cleartext));
3591
3611
  }
3612
+ var attachmentData = (pairing, id) => utf8ToBytes(JSON.stringify([PROTOCOL_VERSION, "attachment", pairing, id]));
3613
+ function sealAttachment(key, pairing, id, bytes, random) {
3614
+ const nonce = random(NONCE_BYTES);
3615
+ const cipher = xchacha20poly1305(hexToBytes(key), nonce, attachmentData(pairing, id));
3616
+ return concatBytes(nonce, cipher.encrypt(bytes));
3617
+ }
3592
3618
  var PAIRING_CODE = new RegExp(
3593
3619
  `^DASHUP:${PROTOCOL_VERSION}:([0-9A-F]{64}):([0-9A-F]{16}):([0-9A-F]{64}):(.+)$`
3594
3620
  );
@@ -3641,10 +3667,14 @@ function saveConfig(config2) {
3641
3667
  }
3642
3668
  }
3643
3669
  async function api(config2, path, body2, method = body2 ? "POST" : "GET") {
3670
+ const bytes = body2 instanceof Uint8Array;
3644
3671
  const response = await fetch(`${config2.api}/v1${path}`, {
3645
3672
  method,
3646
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${config2.secret}` },
3647
- body: body2 ? JSON.stringify(body2) : void 0
3673
+ headers: {
3674
+ "Content-Type": bytes ? "application/octet-stream" : "application/json",
3675
+ Authorization: `Bearer ${config2.secret}`
3676
+ },
3677
+ body: bytes ? new Uint8Array(body2) : body2 ? JSON.stringify(body2) : void 0
3648
3678
  });
3649
3679
  const value2 = await response.json();
3650
3680
  if (!response.ok || !value2.success)
@@ -4016,7 +4046,7 @@ import * as ui from "@clack/prompts";
4016
4046
  import { z } from "zod";
4017
4047
 
4018
4048
  // package.json
4019
- var version = "0.1.9";
4049
+ var version = "0.1.11";
4020
4050
 
4021
4051
  // src/pair.ts
4022
4052
  var Invitation = z.object({
@@ -4173,6 +4203,13 @@ function agentHarness(name) {
4173
4203
  if (/opencode/i.test(name)) return "opencode";
4174
4204
  return void 0;
4175
4205
  }
4206
+ var attached = (id, mime, data, name) => ({
4207
+ id,
4208
+ mime,
4209
+ bytes: Buffer.byteLength(data, "base64"),
4210
+ ...name ? { name: name.slice(-ATTACHMENT_NAME_CHARS) } : {},
4211
+ read: () => Buffer.from(data, "base64")
4212
+ });
4176
4213
  var MAX_PREVIEW = 64 * 1024;
4177
4214
  var NO_PREVIEW = "Review the full request on your computer before approving.";
4178
4215
 
@@ -4332,15 +4369,28 @@ function text(value2) {
4332
4369
  const part = object(value2);
4333
4370
  return part ? typeof part.text === "string" ? part.text : text(part.content) : "";
4334
4371
  }
4372
+ var DATA_URL = /^data:([^;,]+);base64,/;
4373
+ function attachments(id, content) {
4374
+ return list(content).flatMap((part, index) => {
4375
+ const key = `${id}:${index}`;
4376
+ const name = [part?.title, part?.filename].find((value2) => typeof value2 === "string");
4377
+ const source = object(part?.source);
4378
+ if (source?.type === "base64" && typeof source.data === "string")
4379
+ return [attached(key, String(source.media_type), source.data, name)];
4380
+ const url2 = String(part?.image_url ?? part?.url ?? "");
4381
+ const data = DATA_URL.exec(url2);
4382
+ return data ? [attached(key, data[1], url2.slice(data[0].length), name)] : [];
4383
+ });
4384
+ }
4335
4385
  function prose(id, role, content) {
4336
4386
  const value2 = text(
4337
4387
  Array.isArray(content) ? content.filter(
4338
4388
  (part) => typeof part === "string" || ["text", "input_text", "output_text"].includes(part?.type)
4339
4389
  ) : content
4340
4390
  );
4341
- return value2 ? [{ id, role, text: value2 }] : [];
4391
+ const files2 = attachments(id, content);
4392
+ return value2 || files2.length ? [{ id, role, text: value2, ...files2.length ? { attachments: files2 } : {} }] : [];
4342
4393
  }
4343
- var SUMMARY_LIMIT = 200;
4344
4394
  var SUMMARY_FIELDS = [
4345
4395
  "command",
4346
4396
  "cmd",
@@ -4357,13 +4407,17 @@ var SUMMARY_FIELDS = [
4357
4407
  var PATCH_FILE = /^\*\*\* (Add|Update|Delete) File: (.+)$/gm;
4358
4408
  function describe(input) {
4359
4409
  const fields = object(input);
4360
- const named = SUMMARY_FIELDS.map((key) => fields?.[key]).find(
4410
+ const named2 = SUMMARY_FIELDS.map((key) => fields?.[key]).find(
4361
4411
  (value3) => typeof value3 === "string" && value3.trim() !== ""
4362
4412
  );
4363
- 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) : "");
4364
4414
  const paths = [...value2.matchAll(PATCH_FILE)].map((match) => match[2]).join(", ");
4365
- const first = value2.split("\n").find((line) => line.trim());
4366
- return (paths || first?.trim())?.slice(0, SUMMARY_LIMIT);
4415
+ return (paths || value2.trim()).slice(0, HISTORY_TOOL_CHARS) || void 0;
4416
+ }
4417
+ function said(input, summary) {
4418
+ const value2 = object(input)?.description;
4419
+ const line = typeof value2 === "string" ? value2.trim().split("\n")[0] : "";
4420
+ return line && line !== summary ? line.slice(0, HISTORY_TOOL_CHARS) : void 0;
4367
4421
  }
4368
4422
  function elapsed(start, end) {
4369
4423
  const ms = instant(end) - instant(start);
@@ -4378,20 +4432,26 @@ function added(path, content) {
4378
4432
  ...lines.map((line) => `+${line}`)
4379
4433
  ].join("\n");
4380
4434
  }
4381
- function step(id, name, input, result) {
4435
+ var binary = (diff) => diff.slice(0, 8e3).includes("\0");
4436
+ function step(id, name, input, result, started) {
4382
4437
  const summary = describe(input);
4383
- const diff = result?.status === "ok" ? result.diff : void 0;
4438
+ const label = said(input, summary);
4439
+ const at = instant(started);
4440
+ const diff = result?.status === "ok" && result.diff && !binary(result.diff) ? result.diff : void 0;
4384
4441
  return {
4385
4442
  id,
4386
4443
  role: "tool",
4387
4444
  text: diff || result?.text || "",
4388
4445
  tool: {
4389
- name: typeof name === "string" && name ? name.slice(0, SUMMARY_LIMIT) : "Tool",
4446
+ name: typeof name === "string" && name ? name.slice(0, HISTORY_TOOL_CHARS) : "Tool",
4447
+ ...label ? { label } : {},
4390
4448
  ...summary ? { summary } : {},
4391
4449
  ...result?.status ? { status: result.status } : {},
4392
4450
  ...result?.status && result.ms !== void 0 ? { ms: result.ms } : {},
4451
+ ...!result?.status && Number.isFinite(at) ? { at: new Date(at).toISOString() } : {},
4393
4452
  ...diff ? { diff: true } : {}
4394
- }
4453
+ },
4454
+ ...result?.attachments?.length ? { attachments: result.attachments } : {}
4395
4455
  };
4396
4456
  }
4397
4457
  var claudeHome = () => process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
@@ -4447,17 +4507,20 @@ function claudeConversation(conversation, rows) {
4447
4507
  ...list(content).flatMap((part, position) => {
4448
4508
  if (part?.type !== "tool_use") return [];
4449
4509
  const result = results.get(part.id);
4510
+ const call = String(part.id ?? `${id}:${position}`);
4450
4511
  return [
4451
4512
  step(
4452
- String(part.id ?? `${id}:${position}`),
4513
+ call,
4453
4514
  part.name,
4454
4515
  part.input,
4455
4516
  result && {
4456
4517
  text: text(result.part.content),
4457
4518
  status: result.part.is_error ? "error" : "ok",
4458
4519
  ms: elapsed(row.timestamp, result.row.timestamp),
4459
- diff: claudeChange(object(result.row.toolUseResult))
4460
- }
4520
+ diff: claudeChange(object(result.row.toolUseResult)),
4521
+ attachments: attachments(call, result.part.content)
4522
+ },
4523
+ row.timestamp
4461
4524
  )
4462
4525
  ];
4463
4526
  })
@@ -4471,7 +4534,7 @@ function claude(root, scan) {
4471
4534
  const latest = [...rows].reverse();
4472
4535
  const owner = rows.find((row) => typeof row.sessionId === "string" && canonical(row.cwd));
4473
4536
  if (!owner) return void 0;
4474
- const named = latest.find((row) => row.type === "custom-title" || row.type === "ai-title");
4537
+ const named2 = latest.find((row) => row.type === "custom-title" || row.type === "ai-title");
4475
4538
  const first = rows.find(
4476
4539
  (row) => row.type === "user" && !row.isMeta && !row.isSidechain && !injected(row.message?.content)
4477
4540
  );
@@ -4479,7 +4542,7 @@ function claude(root, scan) {
4479
4542
  id: String(owner.sessionId),
4480
4543
  cwd: canonical(owner.cwd),
4481
4544
  title: title(
4482
- named?.customTitle ?? named?.aiTitle ?? text(first?.message?.content),
4545
+ named2?.customTitle ?? named2?.aiTitle ?? text(first?.message?.content),
4483
4546
  "Claude Code thread"
4484
4547
  ),
4485
4548
  updatedAt: date(latest.find(claudeMessage)?.timestamp, stamp.mtimeMs)
@@ -4535,16 +4598,19 @@ function codexMessages(path) {
4535
4598
  const input = item.type === "function_call" ? parse(String(item.arguments)) ?? item.arguments : item.input;
4536
4599
  const output = outputs.get(item.call_id);
4537
4600
  const patch = item.name === "apply_patch" && typeof input === "string" ? input : "";
4601
+ const call = String(item.call_id ?? id);
4538
4602
  return [
4539
4603
  step(
4540
- String(item.call_id ?? id),
4604
+ call,
4541
4605
  item.name,
4542
4606
  input,
4543
4607
  output && {
4544
4608
  ...codexResult(text(output.payload.output)),
4545
4609
  ms: elapsed(row.timestamp, output.timestamp),
4546
- diff: patch && applyPatch(patch)
4547
- }
4610
+ diff: patch && applyPatch(patch),
4611
+ attachments: attachments(call, output.payload.output)
4612
+ },
4613
+ row.timestamp
4548
4614
  )
4549
4615
  ];
4550
4616
  });
@@ -4625,6 +4691,10 @@ function openCodeParts(id, role, parts2) {
4625
4691
  const key = String(part.id ?? `${id}:${index}`);
4626
4692
  if (part.type === "text" && typeof part.text === "string")
4627
4693
  return [{ id: key, role, text: part.text }];
4694
+ if (part.type === "file") {
4695
+ const files2 = attachments(key, [part]);
4696
+ return files2.length ? [{ id: key, role, text: "", attachments: files2 }] : [];
4697
+ }
4628
4698
  if (part.type !== "tool") return [];
4629
4699
  const state = object(part.state) ?? {};
4630
4700
  const status2 = state.status === "completed" ? "ok" : state.status === "error" ? "error" : void 0;
@@ -4637,8 +4707,10 @@ function openCodeParts(id, role, parts2) {
4637
4707
  text: text(state.output ?? state.error),
4638
4708
  status: status2,
4639
4709
  ms: elapsed(state.time?.start, state.time?.end),
4640
- diff: openCodeChange(state)
4641
- }
4710
+ diff: openCodeChange(state),
4711
+ attachments: attachments(key, state.attachments)
4712
+ },
4713
+ state.time?.start
4642
4714
  )
4643
4715
  ];
4644
4716
  });
@@ -4999,6 +5071,7 @@ var Discovery = class {
4999
5071
  read: true,
5000
5072
  send: Boolean(session.send),
5001
5073
  stop: Boolean(session.stop),
5074
+ now: Boolean(session.now),
5002
5075
  approve: Boolean(session.approve)
5003
5076
  }
5004
5077
  });
@@ -5148,12 +5221,12 @@ var Discovery = class {
5148
5221
  }
5149
5222
  }
5150
5223
  }
5151
- read(thread, attached) {
5224
+ read(thread, attached2) {
5152
5225
  const location2 = this.locations.get(thread.id);
5153
5226
  if (!location2) throw new Error("Thread is no longer available");
5154
5227
  if (location2.history) return location2.history.read();
5155
5228
  if (location2.live) return location2.live.messages;
5156
- if (attached) return attached;
5229
+ if (attached2) return attached2;
5157
5230
  if (location2.editor === "vscode") {
5158
5231
  const state = chatSession(location2.path);
5159
5232
  return (state.requests ?? []).flatMap((request2, index) => [
@@ -16110,13 +16183,19 @@ import { createHmac as createHmac2, randomBytes as randomBytes3 } from "node:cry
16110
16183
  var ID = /^[a-f0-9]{64}$/;
16111
16184
  var ROLES = new Set(MESSAGE_ROLES);
16112
16185
  var ROUTING_FIELDS = ["pairing", "sender", "recipient", "project", "thread"];
16113
- var TOOL_TEXT = 500;
16186
+ var MIME = /^[\w.+-]{1,64}\/[\w.+-]{1,64}$/;
16187
+ function validAttachment(value2) {
16188
+ 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);
16189
+ }
16114
16190
  function validTool(value2) {
16115
- 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");
16191
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.name === "string" && value2.name.length <= HISTORY_TOOL_CHARS && [value2.label, value2.summary].every(
16192
+ (text3) => text3 === void 0 || typeof text3 === "string" && text3.length <= HISTORY_TOOL_CHARS
16193
+ ) && (value2.at === void 0 || typeof value2.at === "string" && !Number.isNaN(Date.parse(value2.at))) && (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");
16116
16194
  }
16117
16195
  function validMessage(value2) {
16118
- 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));
16196
+ 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));
16119
16197
  }
16198
+ var preview = (text3) => text3.split("\n", OUTPUT_PREVIEW.lines).join("\n").slice(0, OUTPUT_PREVIEW.chars);
16120
16199
  function splitHistoryMessages(messages5, opaqueId2) {
16121
16200
  return messages5.flatMap((message) => {
16122
16201
  const text3 = [];
@@ -16135,7 +16214,9 @@ function splitHistoryMessages(messages5, opaqueId2) {
16135
16214
  text: part,
16136
16215
  part: index,
16137
16216
  parts: text3.length,
16138
- ...index === 0 && message.tool ? { tool: message.tool } : {}
16217
+ ...index === 0 && message.tool ? { tool: message.tool } : {},
16218
+ ...index === 0 && message.full ? { full: message.full } : {},
16219
+ ...index === 0 && message.attachments?.length ? { attachments: message.attachments } : {}
16139
16220
  }));
16140
16221
  });
16141
16222
  }
@@ -16177,7 +16258,21 @@ function sealHistory(key, routing, message, random) {
16177
16258
  var HISTORY_PATH = "/dashup/history";
16178
16259
  var HISTORY_KEPT = 300;
16179
16260
  var opaque = (key, value2) => createHmac2("sha256", Buffer.from(key, "hex")).update(JSON.stringify(value2)).digest("hex");
16180
- var historyMessages = (key, project, thread, messages5) => splitHistoryMessages(messages5, (id) => opaque(key, ["history-message", project, thread, id]));
16261
+ function historyMessages(key, project, thread, messages5) {
16262
+ const name = (attachment) => named(key, project, thread, attachment);
16263
+ return splitHistoryMessages(
16264
+ messages5.map((message) => {
16265
+ const { full, attachments: attachments2, ...rest } = kept(message);
16266
+ const files2 = attachments2?.map(name).filter(validAttachment).slice(0, HISTORY_ATTACHMENTS);
16267
+ return {
16268
+ ...rest,
16269
+ ...full ? { full: name(full) } : {},
16270
+ ...files2?.length ? { attachments: files2 } : {}
16271
+ };
16272
+ }),
16273
+ (id) => opaque(key, ["history-message", project, thread, id])
16274
+ );
16275
+ }
16181
16276
  var copy = (messages5) => new Map(
16182
16277
  messages5.slice(-HISTORY_PAGE_ITEMS).map((message) => [message.id, JSON.stringify(message)])
16183
16278
  );
@@ -16253,6 +16348,51 @@ async function publishHistory(config2, pair, project, thread, messages5) {
16253
16348
  throw new Error("The encrypted history snapshot was not published");
16254
16349
  return summary;
16255
16350
  }
16351
+ var ATTACHMENTS_PATH = "/dashup/attachments";
16352
+ var PASTED = new RegExp(
16353
+ `data:([\\w.+-]+/[\\w.+-]+);base64,([A-Za-z0-9+/]{${OUTPUT_PREVIEW.chars},}={0,2})`,
16354
+ "g"
16355
+ );
16356
+ function kept(message) {
16357
+ const files2 = [...message.attachments ?? []];
16358
+ const text3 = message.role === "tool" ? message.text : message.text.replace(PASTED, (_, mime, data, at) => {
16359
+ files2.push(attached(`${message.id}@${at}`, mime, data));
16360
+ return "";
16361
+ });
16362
+ const start = message.role === "tool" && !message.tool?.diff ? preview(text3) : text3;
16363
+ const full = start.length < text3.length ? {
16364
+ id: `${message.id}:full`,
16365
+ mime: "text/plain",
16366
+ bytes: Buffer.byteLength(text3),
16367
+ read: () => Buffer.from(text3)
16368
+ } : void 0;
16369
+ return {
16370
+ ...message,
16371
+ text: start,
16372
+ ...full ? { full } : {},
16373
+ ...files2.length ? { attachments: files2 } : {}
16374
+ };
16375
+ }
16376
+ var named = (key, project, thread, attachment) => ({
16377
+ id: opaque(key, ["history-attachment", project, thread, attachment.id, attachment.bytes]),
16378
+ mime: attachment.mime,
16379
+ bytes: attachment.bytes,
16380
+ ...attachment.name ? { name: attachment.name } : {}
16381
+ });
16382
+ var readable = (attachment) => "read" in attachment;
16383
+ async function shareAttachment(config2, pair, project, thread, messages5, id) {
16384
+ if (!activePair(config2, pair)) throw new Error("Pairing is no longer available");
16385
+ const key = config2.pairs[pair.id].key;
16386
+ const found = messages5.flatMap((message) => {
16387
+ const { full, attachments: attachments2 = [] } = kept(message);
16388
+ return full ? [full, ...attachments2] : attachments2;
16389
+ }).find((attachment) => named(key, project, thread, attachment).id === id);
16390
+ if (!found || !readable(found)) throw new Error("This attachment is no longer on your computer");
16391
+ if (found.bytes > MAX_ATTACHMENT_BYTES)
16392
+ throw new Error("This attachment is too large to open on your phone");
16393
+ const sealed = sealAttachment(key, pair.id, id, found.read(), randomBytes3);
16394
+ await api(config2, `${ATTACHMENTS_PATH}/${pair.id}/${id}`, sealed);
16395
+ }
16256
16396
 
16257
16397
  // src/harness/live.ts
16258
16398
  import { promisify as promisify5 } from "node:util";
@@ -16367,6 +16507,8 @@ var Client = class {
16367
16507
  pending = /* @__PURE__ */ new Map();
16368
16508
  approvals = /* @__PURE__ */ new Map();
16369
16509
  requests = /* @__PURE__ */ new Map();
16510
+ // The reply each thread is writing, as far as it has streamed, and the item it belongs to.
16511
+ typing = /* @__PURE__ */ new Map();
16370
16512
  async connect() {
16371
16513
  if (this.socket?.readyState === WebSocket2.OPEN) return;
16372
16514
  if (this.connecting) return this.connecting;
@@ -16401,13 +16543,20 @@ var Client = class {
16401
16543
  return;
16402
16544
  }
16403
16545
  if (typeof message.method !== "string") return;
16546
+ const params = message.params;
16547
+ if (message.method === "item/agentMessage/delta" && typeof params?.threadId === "string" && typeof params.delta === "string") {
16548
+ const known = this.typing.get(params.threadId);
16549
+ this.typing.set(params.threadId, {
16550
+ item: params.itemId,
16551
+ text: (known && known.item === params.itemId ? known.text : "") + params.delta
16552
+ });
16553
+ }
16404
16554
  if (message.method === "serverRequest/resolved") {
16405
16555
  for (const [id, pending] of this.approvals)
16406
16556
  if (pending.requestId === message.params?.requestId) this.approvals.delete(id);
16407
16557
  }
16408
16558
  if ((message.method === "item/commandExecution/requestApproval" || message.method === "item/fileChange/requestApproval") && (typeof message.id === "string" || typeof message.id === "number") && typeof message.params?.threadId === "string") {
16409
16559
  const id = randomUUID2();
16410
- const params = message.params;
16411
16560
  this.approvals.set(id, {
16412
16561
  requestId: message.id,
16413
16562
  threadId: params.threadId,
@@ -16517,6 +16666,7 @@ var Client = class {
16517
16666
  updatedAt: new Date(Number(thread.updatedAt ?? 0) * 1e3).toISOString(),
16518
16667
  status: approvals.length ? "attention" : status(thread),
16519
16668
  messages: messages2(thread),
16669
+ typing: this.typing.get(id)?.text,
16520
16670
  approvals,
16521
16671
  // An interrupt for a finished turn is never answered, so the active turn is read first.
16522
16672
  stop: this.controls ? (requestId) => this.once(requestId, async () => {
@@ -16724,12 +16874,21 @@ var Session = class {
16724
16874
  turn;
16725
16875
  // Messages written to this turn's process that Claude Code has not finished answering.
16726
16876
  open = 0;
16877
+ // The text Claude Code is writing, as far as it has streamed.
16878
+ typing = "";
16727
16879
  approvals = /* @__PURE__ */ new Map();
16728
16880
  requests = /* @__PURE__ */ new Map();
16729
16881
  write(value2) {
16730
16882
  this.turn?.stdin.write(`${JSON.stringify(value2)}
16731
16883
  `);
16732
16884
  }
16885
+ interrupt() {
16886
+ this.write({
16887
+ type: "control_request",
16888
+ request_id: randomUUID3(),
16889
+ request: { subtype: "interrupt" }
16890
+ });
16891
+ }
16733
16892
  once(id, action) {
16734
16893
  const known = this.requests.get(id);
16735
16894
  if (known) return known;
@@ -16739,13 +16898,16 @@ var Session = class {
16739
16898
  this.requests.set(id, result);
16740
16899
  return result;
16741
16900
  }
16742
- // Resolves once Claude Code takes the message, or says why it could not start.
16743
- say(text3) {
16901
+ // Resolves once Claude Code takes the message, or says why it could not start. `now` stops the
16902
+ // turn under way, so Claude Code takes the message at once.
16903
+ say(text3, now = false) {
16744
16904
  this.messages.push({ id: randomUUID3(), role: "user", text: text3 });
16905
+ this.typing = "";
16745
16906
  this.status = "working";
16746
16907
  this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16747
16908
  const message = { type: "user", message: { role: "user", content: text3 } };
16748
16909
  if (this.turn) {
16910
+ if (now) this.interrupt();
16749
16911
  this.open++;
16750
16912
  this.write(message);
16751
16913
  this.changed();
@@ -16760,6 +16922,7 @@ var Session = class {
16760
16922
  "--output-format",
16761
16923
  "stream-json",
16762
16924
  "--verbose",
16925
+ "--include-partial-messages",
16763
16926
  "--permission-prompt-tool",
16764
16927
  "stdio",
16765
16928
  ...this.saved ? ["--resume", this.id] : ["--session-id", this.id]
@@ -16817,6 +16980,14 @@ var Session = class {
16817
16980
  this.status = "attention";
16818
16981
  } else if (message.type === "result") {
16819
16982
  if (--this.open <= 0) this.turn?.stdin.end();
16983
+ } else if (message.type === "stream_event" && !message.parent_tool_use_id) {
16984
+ const event = message.event;
16985
+ if (event?.type === "content_block_start") this.typing = "";
16986
+ else if (event?.type === "content_block_delta" && event.delta?.type === "text_delta")
16987
+ this.typing += String(event.delta.text ?? "");
16988
+ else return false;
16989
+ this.changed();
16990
+ return false;
16820
16991
  } else return false;
16821
16992
  this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16822
16993
  this.changed();
@@ -16831,15 +17002,13 @@ var Session = class {
16831
17002
  updatedAt: this.updatedAt,
16832
17003
  status: this.status,
16833
17004
  messages: [...this.messages],
17005
+ typing: this.typing,
16834
17006
  approvals: [...this.approvals.values()].map((pending) => pending.approval),
16835
- send: (requestId, text3) => this.once(requestId, () => this.say(text3)),
17007
+ send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17008
+ now: true,
16836
17009
  stop: this.turn ? (requestId) => this.once(requestId, async () => {
16837
17010
  if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
16838
- this.write({
16839
- type: "control_request",
16840
- request_id: randomUUID3(),
16841
- request: { subtype: "interrupt" }
16842
- });
17011
+ this.interrupt();
16843
17012
  }) : void 0,
16844
17013
  approve: this.approvals.size ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16845
17014
  const pending = this.approvals.get(approvalId);
@@ -16917,6 +17086,7 @@ import { join as join5 } from "node:path";
16917
17086
  import { connect as connect2 } from "node:net";
16918
17087
  import { watch as watch2 } from "node:fs";
16919
17088
  import { lstat, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
17089
+ var STOP = "Stop here, and wait for my next message.";
16920
17090
  var PEER_PROTOCOL = 1;
16921
17091
  var MAX_SESSIONS2 = 100;
16922
17092
  var REQUEST_MS2 = 4e3;
@@ -16946,7 +17116,7 @@ async function registrations() {
16946
17116
  );
16947
17117
  return found.flat();
16948
17118
  }
16949
- async function post(session, path, text3) {
17119
+ async function post(session, path, text3, priority) {
16950
17120
  const file = await lstat(path);
16951
17121
  if (!file.isSocket() || process.getuid && file.uid !== process.getuid())
16952
17122
  throw new Error("Claude Code inbox is not owned by this user");
@@ -16954,7 +17124,7 @@ async function post(session, path, text3) {
16954
17124
  const line = JSON.stringify({
16955
17125
  type: "user",
16956
17126
  session_id: session.sessionId,
16957
- priority: "next",
17127
+ priority,
16958
17128
  message: { role: "user", content: `<${ENVELOPE} from-name="Dashup">
16959
17129
  ${body2}
16960
17130
  </${ENVELOPE}>` }
@@ -16987,7 +17157,9 @@ var Claude = class {
16987
17157
  status: STATUS[session.status ?? ""] ?? "unknown",
16988
17158
  messages: [],
16989
17159
  approvals: [],
16990
- send: path ? (requestId, text3) => this.once(requestId, () => post(session, path, text3)) : void 0
17160
+ send: path ? (requestId, text3, now) => this.once(requestId, () => post(session, path, text3, now ? "now" : "next")) : void 0,
17161
+ stop: path ? (requestId) => this.once(requestId, () => post(session, path, STOP, "now")) : void 0,
17162
+ now: Boolean(path)
16991
17163
  };
16992
17164
  });
16993
17165
  }
@@ -17405,12 +17577,12 @@ var Host = class {
17405
17577
  for (const pending of Array.isArray(session.inputNeeded) ? session.inputNeeded : []) {
17406
17578
  if (pending.kind !== "toolConfirmation" || pending.chat !== item.resource || pending.toolCall?.status !== "pending-confirmation")
17407
17579
  continue;
17408
- const preview = confirmation(pending.toolCall);
17580
+ const preview2 = confirmation(pending.toolCall);
17409
17581
  approvals.push({
17410
17582
  id: pending.id,
17411
17583
  title: text2(pending.toolCall.confirmationTitle) || pending.toolCall.displayName || "Native agent approval",
17412
- detail: preview.detail,
17413
- choices: preview.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17584
+ detail: preview2.detail,
17585
+ choices: preview2.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17414
17586
  });
17415
17587
  }
17416
17588
  const id = `vscode-ahp:${createHash3("sha256").update(this.entry.root).digest("hex").slice(0, 16)}:${item.resource}`;
@@ -18528,7 +18700,7 @@ var NativeQueue = class {
18528
18700
  // src/connector.ts
18529
18701
  var EPHEMERAL_TTL_MS = 6e4;
18530
18702
  var HOME_RECENT = 20;
18531
- var FOLLOW_MS = 5 * 6e4;
18703
+ var FOLLOW_MS = 2 * 6e4;
18532
18704
  var RESCAN_MS = 1e3;
18533
18705
  var Command = z2.object({
18534
18706
  type: z2.literal("command"),
@@ -18538,10 +18710,16 @@ var Command = z2.object({
18538
18710
  action: z2.discriminatedUnion("kind", [
18539
18711
  z2.object({ kind: z2.literal("projects.refresh") }).strict(),
18540
18712
  z2.object({ kind: z2.literal("thread.read") }).strict(),
18541
- z2.object({ kind: z2.literal("thread.send"), text: z2.string().min(1).max(1e5) }).strict(),
18713
+ z2.object({ kind: z2.literal("thread.follow") }).strict(),
18714
+ z2.object({
18715
+ kind: z2.literal("thread.send"),
18716
+ text: z2.string().min(1).max(1e5),
18717
+ now: z2.boolean().optional()
18718
+ }).strict(),
18542
18719
  z2.object({ kind: z2.literal("thread.queue"), text: z2.string().min(1).max(1e5) }).strict(),
18543
18720
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18544
18721
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
18722
+ z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
18545
18723
  z2.object({
18546
18724
  kind: z2.literal("session.start"),
18547
18725
  harness: z2.enum(["claude", "codex", "opencode"]),
@@ -18931,6 +19109,27 @@ var Connector = class {
18931
19109
  for (const pair of this.activePairs()) await this.listProjects(pair);
18932
19110
  await this.publishQueueReceipts();
18933
19111
  }
19112
+ // Publishes a conversation for the phone, which then gets what changes in it.
19113
+ async read(pair, project, thread) {
19114
+ const messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
19115
+ const history = await publishHistory(this.config, pair, project.id, thread, messages5);
19116
+ this.follows.set(`${pair.id}:${thread.id}`, {
19117
+ pair,
19118
+ project: project.id,
19119
+ thread: thread.id,
19120
+ until: Date.now() + FOLLOW_MS,
19121
+ copy: copy(messages5),
19122
+ updatedAt: thread.updatedAt,
19123
+ typing: ""
19124
+ });
19125
+ return this.thread(pair, project, thread, history);
19126
+ }
19127
+ // The reply an agent this computer runs is still writing, until the conversation holds it.
19128
+ typing(thread, messages5) {
19129
+ const typing = this.discovery.locations.get(thread.id)?.live?.typing ?? "";
19130
+ const last = messages5.filter((message) => message.role === "assistant").at(-1);
19131
+ return typing && !last?.text.startsWith(typing) ? typing : "";
19132
+ }
18934
19133
  // A phone reading a conversation gets its messages as they change, before anything that says the
18935
19134
  // conversation changed, so it never asks for a copy it already has.
18936
19135
  async stream() {
@@ -18939,18 +19138,19 @@ var Connector = class {
18939
19138
  let messages5;
18940
19139
  try {
18941
19140
  if (thread && follow.until > Date.now() && activePair(this.config, follow.pair))
18942
- messages5 = changes(
18943
- follow.copy,
18944
- this.discovery.read(thread, this.attached(thread)?.messages)
18945
- );
19141
+ messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
18946
19142
  } catch {
18947
19143
  }
18948
19144
  if (!thread || !messages5) {
18949
19145
  this.follows.delete(key);
18950
19146
  continue;
18951
19147
  }
18952
- if (!messages5.length && thread.updatedAt === follow.updatedAt) continue;
19148
+ const changed = changes(follow.copy, messages5);
19149
+ const typing = this.typing(thread, messages5);
19150
+ if (!changed.length && thread.updatedAt === follow.updatedAt && typing === follow.typing)
19151
+ continue;
18953
19152
  follow.updatedAt = thread.updatedAt;
19153
+ follow.typing = typing;
18954
19154
  await this.send(follow.pair, {
18955
19155
  type: "thread-tail",
18956
19156
  projectId: follow.project,
@@ -18960,8 +19160,9 @@ var Connector = class {
18960
19160
  this.config.pairs[follow.pair.id].key,
18961
19161
  follow.project,
18962
19162
  follow.thread,
18963
- messages5
18964
- )
19163
+ changed
19164
+ ),
19165
+ ...typing ? { typing } : {}
18965
19166
  });
18966
19167
  }
18967
19168
  }
@@ -19036,6 +19237,7 @@ var Connector = class {
19036
19237
  throw new Error("Command expired. Try again while this computer is online.");
19037
19238
  const outcome = await this.command(pair, content);
19038
19239
  delete this.config.unconfirmed[content.requestId];
19240
+ if (content.action.kind === "thread.follow") return;
19039
19241
  if (outcome && "state" in outcome) {
19040
19242
  queue = outcome;
19041
19243
  await this.queueResult(pair, content.requestId, content.projectId, queue);
@@ -19049,6 +19251,7 @@ var Connector = class {
19049
19251
  );
19050
19252
  } catch (error) {
19051
19253
  delete this.config.unconfirmed[content.requestId];
19254
+ if (content.action.kind === "thread.follow") return;
19052
19255
  if (queue) {
19053
19256
  await this.queueResult(pair, content.requestId, content.projectId, queue);
19054
19257
  return;
@@ -19181,19 +19384,27 @@ var Connector = class {
19181
19384
  if (!project) throw new Error("Project is no longer open on this computer");
19182
19385
  const thread = project.threads.find((item) => item.id === command2.threadId);
19183
19386
  switch (command2.action.kind) {
19184
- case "thread.read": {
19387
+ case "thread.read":
19388
+ if (!thread) throw new Error("Thread not found");
19389
+ return this.read(pair, project, thread);
19390
+ case "thread.follow": {
19391
+ if (!thread) throw new Error("Thread not found");
19392
+ const follow = this.follows.get(`${pair.id}:${thread.id}`);
19393
+ if (!follow) return this.read(pair, project, thread);
19394
+ follow.until = Date.now() + FOLLOW_MS;
19395
+ return;
19396
+ }
19397
+ case "attachment.read": {
19185
19398
  if (!thread) throw new Error("Thread not found");
19186
19399
  const messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
19187
- const history = await publishHistory(this.config, pair, project.id, thread, messages5);
19188
- this.follows.set(`${pair.id}:${thread.id}`, {
19400
+ return shareAttachment(
19401
+ this.config,
19189
19402
  pair,
19190
- project: project.id,
19191
- thread: thread.id,
19192
- until: Date.now() + FOLLOW_MS,
19193
- copy: copy(messages5),
19194
- updatedAt: thread.updatedAt
19195
- });
19196
- return this.thread(pair, project, thread, history);
19403
+ project.id,
19404
+ thread.id,
19405
+ messages5,
19406
+ command2.action.id
19407
+ );
19197
19408
  }
19198
19409
  case "thread.send": {
19199
19410
  if (!thread || !thread.capabilities.send)
@@ -19201,7 +19412,8 @@ var Connector = class {
19201
19412
  "This session has no live reply connection. Use the existing agent on your computer."
19202
19413
  );
19203
19414
  const location2 = this.discovery.locations.get(thread.id);
19204
- if (location2?.live?.send) return location2.live.send(command2.requestId, command2.action.text);
19415
+ if (location2?.live?.send)
19416
+ return location2.live.send(command2.requestId, command2.action.text, command2.action.now);
19205
19417
  const agent = this.attached(thread);
19206
19418
  const saved = thread.agent;
19207
19419
  if (!agent && location2?.history && saved && saved.harness !== "other") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dashup": "dist/index.js"
@@ -30,10 +30,10 @@
30
30
  "tsx": "^4.22.4",
31
31
  "typescript": "^5.9.3",
32
32
  "@dashup/models": "0.1.0",
33
- "@dashup/protocol": "0.1.0",
34
33
  "@dashup/typescript-config": "0.0.0",
34
+ "@parcae/lynx": "0.1.0",
35
35
  "@parcae/sdk": "0.8.1",
36
- "@parcae/lynx": "0.1.0"
36
+ "@dashup/protocol": "0.1.0"
37
37
  },
38
38
  "scripts": {
39
39
  "dashup": "tsx src/index.ts",