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-
|
|
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
|
|
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
|
+
}
|
package/dist-node/src/git.js
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
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(
|
|
100
|
+
mergeBase = (await git(cwd, ['merge-base', ref, 'HEAD'])).trim();
|
|
75
101
|
}
|
|
76
102
|
catch {
|
|
77
103
|
mergeBase = null;
|
|
78
104
|
}
|
|
79
|
-
|
|
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')
|
package/dist-node/src/server.js
CHANGED
|
@@ -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.
|
|
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.",
|