conductor-remote 1.57.4 → 1.58.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
@@ -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-CSWnlNUe.js"></script>
17
- <link rel="stylesheet" crossorigin href="/assets/index-CoOO4JDW.css">
16
+ <script type="module" crossorigin src="/assets/index-DTYIH7h9.js"></script>
17
+ <link rel="stylesheet" crossorigin href="/assets/index-DT9enoqA.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,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:"f67409db205e29a14d4c249ee12f9253"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CoOO4JDW.css",revision:null},{url:"assets/index-CSWnlNUe.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:"65eae6f412bc584ef0604735364c56f5"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DTYIH7h9.js",revision:null},{url:"assets/index-DT9enoqA.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\//]}))});
@@ -5,7 +5,7 @@
5
5
  handlers to run and hands the result to `osascript`. It lives in its own file
6
6
  rather than a TypeScript template literal for three reasons: an editor can
7
7
  highlight it, `yarn verify` can compile-check it (scripts/check-applescript.ts,
8
- which is the only automated test this code has), and a backtick in a comment
8
+ the repository validator for this language), and a backtick in a comment
9
9
  no longer terminates the string it used to sit inside — so this sentence,
10
10
  which contains `one`, is now harmless.
11
11
 
@@ -89,13 +89,13 @@ export class FirstPromptQueue {
89
89
  * prompt lands (`null`) or is given up on (the failed entry) — awaited by API
90
90
  * callers that asked to block, ignored by the phone.
91
91
  */
92
- enqueue(workspaceId, text, sendImmediately = true, attachmentIds = [], model) {
92
+ enqueue(workspaceId, text, sendImmediately = true, attachmentIds = [], agent) {
93
93
  this.entries = [
94
94
  ...this.entries.filter(e => e.workspaceId !== workspaceId),
95
95
  {
96
96
  workspaceId,
97
97
  text,
98
- ...(model?.trim() ? { model: model.trim() } : {}),
98
+ ...(agent && Object.keys(agent).length ? { agent } : {}),
99
99
  ...(attachmentIds.length ? { attachmentIds } : {}),
100
100
  status: 'waiting',
101
101
  attempts: 0,
@@ -187,7 +187,7 @@ export class FirstPromptQueue {
187
187
  return;
188
188
  // It already went — the user sent it from the Mac, where the deep link left it
189
189
  // pre-filled in the composer. Sending again would double it, and changing the
190
- // model now would affect a later turn instead of the first one they chose it for.
190
+ // agent now would affect a later turn instead of the first one they configured.
191
191
  if (target.alreadySent)
192
192
  return this.delivered(entry);
193
193
  // Before `ready`, a send is worth trying and not worth judging: Conductor may not
@@ -211,8 +211,9 @@ export class FirstPromptQueue {
211
211
  else
212
212
  entry.attempts += 1;
213
213
  this.save();
214
- const result = entry.model
215
- ? await this.setModel(entry, target.sessionId)
214
+ const agent = entry.agent ?? (entry.model ? { model: entry.model } : undefined);
215
+ const result = agent
216
+ ? await this.setAgent(entry, target.sessionId, agent)
216
217
  : entry.text.trim()
217
218
  ? await this.deps.send(entry.workspaceId, target.sessionId, entry.text)
218
219
  : { ok: true };
@@ -242,18 +243,17 @@ export class FirstPromptQueue {
242
243
  this.save();
243
244
  }
244
245
  /**
245
- * Keep a model choice in the same attempt as its prompt. A successful picker
246
- * change is persisted before the send starts, so a send retry cannot toggle a
247
- * model back and forth. A workspace with no prompt settles after this step.
246
+ * Keep agent choices in the same attempt as the prompt. A successful settings
247
+ * change is persisted before the send starts, so a retry cannot toggle controls
248
+ * back and forth. A workspace with no prompt settles after this step.
248
249
  */
249
- async setModel(entry, sessionId) {
250
- if (!entry.model)
251
- return { ok: true };
252
- if (!this.deps.setModel)
253
- return { ok: false, error: 'the relay cannot set a model for a new workspace' };
254
- const result = await this.deps.setModel(entry.workspaceId, sessionId, entry.model);
250
+ async setAgent(entry, sessionId, agent) {
251
+ if (!this.deps.setAgent)
252
+ return { ok: false, error: 'the relay cannot configure a new workspace’s agent' };
253
+ const result = await this.deps.setAgent(entry.workspaceId, sessionId, agent);
255
254
  if (!result.ok)
256
255
  return result;
256
+ entry.agent = undefined;
257
257
  entry.model = undefined;
258
258
  this.save();
259
259
  return entry.text.trim() ? this.deps.send(entry.workspaceId, sessionId, entry.text) : { ok: true };
@@ -31,7 +31,7 @@ import { HIT_CLOSE, HIT_OPEN, 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. create_workspace opens its workspace link without direct UI control; a requested model and the first prompt are applied later through Conductor’s UI. dismiss_prompt, keep_awake and relay_logs touch no UI. send_prompt, split_chat, stop_turn, set_agent_options, a live list_models call and set_workspace_status 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.';
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. create_workspace opens its workspace link without direct UI control; requested agent settings and the first prompt are applied later through Conductor’s UI. dismiss_prompt, keep_awake and relay_logs touch no UI. send_prompt, split_chat, stop_turn, set_agent_options, a live list_models call and set_workspace_status 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.';
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;
@@ -331,7 +331,7 @@ export function createTools(call) {
331
331
  },
332
332
  {
333
333
  name: 'create_workspace',
334
- description: 'Start a new Conductor workspace in a repo, optionally with a first prompt and model. The workspace starts from a Conductor deep link, so creation itself needs no Accessibility. The relay applies a requested model through Conductor’s UI after it creates the first chat and before it delivers the prompt. Returns as soon as the workspace row exists (~2s), before the worktree is built.',
334
+ description: 'Start a new Conductor workspace in a repo, optionally with a first prompt and agent settings. The workspace starts from a Conductor deep link, so creation itself needs no Accessibility. The relay applies requested model, effort, plan, and fast settings through Conductor’s UI after it creates the first chat and before it delivers the prompt. Returns as soon as the workspace row exists (~2s), before the worktree is built.',
335
335
  inputSchema: {
336
336
  type: 'object',
337
337
  properties: {
@@ -341,6 +341,13 @@ export function createTools(call) {
341
341
  type: 'string',
342
342
  description: 'Picker label from list_models with no session_id. The relay applies it before the first prompt, or configures an empty workspace once its chat exists.'
343
343
  },
344
+ effort: {
345
+ type: 'string',
346
+ enum: ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
347
+ description: 'Reasoning effort applied before the first prompt.'
348
+ },
349
+ plan: { type: 'boolean', description: 'Start the first chat in Conductor’s Plan mode.' },
350
+ fast: { type: 'boolean', description: 'Set Fast mode before the first prompt.' },
344
351
  wait_for_send: {
345
352
  type: 'boolean',
346
353
  description: 'Block until the first prompt is actually delivered (tens of seconds, and longer behind other queued sends). Default false.'
@@ -356,12 +363,16 @@ export function createTools(call) {
356
363
  const repo = need(args, 'repo');
357
364
  const prompt = str(args.prompt);
358
365
  const model = str(args.model);
366
+ const effort = str(args.effort);
367
+ const plan = typeof args.plan === 'boolean' ? args.plan : undefined;
368
+ const fast = typeof args.fast === 'boolean' ? args.fast : undefined;
369
+ const configured = model !== undefined || effort !== undefined || plan !== undefined || fast !== undefined;
359
370
  const send = args.wait_for_send === true;
360
371
  // Default on, matching the relay: an omitted flag must never mean "wait".
361
372
  const sendImmediately = args.send_immediately !== false;
362
373
  const data = await call(routes.createWorkspace.path(), {
363
374
  method: routes.createWorkspace.method,
364
- body: { repo, prompt, model, send, sendImmediately },
375
+ body: { repo, prompt, model, effort, plan, fast, send, sendImmediately },
365
376
  timeoutMs: send ? WRITE_TIMEOUT_MS : 30_000
366
377
  });
367
378
  const lines = [
@@ -374,8 +385,8 @@ export function createTools(call) {
374
385
  lines.push('the first prompt is queued — the relay delivers it, so do not send it again');
375
386
  else if (data.sent)
376
387
  lines.push('the first prompt was delivered');
377
- if (data.model)
378
- lines.push(data.configured ? `model applied: ${data.model}` : `model queued: ${data.model}`);
388
+ if (configured)
389
+ lines.push(data.configured ? 'agent settings applied' : 'agent settings queued');
379
390
  return lines.join('\n');
380
391
  }
381
392
  },
@@ -253,7 +253,7 @@ function turnEnded(status) {
253
253
  }
254
254
  /**
255
255
  * The status state machine, kept apart from the store and the network so it can be
256
- * driven a tick at a time by `scripts/check-notify.ts`. Every rule in it is a rule
256
+ * driven a tick at a time by `tests/notify.test.ts`. Every rule in it is a rule
257
257
  * about *not* notifying, and each one is a nuisance or a silence that nothing in the
258
258
  * type system can catch.
259
259
  */
@@ -4,6 +4,14 @@ import path from 'node:path';
4
4
  import { describeRepoIcon, resolveRepoIcon } from "./icons.js";
5
5
  import { workspaceTitle } from "./shared.js";
6
6
  import { parseMessage } from "./transcript.js";
7
+ /** Keep the stable wire field in sync with whichever provider owns the chat. */
8
+ function toSessionRow(row) {
9
+ const { codex_thinking_level, ...session } = row;
10
+ return {
11
+ ...session,
12
+ claude_effort_level: row.agent_type === 'codex' ? codex_thinking_level : row.claude_effort_level
13
+ };
14
+ }
7
15
  const worktreeCache = new Map();
8
16
  /**
9
17
  * Neutralize LIKE's own wildcards in a user's search word. `queryTokens` already
@@ -232,7 +240,8 @@ export class Reads {
232
240
  // have `sent_at` NULL and are skipped, which is why a queued message can't blip the
233
241
  // timer while the previous answer is still going. Served straight off
234
242
  // idx_session_messages_sent_at(session_id, sent_at) — measured free at this poll rate.
235
- return this.db.query(`SELECT s.id, s.status, s.title, s.model, s.permission_mode, s.claude_effort_level, s.fast_mode, s.agent_type,
243
+ const rows = this.db.query(`SELECT s.id, s.status, s.title, s.model, s.permission_mode,
244
+ s.claude_effort_level, s.codex_thinking_level, s.fast_mode, s.agent_type,
236
245
  s.context_used_percent, s.unread_count,
237
246
  s.created_at, s.updated_at, s.last_user_message_at,
238
247
  (SELECT MAX(m.sent_at) FROM session_messages m
@@ -240,6 +249,7 @@ export class Reads {
240
249
  FROM sessions s
241
250
  WHERE s.workspace_id = ? AND COALESCE(s.is_hidden, 0) = 0
242
251
  ORDER BY s.created_at ASC`, [workspaceId]);
252
+ return rows.map(toSessionRow);
243
253
  }
244
254
  /**
245
255
  * Every live chat's status, across all workspaces — what the notifier watches for
@@ -312,11 +312,11 @@ const firstPrompts = new FirstPromptQueue(path.join(stateDir(), 'first-prompts.j
312
312
  const result = await deliverPrompt(ws, sessionId, text);
313
313
  return { ok: result.ok, error: result.error, blocked: lockBlocked(result.error) };
314
314
  }),
315
- setModel: (workspaceId, sessionId, model) => withUiPriority('background', async () => {
315
+ setAgent: (workspaceId, sessionId, agent) => withUiPriority('background', async () => {
316
316
  const ws = reads.getWorkspace(workspaceId);
317
317
  if (!ws)
318
318
  return { ok: false, error: 'the workspace is gone' };
319
- const result = await applyAgentPatch(ws, sessionId, { model });
319
+ const result = await applyAgentPatch(ws, sessionId, agent);
320
320
  return { ok: result.ok, error: result.error, blocked: lockBlocked(result.error) };
321
321
  }),
322
322
  materialize: async (_workspaceId, worktree, attachmentIds) => {
@@ -950,13 +950,29 @@ const server = http.createServer(async (req, res) => {
950
950
  return json(req, res, discardStagedAttachment(STAGED_ATTACHMENTS_DIR, stagedAttachment) ? 200 : 404, {
951
951
  ok: true
952
952
  });
953
- // POST /api/workspaces { repo, prompt, send? } create a workspace via Conductor's deep link
953
+ // POST /api/workspaces { repo, prompt, model?, effort?, plan?, fast?, send? }
954
+ // — create a workspace via Conductor's deep link, then configure its first chat.
954
955
  if (isRoute(routes.createWorkspace, req.method, pathname)) {
955
956
  const body = JSON.parse((await readBody(req)) || '{}');
956
957
  const attachmentIds = body.attachmentIds ?? [];
957
958
  if (body.model !== undefined && typeof body.model !== 'string')
958
959
  return json(req, res, 400, { error: 'model must be a picker label' });
959
- const model = body.model?.trim() || undefined;
960
+ if (body.effort !== undefined && typeof body.effort !== 'string')
961
+ return json(req, res, 400, { error: 'effort must be a string' });
962
+ const effort = body.effort?.trim() || undefined;
963
+ if (effort && !EFFORT_LABELS[effort])
964
+ return json(req, res, 400, { error: `effort must be one of ${Object.keys(EFFORT_LABELS).join(', ')}` });
965
+ if (body.plan !== undefined && typeof body.plan !== 'boolean')
966
+ return json(req, res, 400, { error: 'plan must be a boolean' });
967
+ if (body.fast !== undefined && typeof body.fast !== 'boolean')
968
+ return json(req, res, 400, { error: 'fast must be a boolean' });
969
+ const agent = {
970
+ model: body.model?.trim() || undefined,
971
+ effort,
972
+ plan: body.plan,
973
+ fast: body.fast
974
+ };
975
+ const configureAgent = Object.values(agent).some(value => value !== undefined);
960
976
  if (!Array.isArray(attachmentIds) || attachmentIds.some(id => typeof id !== 'string'))
961
977
  return json(req, res, 400, { error: 'attachment ids must be a list of strings' });
962
978
  const attachments = stagedAttachments(STAGED_ATTACHMENTS_DIR, attachmentIds);
@@ -1000,8 +1016,8 @@ const server = http.createServer(async (req, res) => {
1000
1016
  // its own schedule and the phone watches it in /api/state; `send:true` opts API
1001
1017
  // callers into waiting.
1002
1018
  // Whatever happens, the prompt is already pre-filled in Conductor's composer.
1003
- const settled = prompt || model
1004
- ? firstPrompts.enqueue(created.id, prompt, body.sendImmediately !== false, attachmentIds, model)
1019
+ const settled = prompt || configureAgent
1020
+ ? firstPrompts.enqueue(created.id, prompt, body.sendImmediately !== false, attachmentIds, configureAgent ? agent : undefined)
1005
1021
  : null;
1006
1022
  const failed = settled && body.send === true ? await settled : null;
1007
1023
  settled?.catch(() => undefined); // fire-and-forget: it reports failure, it never rejects
@@ -1010,11 +1026,11 @@ const server = http.createServer(async (req, res) => {
1010
1026
  workspaceId: created.id,
1011
1027
  workspace: reads.getWorkspace(created.id) ?? created,
1012
1028
  pendingPrompt: prompt || undefined,
1013
- model,
1029
+ model: agent.model,
1014
1030
  sent: body.send === true && !!prompt && !failed,
1015
- configured: body.send === true && !!model && !failed,
1031
+ configured: body.send === true && configureAgent && !failed,
1016
1032
  warning: failed?.error &&
1017
- `Workspace created; the initial ${model ? 'model selection and prompt' : 'prompt'} didn’t finish (${failed.error}).`
1033
+ `Workspace created; the initial ${configureAgent ? 'agent settings and prompt' : 'prompt'} didn’t finish (${failed.error}).`
1018
1034
  });
1019
1035
  }
1020
1036
  // GET /api/repos/:name/icon — the repo's resolved sidebar icon (see src/icons.ts)
@@ -38,7 +38,7 @@ function pumpUi() {
38
38
  next.start();
39
39
  }
40
40
  /**
41
- * Take the lock, run `op`, release it. Exported for `scripts/check-uilock.ts`,
41
+ * Take the lock, run `op`, release it. Exported for `tests/ui-lock.test.ts`,
42
42
  * which is the only way this queue's control flow gets read by anything.
43
43
  */
44
44
  export function uiTurn(op) {
@@ -501,10 +501,11 @@ my joinInstantHotspot()`.trim();
501
501
  }
502
502
  }
503
503
  /**
504
- * Conductor stores the effort level as `sessions.claude_effort_level`, but the
505
- * composer button is labelled with the human name and *cycles* through them in
506
- * this order. Both directions are needed: the label to press toward, and the DB
507
- * value to confirm against.
504
+ * Conductor stores the effort level in a provider-specific session column
505
+ * (`codex_thinking_level` or `claude_effort_level`), normalized by Reads onto the
506
+ * relay's stable wire field. The composer button is labelled with the human name
507
+ * and *cycles* through these values, so both directions are needed: the label to
508
+ * press toward, and the normalized DB value to confirm against.
508
509
  */
509
510
  export const EFFORT_LABELS = {
510
511
  low: 'Low',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.57.4",
3
+ "version": "1.58.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.",
@@ -50,29 +50,16 @@
50
50
  "typecheck": "tsc -p tsconfig.json",
51
51
  "lint": "biome check .",
52
52
  "fix": "biome check . --fix",
53
- "verify": "yarn typecheck && yarn lint && yarn check:pr && yarn check:imports && yarn check:routes && yarn check:attachments && yarn check:applescript && yarn check:config && yarn check:utf8-target && yarn check:file-links && yarn check:model-cache && yarn check:mcp-chat-read && yarn check:model-labels && yarn check:transcript-details && yarn check:transcript-actions && yarn check:nosleep && yarn check:uilock && yarn check:firstprompt && yarn check:sendonce && yarn check:notify && yarn check:online",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "test:coverage": "vitest run --coverage",
56
+ "check:repo": "yarn check:imports && yarn check:applescript",
57
+ "verify": "yarn typecheck && yarn lint && yarn test && yarn check:repo",
54
58
  "release": "semantic-release",
55
59
  "prepack": "yarn build && yarn build:node",
56
60
  "postinstall": "husky || true",
57
- "check:attachments": "node scripts/check-attachments.ts",
58
61
  "check:applescript": "node scripts/check-applescript.ts",
59
- "check:config": "node scripts/check-config.ts",
60
- "check:utf8-target": "node scripts/check-utf8-target.ts",
61
- "check:file-links": "node scripts/check-file-links.ts",
62
- "check:firstprompt": "node scripts/check-firstprompt.ts",
63
- "check:model-cache": "node scripts/check-model-cache.ts",
64
- "check:mcp-chat-read": "node scripts/check-mcp-chat-read.ts",
65
- "check:model-labels": "node scripts/check-model-labels.ts",
66
- "check:transcript-details": "node scripts/check-transcript-details.ts",
67
- "check:transcript-actions": "node scripts/check-transcript-actions.ts",
68
- "check:sendonce": "node scripts/check-sendonce.ts",
69
- "check:notify": "node scripts/check-notify.ts",
70
- "check:online": "node scripts/check-online.ts",
71
- "check:pr": "node scripts/check-pr.ts",
72
- "check:imports": "node scripts/check-imports.ts",
73
- "check:nosleep": "node scripts/check-nosleep.ts",
74
- "check:routes": "node scripts/check-routes.ts",
75
- "check:uilock": "node scripts/check-uilock.ts"
62
+ "check:imports": "node scripts/check-imports.ts"
76
63
  },
77
64
  "devDependencies": {
78
65
  "@biomejs/biome": "^2.3.8",
@@ -85,6 +72,7 @@
85
72
  "@types/react": "^19.2.10",
86
73
  "@types/react-dom": "^19.2.3",
87
74
  "@vitejs/plugin-react": "^5.0.4",
75
+ "@vitest/coverage-v8": "^4.1.11",
88
76
  "clsx": "^2.1.1",
89
77
  "husky": "^9.1.7",
90
78
  "jsqr": "^1.4.0",
@@ -102,6 +90,7 @@
102
90
  "typescript": "^5.9.3",
103
91
  "vite": "^7.3.1",
104
92
  "vite-plugin-pwa": "^1.2.0",
93
+ "vitest": "^4.1.11",
105
94
  "zustand": "^5.0.12"
106
95
  }
107
96
  }