conductor-remote 1.69.0 → 1.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -13,7 +13,7 @@
13
13
  <title>Conductor Remote</title>
14
14
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
15
15
  <script src="/self-heal.js"></script>
16
- <script type="module" crossorigin src="/assets/index-2nsPbh1M.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-CMOaVjNQ.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-CS-jxAPg.css">
18
18
  <link rel="manifest" href="/manifest.webmanifest"></head>
19
19
  <body>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"2b3de0c9ea5ae3535f7f5ea8b0ebdc1e"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CS-jxAPg.css",revision:null},{url:"assets/index-2nsPbh1M.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"9fd3d35917bdcc36eb68d82f29d8d01b"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CS-jxAPg.css",revision:null},{url:"assets/index-CMOaVjNQ.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
@@ -436,7 +436,7 @@ export function createTools(call) {
436
436
  },
437
437
  {
438
438
  name: 'split_chat',
439
- description: 'Move a tangent out of a chat: copy that chat into a fresh tab beside it, as a Conductor attachment, and ask the new agent your question there. Use it when a conversation has grown a second topic — a running agent steered mid-turn ends up holding three threads at once, which reads badly for everyone afterwards. The copy carries the prose and the reasoning, not the tool calls, so the new agent knows what was said and decided but not every file that was read. This DRIVES THE REAL UI twice (a new tab, then the send) and steals focus for a few seconds. To split the chat you are in, find its session_id with list_chats on your own workspace.',
439
+ description: 'Move a tangent out of a chat: copy that chat into a fresh tab beside it, as a Conductor attachment, and ask the new agent your question there. Use it when a conversation has grown a second topic — a running agent steered mid-turn ends up holding three threads at once, which reads badly for everyone afterwards. The copy carries the prose and the reasoning, not the tool calls, so the new agent knows what was said and decided but not every file that was read. It runs to the end of the chat unless through names an earlier message to stop at, which is how you branch from before the conversation turned. This DRIVES THE REAL UI twice (a new tab, then the send) and steals focus for a few seconds. To split the chat you are in, find its session_id with list_chats on your own workspace.',
440
440
  inputSchema: {
441
441
  type: 'object',
442
442
  properties: {
@@ -450,6 +450,10 @@ export function createTools(call) {
450
450
  include_tools: {
451
451
  type: 'boolean',
452
452
  description: 'Carry tool calls across as one line each (default false — mostly noise, and most of the bytes).'
453
+ },
454
+ through: {
455
+ type: 'string',
456
+ description: 'Stop the copy at this message, it included — a cursor from read_chat or search_chats. Default: the whole chat.'
453
457
  }
454
458
  },
455
459
  required: ['session_id', 'prompt']
@@ -457,13 +461,19 @@ export function createTools(call) {
457
461
  run: async (args) => {
458
462
  const sessionId = need(args, 'session_id');
459
463
  const prompt = need(args, 'prompt');
464
+ const through = str(args.through);
465
+ const throughRowid = through ? parseChatCursor(through) : null;
466
+ if (through && throughRowid === null) {
467
+ throw new Error('through must be a cursor returned by read_chat or search_chats');
468
+ }
460
469
  const split = await call(routes.splitChat.path(sessionId), {
461
470
  method: routes.splitChat.method,
462
471
  body: {
463
472
  prompt,
464
473
  workspaceId: str(args.workspace_id),
465
474
  includeThinking: args.include_thinking !== false,
466
- includeTools: args.include_tools === true
475
+ includeTools: args.include_tools === true,
476
+ throughRowid: throughRowid ?? undefined
467
477
  },
468
478
  timeoutMs: WRITE_TIMEOUT_MS
469
479
  });
@@ -474,7 +484,8 @@ export function createTools(call) {
474
484
  // like a complete one to whoever gets it next.
475
485
  const cut = [
476
486
  file.elided.tools ? plural(file.elided.tools, 'tool call') : '',
477
- file.elided.thinking ? plural(file.elided.thinking, 'thinking block') : ''
487
+ file.elided.thinking ? plural(file.elided.thinking, 'thinking block') : '',
488
+ file.elided.later ? plural(file.elided.later, 'entry after the cut', 'entries after the cut') : ''
478
489
  ].filter(Boolean);
479
490
  const lines = [
480
491
  `copied ${plural(file.kept, 'entry', 'entries')} (${Math.round(file.bytes / 1024)}kB) to ${file.path}${cut.length ? `, without ${cut.join(' or ')}` : ''}`
@@ -30,7 +30,7 @@ import { readSettings, writeSettings } from "./settings.js";
30
30
  import { withoutWindowEvidence } from "./shared.js";
31
31
  import { discardStagedAttachment, materializeStagedAttachments, stageAttachment, stagedAttachments } from "./staged-attachments.js";
32
32
  import { driftWarningLines, readExposeMode, tailscaleBin } from "./tailscale.js";
33
- import { renderTranscript } from "./transcript.js";
33
+ import { renderTranscript, transcriptThrough } from "./transcript.js";
34
34
  import { autoJoinHotspotMode, currentSsid, looksLikeHotspot, preferredNetworks } from "./wifi.js";
35
35
  import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, lockBlocked, newChat, pickActuator, retryWontHelp, screenLocked, sendNeverStarted, setAgentOptions, setRestartGuard, setWorkspaceStatus, stopTurn, UiBusyError, uiQueueDepth, WORKSPACE_STATUS_LABELS, withUiPriority } from "./writes.js";
36
36
  // Before anything that logs: from here on every console line is also kept in memory for
@@ -1494,7 +1494,7 @@ const server = http.createServer(async (req, res) => {
1494
1494
  });
1495
1495
  return json(req, res, answer.status, answer.body);
1496
1496
  }
1497
- // POST /api/sessions/:id/split { prompt?, includeThinking?, includeTools? }
1497
+ // POST /api/sessions/:id/split { prompt?, includeThinking?, includeTools?, throughRowid? }
1498
1498
  //
1499
1499
  // Conductor's own "Fork to new tab" resumes the agent's real session. This copies
1500
1500
  // the conversation instead, as a Conductor attachment, which is the cut that
@@ -1527,7 +1527,15 @@ const server = http.createServer(async (req, res) => {
1527
1527
  return json(req, res, 404, { error: 'chat not found in that workspace' });
1528
1528
  const format = { thinking: body.includeThinking !== false, tools: body.includeTools === true };
1529
1529
  const { entries } = reads.getMessages(sessionId);
1530
- const rendered = renderTranscript(entries, format);
1530
+ const through = body.throughRowid;
1531
+ if (through !== undefined && (!Number.isSafeInteger(through) || through < 1)) {
1532
+ return json(req, res, 400, { error: 'throughRowid must be a positive integer' });
1533
+ }
1534
+ const cut = through === undefined ? { entries, later: 0 } : transcriptThrough(entries, through);
1535
+ if (!cut)
1536
+ return json(req, res, 409, { error: 'that message is not in this chat' });
1537
+ const rendered = renderTranscript(cut.entries, format);
1538
+ const elided = { ...rendered.elided, later: cut.later };
1531
1539
  if (!rendered.kept)
1532
1540
  return json(req, res, 409, { error: 'that chat has nothing to copy yet' });
1533
1541
  // Conductor's own name for a copied transcript, so the chip reads the same as one
@@ -1536,18 +1544,24 @@ const server = http.createServer(async (req, res) => {
1536
1544
  const title = source.title?.trim() || 'chat';
1537
1545
  const carried = [`thinking ${format.thinking ? 'included' : 'omitted'}`];
1538
1546
  carried.push(`tool calls ${format.tools ? 'included' : 'omitted'}`);
1547
+ const stops = cut.later
1548
+ ? [
1549
+ `The copy stops partway through: ${cut.later} later ${cut.later === 1 ? 'entry is' : 'entries are'} not in it.`
1550
+ ]
1551
+ : [];
1539
1552
  const header = [
1540
1553
  `# Transcript of ${title}`,
1541
1554
  '',
1542
1555
  `${[ws.repo_name, ws.branch].filter(Boolean).join(' · ')}`,
1543
1556
  `Copied from the Conductor chat \`${sessionId}\` by conductor-remote. ${carried.join(', ')}.`,
1557
+ ...stops,
1544
1558
  '',
1545
1559
  ''
1546
1560
  ].join('\n');
1547
1561
  const attachment = writeAttachment(ws.worktree, `Transcript of ${title}.md`, header + rendered.text);
1548
1562
  const opened = await openChat(ws);
1549
1563
  if ('error' in opened) {
1550
- return json(req, res, 502, { ...opened.result, attachment: { ...attachment, ...rendered } });
1564
+ return json(req, res, 502, { ...opened.result, attachment: { ...attachment, ...rendered, elided } });
1551
1565
  }
1552
1566
  // The token is what Conductor turns into the attachment chip and supplies to the
1553
1567
  // receiving agent. Do not repeat `attachment.relPath` in prose: that renders a
@@ -1563,7 +1577,7 @@ const server = http.createServer(async (req, res) => {
1563
1577
  path: attachment.relPath,
1564
1578
  bytes: attachment.bytes,
1565
1579
  kept: rendered.kept,
1566
- elided: rendered.elided
1580
+ elided
1567
1581
  }
1568
1582
  });
1569
1583
  }
@@ -259,6 +259,25 @@ function toolLine(e) {
259
259
  function plural(n, one) {
260
260
  return `${n} ${one}${n === 1 ? '' : 's'}`;
261
261
  }
262
+ /**
263
+ * Cut a transcript at one message, that message included — a fork from an earlier point.
264
+ *
265
+ * The cut is made on the rowid rather than on a position, because that is the granularity
266
+ * every pointer into a chat already has: `read_chat`'s cursor is a rowid, and one source
267
+ * row yields several entries (the reasoning, the prose, the tool calls it made) that belong
268
+ * to the same message and have to cross together.
269
+ *
270
+ * Null means no message here carries that rowid — a cursor from a different chat, or one
271
+ * past the end of this one. Copying the whole chat in that case is the silent failure this
272
+ * exists to prevent: a transcript that stops at the wrong place reads exactly like one that
273
+ * stops where it was asked to.
274
+ */
275
+ export function transcriptThrough(entries, rowid) {
276
+ if (!entries.some(entry => entry.rowid === rowid))
277
+ return null;
278
+ const kept = entries.filter(entry => entry.rowid <= rowid);
279
+ return { entries: kept, later: entries.length - kept.length };
280
+ }
262
281
  /**
263
282
  * A chat as markdown, in Conductor's own transcript layout.
264
283
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.69.0",
3
+ "version": "1.70.0",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",