conductor-remote 1.25.0 → 1.26.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/assets/index-0lGqzMHX.css +1 -0
- package/dist/assets/index-q0jubVD3.js +40 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/reads.js +19 -1
- package/dist-node/src/server.js +166 -2
- package/dist-node/src/writes.js +702 -43
- package/package.json +1 -1
- package/dist/assets/index-D6B0k-tm.css +0 -1
- package/dist/assets/index-qjWM9xpd.js +0 -40
package/dist/index.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
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-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-q0jubVD3.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-0lGqzMHX.css">
|
|
18
18
|
<link rel="manifest" href="/manifest.webmanifest"></head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,i={};const s=(s,n)=>(s=new URL(s+".js",n).href,i[s]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=i,document.head.appendChild(e)}else e=s,importScripts(s),i()}).then(()=>{let e=i[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let l={};const
|
|
1
|
+
if(!self.define){let e,i={};const s=(s,n)=>(s=new URL(s+".js",n).href,i[s]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=i,document.head.appendChild(e)}else e=s,importScripts(s),i()}).then(()=>{let e=i[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let l={};const c=e=>s(e,o),t={module:{uri:o},exports:l,require:c};i[o]=Promise.all(n.map(e=>t[e]||c(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"index.html",revision:"a8e25a01d32edf68c3ca0b827fb79612"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-q0jubVD3.js",revision:null},{url:"assets/index-0lGqzMHX.css",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\//]}))});
|
package/dist-node/src/reads.js
CHANGED
|
@@ -74,6 +74,23 @@ export class Reads {
|
|
|
74
74
|
getWorkspace(id) {
|
|
75
75
|
return this.listWorkspaces().find(w => w.id === id) ?? null;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The repos Conductor knows about, in its own sidebar order. `root_path` is
|
|
79
|
+
* what a `conductor://…&path=` deep link needs to pick the target repo — with
|
|
80
|
+
* no path Conductor silently falls back to the *first* repo.
|
|
81
|
+
*/
|
|
82
|
+
listRepos() {
|
|
83
|
+
const rows = this.db.query(`SELECT name, root_path, default_branch, icon, remote_url
|
|
84
|
+
FROM repos
|
|
85
|
+
WHERE COALESCE(hidden, 0) = 0
|
|
86
|
+
ORDER BY (display_order IS NULL), display_order, name`);
|
|
87
|
+
return rows.map(r => ({
|
|
88
|
+
name: r.name,
|
|
89
|
+
root_path: r.root_path,
|
|
90
|
+
default_branch: r.default_branch,
|
|
91
|
+
icon: describeRepoIcon({ icon: r.icon, repoRoot: r.root_path, remoteUrl: r.remote_url })
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
77
94
|
/** Resolve a repo's icon by its name (the sidebar avatar) — null if the repo or icon is unknown. */
|
|
78
95
|
resolveRepoIcon(repoName) {
|
|
79
96
|
const rows = this.db.query('SELECT root_path FROM repos WHERE name = ? LIMIT 1', [
|
|
@@ -84,7 +101,8 @@ export class Reads {
|
|
|
84
101
|
}
|
|
85
102
|
listSessions(workspaceId) {
|
|
86
103
|
// created_at ASC keeps tab order stable (matches the desktop app) instead of jumping on activity.
|
|
87
|
-
return this.db.query(`SELECT id, status, title, model, permission_mode,
|
|
104
|
+
return this.db.query(`SELECT id, status, title, model, permission_mode, claude_effort_level, fast_mode, agent_type,
|
|
105
|
+
context_used_percent, unread_count,
|
|
88
106
|
created_at, updated_at, last_user_message_at
|
|
89
107
|
FROM sessions
|
|
90
108
|
WHERE workspace_id = ? AND COALESCE(is_hidden, 0) = 0
|
package/dist-node/src/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import { mergePr } from "./merge.js";
|
|
|
12
12
|
import { attachPrStatus } from "./pr.js";
|
|
13
13
|
import { Reads } from "./reads.js";
|
|
14
14
|
import { driftWarningLines, tailscaleBin } from "./tailscale.js";
|
|
15
|
-
import { describeActuator, newChat, pickActuator } from "./writes.js";
|
|
15
|
+
import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, newChat, pickActuator, setAgentOptions } from "./writes.js";
|
|
16
16
|
const cfg = loadConfig();
|
|
17
17
|
const db = new ConductorDb(cfg.dbPath);
|
|
18
18
|
const reads = new Reads(db, cfg.workspacesRoot);
|
|
@@ -37,6 +37,65 @@ async function confirmDelivery(sessionId, text, sinceRowid) {
|
|
|
37
37
|
}
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Where a chat sits in Conductor's tab strip. Both write paths need it: the
|
|
42
|
+
* actuator selects that tab before touching anything, otherwise it acts on
|
|
43
|
+
* whichever tab happens to be active.
|
|
44
|
+
*/
|
|
45
|
+
function locateChat(ws, sessionId) {
|
|
46
|
+
const sessions = reads.listSessions(ws.id);
|
|
47
|
+
const index = sessions.findIndex(s => s.id === sessionId);
|
|
48
|
+
if (index < 0 && sessions.length > 1)
|
|
49
|
+
return { error: 'chat is no longer one of the workspace’s tabs' };
|
|
50
|
+
if (index < 0)
|
|
51
|
+
return { tab: undefined, session: undefined };
|
|
52
|
+
return {
|
|
53
|
+
tab: { index: index + 1, count: sessions.length, title: sessions[index].title ?? '' },
|
|
54
|
+
session: sessions[index]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Poll the DB until Conductor records the setting we just drove through the UI. */
|
|
58
|
+
async function confirmAgentOptions(ws, sessionId, opts) {
|
|
59
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
60
|
+
const s = reads.listSessions(ws.id).find(row => row.id === sessionId);
|
|
61
|
+
const effortOk = !opts.effort || s?.claude_effort_level === opts.effort;
|
|
62
|
+
const planOk = opts.plan === undefined || s?.permission_mode === (opts.plan ? 'plan' : 'default');
|
|
63
|
+
if (effortOk && planOk)
|
|
64
|
+
return true;
|
|
65
|
+
await sleep(300);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Send a freshly-created workspace's first prompt. Never fails the request: the
|
|
71
|
+
* workspace exists either way, so an unsent prompt is a `warning` (it's still
|
|
72
|
+
* pre-filled in Conductor's composer, one tap from going) rather than an error.
|
|
73
|
+
*/
|
|
74
|
+
async function submitFirstPrompt(workspaceId, prompt) {
|
|
75
|
+
// A new workspace is 'setting_up' while its worktree (and setup script) runs;
|
|
76
|
+
// its composer isn't the visible pane yet, so wait for 'ready' before typing.
|
|
77
|
+
let ws = reads.getWorkspace(workspaceId);
|
|
78
|
+
for (let attempt = 0; attempt < 60 && ws?.state !== 'ready'; attempt++) {
|
|
79
|
+
await sleep(500);
|
|
80
|
+
ws = reads.getWorkspace(workspaceId);
|
|
81
|
+
}
|
|
82
|
+
if (ws?.state !== 'ready') {
|
|
83
|
+
return { sent: false, warning: 'Workspace created; still setting up, so the prompt is pre-filled but not sent.' };
|
|
84
|
+
}
|
|
85
|
+
const located = locateChat(ws, reads.listSessions(workspaceId)[0]?.id ?? '');
|
|
86
|
+
const tab = 'error' in located ? undefined : located.tab;
|
|
87
|
+
const sessionId = reads.listSessions(workspaceId)[0]?.id;
|
|
88
|
+
if (!sessionId)
|
|
89
|
+
return { sent: false, warning: 'Workspace created, but it has no chat yet — prompt is pre-filled.' };
|
|
90
|
+
const beforeRowid = reads.getMessages(sessionId).cursor;
|
|
91
|
+
const result = await actuator.send({ workspace: ws, sessionId, tab }, prompt);
|
|
92
|
+
if (!result.ok)
|
|
93
|
+
return { sent: false, warning: `Workspace created; the prompt is pre-filled but wasn’t sent (${result.error}).` };
|
|
94
|
+
if (!(await confirmDelivery(sessionId, prompt, beforeRowid))) {
|
|
95
|
+
return { sent: false, warning: 'Workspace created; the prompt is pre-filled but didn’t land — send it again.' };
|
|
96
|
+
}
|
|
97
|
+
return { sent: true };
|
|
98
|
+
}
|
|
40
99
|
const MIME = {
|
|
41
100
|
'.html': 'text/html; charset=utf-8',
|
|
42
101
|
'.js': 'text/javascript; charset=utf-8',
|
|
@@ -150,6 +209,57 @@ const server = http.createServer(async (req, res) => {
|
|
|
150
209
|
update
|
|
151
210
|
});
|
|
152
211
|
}
|
|
212
|
+
// GET /api/repos — repos a new workspace can be created in
|
|
213
|
+
if (req.method === 'GET' && pathname === '/api/repos') {
|
|
214
|
+
return json(req, res, 200, { repos: reads.listRepos() });
|
|
215
|
+
}
|
|
216
|
+
// POST /api/workspaces { repo, prompt, send? } — create a workspace via Conductor's deep link
|
|
217
|
+
if (req.method === 'POST' && pathname === '/api/workspaces') {
|
|
218
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
219
|
+
// The prompt is optional — a bare `path=` opens an empty workspace, like
|
|
220
|
+
// Conductor's own New workspace — but *something* has to say where it goes.
|
|
221
|
+
const prompt = (body.prompt ?? '').trim();
|
|
222
|
+
if (!prompt && !body.repo)
|
|
223
|
+
return json(req, res, 400, { error: 'need a repo or a prompt' });
|
|
224
|
+
// Resolve the repo to a real path: an unmatched `path` would silently land
|
|
225
|
+
// the workspace in whichever repo Conductor happens to list first.
|
|
226
|
+
const repo = body.repo ? reads.listRepos().find(r => r.name === body.repo) : undefined;
|
|
227
|
+
if (body.repo && !repo)
|
|
228
|
+
return json(req, res, 404, { error: `unknown repo ${body.repo}` });
|
|
229
|
+
if (repo && !repo.root_path)
|
|
230
|
+
return json(req, res, 409, { error: `${repo.name} has no checkout path` });
|
|
231
|
+
const before = new Set(reads.listWorkspaces().map(w => w.id));
|
|
232
|
+
const result = await createWorkspace(prompt, repo?.root_path ?? null);
|
|
233
|
+
if (!result.ok)
|
|
234
|
+
return json(req, res, 502, result);
|
|
235
|
+
// The deep link is fire-and-forget, so the new row is the only proof it worked.
|
|
236
|
+
// Creating a worktree takes a beat longer than opening a chat does.
|
|
237
|
+
let created;
|
|
238
|
+
for (let attempt = 0; attempt < 40 && !created; attempt++) {
|
|
239
|
+
await sleep(500);
|
|
240
|
+
created = reads.listWorkspaces().find(w => !before.has(w.id));
|
|
241
|
+
}
|
|
242
|
+
if (!created) {
|
|
243
|
+
return json(req, res, 502, {
|
|
244
|
+
ok: false,
|
|
245
|
+
strategy: result.strategy,
|
|
246
|
+
error: 'Conductor didn’t create a workspace — check it’s running and not showing a dialog.'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// Return as soon as the row exists (~2s) and let the caller submit the prompt
|
|
250
|
+
// once the worktree is ready. Waiting here would block the request through
|
|
251
|
+
// Conductor's whole setup — measured at 30s+ on a real repo, past the phone's
|
|
252
|
+
// own 25s budget. `send:true` opts into the blocking path for API callers.
|
|
253
|
+
// Whatever happens, the prompt is already pre-filled in Conductor's composer.
|
|
254
|
+
const submitted = body.send === true && prompt ? await submitFirstPrompt(created.id, prompt) : { sent: false };
|
|
255
|
+
return json(req, res, 200, {
|
|
256
|
+
ok: true,
|
|
257
|
+
workspaceId: created.id,
|
|
258
|
+
workspace: reads.getWorkspace(created.id) ?? created,
|
|
259
|
+
pendingPrompt: prompt || undefined,
|
|
260
|
+
...submitted
|
|
261
|
+
});
|
|
262
|
+
}
|
|
153
263
|
// GET /api/repos/:name/icon — the repo's resolved sidebar icon (see src/icons.ts)
|
|
154
264
|
let m = pathname.match(/^\/api\/repos\/([^/]+)\/icon$/);
|
|
155
265
|
if (req.method === 'GET' && m) {
|
|
@@ -213,6 +323,56 @@ const server = http.createServer(async (req, res) => {
|
|
|
213
323
|
const after = Number(url.searchParams.get('after') ?? 0);
|
|
214
324
|
return json(req, res, 200, reads.getMessages(decodeURIComponent(m[1]), Number.isFinite(after) ? after : 0));
|
|
215
325
|
}
|
|
326
|
+
// GET /api/sessions/:id/models?workspaceId= — labels from Conductor's live picker
|
|
327
|
+
m = pathname.match(/^\/api\/sessions\/([^/]+)\/models$/);
|
|
328
|
+
if (req.method === 'GET' && m) {
|
|
329
|
+
const sessionId = decodeURIComponent(m[1]);
|
|
330
|
+
const ws = reads.getWorkspace(url.searchParams.get('workspaceId') ?? '');
|
|
331
|
+
if (!ws)
|
|
332
|
+
return json(req, res, 404, { error: 'workspace for session not found' });
|
|
333
|
+
const located = locateChat(ws, sessionId);
|
|
334
|
+
if ('error' in located)
|
|
335
|
+
return json(req, res, 409, { error: located.error });
|
|
336
|
+
const result = await listAgentModels({ workspace: ws, sessionId, tab: located.tab });
|
|
337
|
+
return json(req, res, result.ok ? 200 : 502, result);
|
|
338
|
+
}
|
|
339
|
+
// POST /api/sessions/:id/agent { effort?, plan?, fast?, model? }
|
|
340
|
+
// Drives the composer's own model/effort/plan/fast controls for one chat.
|
|
341
|
+
m = pathname.match(/^\/api\/sessions\/([^/]+)\/agent$/);
|
|
342
|
+
if (req.method === 'POST' && m) {
|
|
343
|
+
const sessionId = decodeURIComponent(m[1]);
|
|
344
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
345
|
+
if (body.effort && !EFFORT_LABELS[body.effort]) {
|
|
346
|
+
return json(req, res, 400, { error: `effort must be one of ${Object.keys(EFFORT_LABELS).join(', ')}` });
|
|
347
|
+
}
|
|
348
|
+
const ws = body.workspaceId
|
|
349
|
+
? reads.getWorkspace(body.workspaceId)
|
|
350
|
+
: (reads.listWorkspaces().find(w => w.active_session_id === sessionId) ?? null);
|
|
351
|
+
if (!ws)
|
|
352
|
+
return json(req, res, 404, { error: 'workspace for session not found' });
|
|
353
|
+
const located = locateChat(ws, sessionId);
|
|
354
|
+
if ('error' in located)
|
|
355
|
+
return json(req, res, 409, { error: located.error });
|
|
356
|
+
// Fast mode exposes no readable state in the UI, so the DB decides whether
|
|
357
|
+
// the button actually needs pressing — pressing blindly would toggle it off.
|
|
358
|
+
const opts = {
|
|
359
|
+
effort: body.effort,
|
|
360
|
+
plan: body.plan,
|
|
361
|
+
model: body.model,
|
|
362
|
+
toggleFast: body.fast === undefined ? false : body.fast !== Boolean(located.session?.fast_mode)
|
|
363
|
+
};
|
|
364
|
+
const result = await setAgentOptions({ workspace: ws, sessionId, tab: located.tab }, opts);
|
|
365
|
+
if (!result.ok)
|
|
366
|
+
return json(req, res, 502, result);
|
|
367
|
+
if (!(await confirmAgentOptions(ws, sessionId, opts))) {
|
|
368
|
+
return json(req, res, 502, {
|
|
369
|
+
ok: false,
|
|
370
|
+
strategy: result.strategy,
|
|
371
|
+
error: 'Conductor didn’t record the change — it may have been asleep. Try again.'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return json(req, res, 200, { ok: true, session: reads.listSessions(ws.id).find(s => s.id === sessionId) });
|
|
375
|
+
}
|
|
216
376
|
// POST /api/sessions/:id/prompt { text }
|
|
217
377
|
m = pathname.match(/^\/api\/sessions\/([^/]+)\/prompt$/);
|
|
218
378
|
if (req.method === 'POST' && m) {
|
|
@@ -226,9 +386,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
226
386
|
: (reads.listWorkspaces().find(w => w.active_session_id === sessionId) ?? null);
|
|
227
387
|
if (!ws)
|
|
228
388
|
return json(req, res, 404, { error: 'workspace for session not found' });
|
|
389
|
+
const located = locateChat(ws, sessionId);
|
|
390
|
+
if ('error' in located)
|
|
391
|
+
return json(req, res, 409, { error: located.error });
|
|
392
|
+
const { tab } = located;
|
|
229
393
|
// Snapshot the transcript cursor so we can confirm the prompt actually lands.
|
|
230
394
|
const beforeRowid = reads.getMessages(sessionId).cursor;
|
|
231
|
-
const result = await actuator.send({ workspace: ws, sessionId }, text);
|
|
395
|
+
const result = await actuator.send({ workspace: ws, sessionId, tab }, text);
|
|
232
396
|
if (result.ok && !(await confirmDelivery(sessionId, text, beforeRowid))) {
|
|
233
397
|
return json(req, res, 502, {
|
|
234
398
|
ok: false,
|