conductor-remote 1.98.0 → 1.99.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
@@ -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-CWS0Qyzc.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-CatXxUrf.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/assets/index-0UqQA-X1.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 c=e=>i(e,o),t={module:{uri:o},exports:l,require:c};s[o]=Promise.all(n.map(e=>t[e]||c(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:"51edeb5f6522b16d55023377170a1bec"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CWS0Qyzc.js",revision:null},{url:"assets/index-0UqQA-X1.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 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:"b64c27d0b8b9afd274befe2a3aa880db"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CatXxUrf.js",revision:null},{url:"assets/index-0UqQA-X1.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,4 +1,4 @@
1
- import { modelAgentType, modelLabel, modelPickerLabel, shortModel } from "./shared.js";
1
+ import { agentTypeCanExposeEffort, agentTypeCanExposeFastMode, modelAgentType, modelLabel, modelPickerLabel, shortModel } from "./shared.js";
2
2
  function modelMatches(state, requested, allowPrefix = false) {
3
3
  const label = modelPickerLabel(requested).trim();
4
4
  const expectedAgentType = modelAgentType(label);
@@ -55,6 +55,12 @@ export async function applyAgentConfig(patch, deps) {
55
55
  if (!current)
56
56
  return { ok: false, error: modelFailure(requestedModel) };
57
57
  }
58
+ if (patch.effort !== undefined && !agentTypeCanExposeEffort(current.agentType)) {
59
+ return { ok: false, error: 'Conductor does not expose a reasoning control for the selected provider.' };
60
+ }
61
+ if (patch.fast !== undefined && !agentTypeCanExposeFastMode(current.agentType)) {
62
+ return { ok: false, error: 'Conductor does not expose a Fast control for the selected provider.' };
63
+ }
58
64
  const controls = {};
59
65
  if (patch.effort && current.effort !== patch.effort)
60
66
  controls.effort = patch.effort;
@@ -27,11 +27,11 @@
27
27
  */
28
28
  import { chatCursor, parseChatCursor } from "./chat-cursor.js";
29
29
  import { routes } from "./routes.js";
30
- import { HIT_CLOSE, HIT_OPEN, isToolResult, timestampMs, workspaceTitle } from "./shared.js";
30
+ import { agentTypeCanExposeEffort, agentTypeCanExposeFastMode, HIT_CLOSE, HIT_OPEN, isToolResult, modelAgentType, timestampMs, workspaceTitle } from "./shared.js";
31
31
  /** Versions we know how to speak. The client's choice wins when we know it. */
32
32
  export const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
33
33
  export const SERVER_INFO = { name: 'conductor-remote', version: '1' };
34
- export const INSTRUCTIONS = 'Drives local Conductor agents through the conductor-remote relay. search_chats and read_chat reach archived workspaces, which is where most finished work lives. delegate_task later drives the real Mac UI and launches another agent through a persisted cross-provider queue; use it only when the user explicitly asked for delegation. It never uses Conductor Plan mode. create_workspace opens its workspace link without direct UI control; requested agent settings and the first prompt are applied later through Conductor’s UI. Its workflow option is itself an explicit delegation request: the configured planning role owns the root chat and may use delegate_task, still in ordinary chat mode and never Plan mode. plan_usage, dismiss_prompt, keep_awake and relay_logs touch no UI. send_prompt, split_chat, stop_turn, close_chat, set_agent_options, set_default_model, a live list_models call, set_workspace_status and archive_workspace drive the real Mac UI and steal focus for a few seconds — confirm with the user before using them on a chat they did not name. archive_workspace also deletes the worktree and stops whatever is running there.';
34
+ export const INSTRUCTIONS = 'Drives local Conductor agents through the conductor-remote relay. search_chats and read_chat reach archived workspaces, which is where most finished work lives. delegate_task later drives the real Mac UI and launches another agent through a persisted cross-provider queue; use it only when the user explicitly asked for delegation. It never uses Conductor Plan mode. create_workspace opens its workspace link without direct UI control; requested agent settings and the first prompt are applied later through Conductor’s UI. Its workflow option is itself an explicit delegation request: the configured planning role owns the root chat and may use delegate_task, still in ordinary chat mode and never Plan mode. plan_usage, dev_server status, dismiss_prompt, keep_awake and relay_logs touch no UI. Use dev_server instead of launching a long-lived development server from a shell: its start/stop actions use Conductor’s configured Run task, drive the real Mac UI and steal focus for a few seconds. send_prompt, split_chat, stop_turn, close_chat, set_agent_options, set_default_model, a live list_models call, set_workspace_status and archive_workspace also drive the real Mac UI and steal focus for a few seconds — confirm with the user before using them on a chat or workspace they did not name. archive_workspace also deletes the worktree and stops whatever is running there.';
35
35
  /** Reads are quick; a UI write is measured in tens of seconds (writes.ts ▸ SEND_ATTEMPT_MS). */
36
36
  export const READ_TIMEOUT_MS = 10_000;
37
37
  export const WRITE_TIMEOUT_MS = 75_000;
@@ -99,6 +99,36 @@ function relative(at) {
99
99
  return `in ${mins}m`;
100
100
  return mins < 0 ? `${-mins}m ago` : 'now';
101
101
  }
102
+ /** Human-readable state that retains every exact Run-config ID and preview URL. */
103
+ function formatDevServer(state, headline = state.running ? 'running' : 'stopped') {
104
+ const summary = [headline];
105
+ if (state.task)
106
+ summary.push(`task ${state.task}`);
107
+ if (state.port !== null)
108
+ summary.push(`port ${state.port}`);
109
+ if (state.forwarded)
110
+ summary.push(state.url ? `forwarded ${state.url}` : 'forwarded');
111
+ else if (state.running)
112
+ summary.push('not forwarded');
113
+ const lines = [summary.join(' · ')];
114
+ if (state.error)
115
+ lines.push(`! ${state.error}`);
116
+ if (state.runConfigs.length) {
117
+ lines.push(`run configs: ${state.runConfigs.map(config => `${config.id} (${config.name})`).join(', ')}`);
118
+ }
119
+ if (state.forwards.length) {
120
+ lines.push('previews:');
121
+ for (const forward of state.forwards) {
122
+ const details = [
123
+ `${forward.name}: port ${forward.port}`,
124
+ forward.running ? 'running' : 'stopped',
125
+ forward.forwarded ? (forward.url ? `forwarded ${forward.url}` : 'forwarded') : 'not forwarded'
126
+ ];
127
+ lines.push(`- ${details.join(' · ')}`);
128
+ }
129
+ }
130
+ return lines.join('\n');
131
+ }
102
132
  const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
103
133
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
104
134
  function need(args, key) {
@@ -358,7 +388,7 @@ export function createTools(call) {
358
388
  },
359
389
  {
360
390
  name: 'set_role',
361
- description: 'Set one cross-provider delegated role using an exact label from list_models. This changes relay config only and does not drive Conductor. Plan mode is intentionally not a role option.',
391
+ description: 'Set one cross-provider delegated role using an exact label from list_models. Effort and Fast are available only when Conductor exposes those controls for the selected provider; omit them for Cursor and OpenCode. This changes relay config only and does not drive Conductor. Plan mode is intentionally not a role option.',
362
392
  inputSchema: {
363
393
  type: 'object',
364
394
  properties: {
@@ -366,9 +396,10 @@ export function createTools(call) {
366
396
  model: { type: 'string', description: 'Exact picker label from list_models.' },
367
397
  effort: {
368
398
  type: 'string',
369
- enum: ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode']
399
+ enum: ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
400
+ description: 'Optional Claude/Codex reasoning control. Omit for Cursor and OpenCode.'
370
401
  },
371
- fast: { type: 'boolean' },
402
+ fast: { type: 'boolean', description: 'Optional Claude/Codex Fast control.' },
372
403
  preamble: { type: 'string', description: 'Optional instructions prepended to each delegated task.' }
373
404
  },
374
405
  required: ['role', 'model']
@@ -380,14 +411,27 @@ export function createTools(call) {
380
411
  const efforts = ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'];
381
412
  if (effort && !efforts.includes(effort))
382
413
  throw new Error('effort is invalid');
414
+ const agentType = modelAgentType(model);
415
+ if (effort && agentType && !agentTypeCanExposeEffort(agentType)) {
416
+ throw new Error('the selected provider has no Conductor reasoning control; omit effort');
417
+ }
418
+ if (typeof args.fast === 'boolean' && agentType && !agentTypeCanExposeFastMode(agentType)) {
419
+ throw new Error('the selected provider has no Conductor Fast control; omit fast');
420
+ }
383
421
  const current = await call(routes.roles.path());
384
422
  const prior = current.roles[name];
423
+ const preserveEffort = agentTypeCanExposeEffort(agentType);
424
+ const preserveFast = agentTypeCanExposeFastMode(agentType);
385
425
  const role = {
386
426
  model,
387
- ...(effort ? { effort: effort } : prior?.effort ? { effort: prior.effort } : {}),
427
+ ...(effort
428
+ ? { effort: effort }
429
+ : preserveEffort && prior?.effort
430
+ ? { effort: prior.effort }
431
+ : {}),
388
432
  ...(typeof args.fast === 'boolean'
389
433
  ? { fast: args.fast }
390
- : prior?.fast !== undefined
434
+ : preserveFast && prior?.fast !== undefined
391
435
  ? { fast: prior.fast }
392
436
  : {}),
393
437
  ...(typeof args.preamble === 'string'
@@ -930,6 +974,57 @@ export function createTools(call) {
930
974
  return `status set to ${status}`;
931
975
  }
932
976
  },
977
+ {
978
+ name: 'dev_server',
979
+ description: 'Inspect, start or stop a workspace’s configured Conductor Run task and tailnet preview. Use this instead of launching a long-lived development server from a shell: Conductor supplies the workspace ports, enforces run-mode rules and owns process cleanup, while the relay avoids duplicate starts. Status is the default and touches no UI. Start and stop DRIVE THE REAL UI and can steal focus for a few seconds; confirm before controlling a workspace the user did not name. Starting requires Tailscale and, when status lists multiple Run configs, an exact run_config_id.',
980
+ inputSchema: {
981
+ type: 'object',
982
+ properties: {
983
+ workspace_id: { type: 'string', description: 'The live local workspace to control.' },
984
+ action: {
985
+ type: 'string',
986
+ enum: ['status', 'start', 'stop'],
987
+ description: 'Default status. Start and stop use Conductor’s managed Run task.'
988
+ },
989
+ run_config_id: {
990
+ type: 'string',
991
+ description: 'Exact ID printed by status. Only valid for start; required when more than one is listed.'
992
+ }
993
+ },
994
+ required: ['workspace_id']
995
+ },
996
+ run: async (args) => {
997
+ const id = need(args, 'workspace_id');
998
+ const action = str(args.action) ?? 'status';
999
+ if (action !== 'status' && action !== 'start' && action !== 'stop')
1000
+ throw new Error('action is invalid');
1001
+ const runConfigId = str(args.run_config_id);
1002
+ if (args.run_config_id !== undefined && !runConfigId)
1003
+ throw new Error('run_config_id must be a non-empty string');
1004
+ if (runConfigId && action !== 'start')
1005
+ throw new Error('run_config_id is only valid with action start');
1006
+ if (action === 'status') {
1007
+ const state = await call(routes.devServer.path(id));
1008
+ return formatDevServer(state);
1009
+ }
1010
+ const route = action === 'start' ? routes.startDevServer : routes.stopDevServer;
1011
+ const result = await call(route.path(id), {
1012
+ method: route.method,
1013
+ ...(action === 'start' && runConfigId ? { body: { runConfigId } } : {}),
1014
+ timeoutMs: WRITE_TIMEOUT_MS
1015
+ });
1016
+ if (!result.ok)
1017
+ throw new Error(result.error ?? `could not ${action} the dev server`);
1018
+ const headline = action === 'start'
1019
+ ? result.changed === false
1020
+ ? 'already running'
1021
+ : 'started'
1022
+ : result.changed === false
1023
+ ? 'already stopped'
1024
+ : 'stopped';
1025
+ return formatDevServer(result, headline);
1026
+ }
1027
+ },
933
1028
  {
934
1029
  name: 'archive_workspace',
935
1030
  description: 'Archive a workspace — Conductor’s own ⌘⇧A, which deletes its worktree and takes any agent still working in it down with it. The chat survives archiving and stays readable through search_chats and read_chat. Drives the real UI: it focuses the workspace on the Mac first, so confirm with the user before archiving one they did not name. A workspace with an agent working is refused unless stop_agents is true.',
@@ -9,7 +9,7 @@
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { stateDir } from "./config.js";
12
- import { modelAgentType, modelCatalogIncludes, modelPickerLabel } from "./shared.js";
12
+ import { agentTypeCanExposeEffort, agentTypeCanExposeFastMode, modelAgentType, modelCatalogIncludes, modelPickerLabel } from "./shared.js";
13
13
  const EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode']);
14
14
  const ROLE_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
15
15
  const MAX_ROLES = 32;
@@ -23,7 +23,7 @@ export const DEFAULT_ROLES = {
23
23
  planning: { model: 'Fable 5', effort: 'max', fast: false, preamble: batonPreamble('planning') },
24
24
  // Conductor currently offers two distinct Muse Spark rows. This descriptive
25
25
  // placeholder intentionally matches neither, so the user must choose exactly.
26
- exploration: { model: 'Muse Spark', effort: 'high', fast: false, preamble: batonPreamble('exploration') },
26
+ exploration: { model: 'Muse Spark', preamble: batonPreamble('exploration') },
27
27
  implementation: { model: '5.6 Sol', effort: 'xhigh', fast: false, preamble: batonPreamble('implementation') }
28
28
  }
29
29
  };
@@ -88,10 +88,17 @@ export function decodeRoles(raw) {
88
88
  function issue(code, message) {
89
89
  return { code, message, retryable: false };
90
90
  }
91
- function effortIssue(name, role, agentType) {
92
- return role.effort === 'none' && agentType !== 'codex'
93
- ? issue('invalid_request', `Role ${name} can use None effort only with a Codex model.`)
94
- : null;
91
+ function controlIssue(name, role, agentType) {
92
+ if (role.effort !== undefined && !agentTypeCanExposeEffort(agentType)) {
93
+ return issue('invalid_request', `Role ${name} cannot set effort because Conductor exposes no reasoning control for this provider.`);
94
+ }
95
+ if (role.effort === 'none' && agentType !== 'codex') {
96
+ return issue('invalid_request', `Role ${name} can use None effort only with a Codex model.`);
97
+ }
98
+ if (role.fast !== undefined && !agentTypeCanExposeFastMode(agentType)) {
99
+ return issue('invalid_request', `Role ${name} cannot set Fast mode because Conductor exposes no Fast control for this provider.`);
100
+ }
101
+ return null;
95
102
  }
96
103
  /** Picker validation stays separate from shape decoding so a vanished model remains editable. */
97
104
  export function roleModelIssues(config, groups) {
@@ -112,7 +119,7 @@ export function roleModelIssues(config, groups) {
112
119
  });
113
120
  continue;
114
121
  }
115
- const invalidEffort = effortIssue(name, role, agentType);
122
+ const invalidEffort = controlIssue(name, role, agentType);
116
123
  if (invalidEffort)
117
124
  issues.push({ role: name, error: invalidEffort });
118
125
  }
@@ -136,7 +143,7 @@ export function resolveRole(config, name, groups) {
136
143
  error: issue('provider_unknown', `Role ${name}'s model label does not identify a supported provider.`)
137
144
  };
138
145
  }
139
- const invalidEffort = effortIssue(name, role, agentType);
146
+ const invalidEffort = controlIssue(name, role, agentType);
140
147
  if (invalidEffort)
141
148
  return { ok: false, error: invalidEffort };
142
149
  return { ok: true, role: { ...cloneRole(role), agentType } };
@@ -178,6 +178,19 @@ export function modelAgentType(model) {
178
178
  return 'acp';
179
179
  return undefined;
180
180
  }
181
+ /**
182
+ * Whether this harness can render a per-session reasoning control. Claude and
183
+ * Codex still decide support model by model; Cursor and OpenCode do not expose
184
+ * one in Conductor today, so a stored default in their session row is not a
185
+ * capability signal.
186
+ */
187
+ export function agentTypeCanExposeEffort(agentType) {
188
+ return agentType === 'claude' || agentType === 'codex';
189
+ }
190
+ /** Conductor currently exposes its Fast session control only for Claude and Codex. */
191
+ export function agentTypeCanExposeFastMode(agentType) {
192
+ return agentType === 'claude' || agentType === 'codex';
193
+ }
181
194
  /** Whether one exact picker label appears in any cached whole-menu snapshot. */
182
195
  export function modelCatalogIncludes(model, groups) {
183
196
  const wanted = modelPickerLabel(model);
@@ -607,9 +607,9 @@ const CODEX_EFFORT_LABELS = {
607
607
  export function effortUiLabel(effort, agentType) {
608
608
  if (agentType === 'codex')
609
609
  return CODEX_EFFORT_LABELS[effort];
610
- if (effort === 'none')
611
- return undefined;
612
- return EFFORT_LABELS[effort];
610
+ if (agentType === 'claude' && effort !== 'none')
611
+ return EFFORT_LABELS[effort];
612
+ return undefined;
613
613
  }
614
614
  /**
615
615
  * A boolean patch says what state the caller wants, not that the matching UI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.98.0",
3
+ "version": "1.99.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.",