conductor-remote 1.87.0 → 1.88.1

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
@@ -25,7 +25,7 @@
25
25
  <title>Conductor Remote</title>
26
26
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
27
27
  <script src="/self-heal.js"></script>
28
- <script type="module" crossorigin src="/assets/index-DCz-Wt8W.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-DGV8ZFW2.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/assets/index-CDKjnPTR.css">
30
30
  <link rel="manifest" href="/manifest.webmanifest"></head>
31
31
  <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:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"ded020603e390e6e29a857f634a34742"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DCz-Wt8W.js",revision:null},{url:"assets/index-CDKjnPTR.css",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{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 c={};const l=e=>i(e,o),t={module:{uri:o},exports:c,require:l};s[o]=Promise.all(n.map(e=>t[e]||l(e))).then(e=>(r(...e),c))}}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:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"9269ec902c30d238ecf55b71e0143d6b"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DGV8ZFW2.js",revision:null},{url:"assets/index-CDKjnPTR.css",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
@@ -0,0 +1,58 @@
1
+ import { workspaceDiffStats } from "./git.js";
2
+ /**
3
+ * Git stats are local and cheap, but not cheap enough to run synchronously for every
4
+ * workspace on the 2.5s state poll. Keep the last answer on the wire while a bounded
5
+ * background queue refreshes it; this makes the sidebar live without letting dozens
6
+ * of worktrees fork dozens of `git` processes at once. Working/updated rows stay hot;
7
+ * an idle branch gets a slower safety refresh for edits made outside its agent.
8
+ */
9
+ const WORKING_STALE_MS = 5_000;
10
+ const IDLE_STALE_MS = 60_000;
11
+ const MAX_CONCURRENT = 4;
12
+ const cache = new Map();
13
+ const scheduled = new Set();
14
+ const queue = [];
15
+ let active = 0;
16
+ function taskKey(worktree, base) {
17
+ return `${worktree}\0${base}`;
18
+ }
19
+ function pump() {
20
+ while (active < MAX_CONCURRENT) {
21
+ const task = queue.shift();
22
+ if (!task)
23
+ return;
24
+ active++;
25
+ void workspaceDiffStats(task.worktree, task.base)
26
+ .then(stats => cache.set(task.key, { at: Date.now(), workspaceUpdatedAt: task.workspaceUpdatedAt, stats }), () => cache.set(task.key, { at: Date.now(), workspaceUpdatedAt: task.workspaceUpdatedAt, stats: null }))
27
+ .finally(() => {
28
+ active--;
29
+ scheduled.delete(task.key);
30
+ pump();
31
+ });
32
+ }
33
+ }
34
+ function schedule(worktree, base, workspaceUpdatedAt) {
35
+ const key = taskKey(worktree, base);
36
+ if (scheduled.has(key))
37
+ return;
38
+ scheduled.add(key);
39
+ queue.push({ key, worktree, base, workspaceUpdatedAt });
40
+ pump();
41
+ }
42
+ /** Attach cached line counts and queue stale worktrees for a background refresh. */
43
+ export function attachChangeStats(workspaces) {
44
+ const now = Date.now();
45
+ for (const workspace of workspaces) {
46
+ if (!workspace.worktree) {
47
+ workspace.change_stats = null;
48
+ continue;
49
+ }
50
+ const key = taskKey(workspace.worktree, workspace.baseBranch);
51
+ const hit = cache.get(key);
52
+ workspace.change_stats = hit?.stats ?? null;
53
+ const staleMs = workspace.session_status === 'working' ? WORKING_STALE_MS : IDLE_STALE_MS;
54
+ if (!hit || hit.workspaceUpdatedAt !== workspace.updated_at || now - hit.at >= staleMs) {
55
+ schedule(workspace.worktree, workspace.baseBranch, workspace.updated_at);
56
+ }
57
+ }
58
+ }
@@ -49,6 +49,36 @@ async function untrackedDiff(cwd) {
49
49
  }
50
50
  return { files, patch: patches.join('') };
51
51
  }
52
+ /**
53
+ * Count untracked text without building the full patches used by the diff viewer.
54
+ * `git diff` does not include these files until they enter the index, so each one
55
+ * needs the same `/dev/null` comparison as `untrackedDiff`; `--numstat` keeps that
56
+ * comparison to one short line even when the new file is large.
57
+ */
58
+ async function untrackedStats(cwd) {
59
+ let listing = '';
60
+ try {
61
+ listing = await git(cwd, ['ls-files', '--others', '--exclude-standard', '-z']);
62
+ }
63
+ catch {
64
+ return { added: 0, removed: 0 };
65
+ }
66
+ const stats = { added: 0, removed: 0 };
67
+ for (const file of listing.split('\0').filter(Boolean)) {
68
+ let out = '';
69
+ try {
70
+ out = await git(cwd, ['diff', '--no-index', '--numstat', '--', '/dev/null', file]);
71
+ }
72
+ catch (err) {
73
+ // --no-index exits 1 for the ordinary "these differ" result.
74
+ out = err.stdout ?? '';
75
+ }
76
+ const counted = sumNumstat(out);
77
+ stats.added += counted.added;
78
+ stats.removed += counted.removed;
79
+ }
80
+ return stats;
81
+ }
52
82
  /** Resolve the base ref, preferring the remote-tracking form if it exists. */
53
83
  async function resolveBase(cwd, base) {
54
84
  for (const ref of [`origin/${base}`, base]) {
@@ -62,21 +92,51 @@ async function resolveBase(cwd, base) {
62
92
  }
63
93
  return base;
64
94
  }
65
- /**
66
- * Everything the workspace changed relative to its target branch — committed
67
- * plus uncommitted which is what a reviewer wants to see. Computed straight
68
- * from the worktree, so it's independent of Conductor entirely.
69
- */
70
- export async function workspaceDiff(worktree, base) {
71
- const ref = await resolveBase(worktree, base);
95
+ /** The commit a workspace diff compares its index and worktree against. */
96
+ async function diffBasis(cwd, base) {
97
+ const ref = await resolveBase(cwd, base);
72
98
  let mergeBase = null;
73
99
  try {
74
- mergeBase = (await git(worktree, ['merge-base', ref, 'HEAD'])).trim();
100
+ mergeBase = (await git(cwd, ['merge-base', ref, 'HEAD'])).trim();
75
101
  }
76
102
  catch {
77
103
  mergeBase = null;
78
104
  }
79
- const against = mergeBase ?? ref;
105
+ return { ref, mergeBase, against: mergeBase ?? ref };
106
+ }
107
+ /** Aggregate the numeric columns of `git diff --numstat`; binary-file dashes count as zero lines. */
108
+ export function sumNumstat(numstat) {
109
+ const stats = { added: 0, removed: 0 };
110
+ for (const line of numstat.split('\n')) {
111
+ if (!line)
112
+ continue;
113
+ const [rawAdded, rawRemoved] = line.split('\t');
114
+ const added = rawAdded === '-' ? 0 : Number(rawAdded);
115
+ const removed = rawRemoved === '-' ? 0 : Number(rawRemoved);
116
+ if (Number.isFinite(added))
117
+ stats.added += added;
118
+ if (Number.isFinite(removed))
119
+ stats.removed += removed;
120
+ }
121
+ return stats;
122
+ }
123
+ /**
124
+ * The sidebar's cheap counterpart to `workspaceDiff`: the same base and the same
125
+ * tracked + untracked semantics, without materialising up to 400 KB of patch text.
126
+ */
127
+ export async function workspaceDiffStats(worktree, base) {
128
+ const { against } = await diffBasis(worktree, base);
129
+ const tracked = sumNumstat(await git(worktree, ['diff', '--numstat', against]).catch(() => ''));
130
+ const untracked = await untrackedStats(worktree);
131
+ return { added: tracked.added + untracked.added, removed: tracked.removed + untracked.removed };
132
+ }
133
+ /**
134
+ * Everything the workspace changed relative to its target branch — committed
135
+ * plus uncommitted — which is what a reviewer wants to see. Computed straight
136
+ * from the worktree, so it's independent of Conductor entirely.
137
+ */
138
+ export async function workspaceDiff(worktree, base) {
139
+ const { ref, mergeBase, against } = await diffBasis(worktree, base);
80
140
  const numstat = await git(worktree, ['diff', '--numstat', against]).catch(() => '');
81
141
  const files = numstat
82
142
  .split('\n')
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import zlib from 'node:zlib';
7
7
  import { attachmentPrompt, writeAttachment } from "./attachments.js";
8
8
  import { startAutoUpdate, updateStatus } from "./autoupdate.js";
9
+ import { attachChangeStats } from "./change-stats.js";
9
10
  import { isDefaultEffortLevel, readDefaultEfforts, writeDefaultEfforts } from "./conductor-settings.js";
10
11
  import { loadConfig, stateDir } from "./config.js";
11
12
  import { ConductorDb } from "./db.js";
@@ -828,6 +829,7 @@ const server = http.createServer(async (req, res) => {
828
829
  if (isRoute(routes.state, req.method, pathname)) {
829
830
  const update = updateStatus();
830
831
  const workspaces = reads.listWorkspaces();
832
+ attachChangeStats(workspaces); // serves the cache now; refreshes stale git stats in the background
831
833
  attachPrStatus(workspaces); // colours pr_status from cache; refreshes stale entries in the background
832
834
  // An undelivered first prompt rides along with its workspace: the phone renders it
833
835
  // in that chat rather than tracking delivery itself (see src/firstprompt.ts).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.87.0",
3
+ "version": "1.88.1",
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.",