viveworker 0.8.0 → 0.8.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.
@@ -75,6 +75,17 @@ if (rawArgs[0] === "stats") {
75
75
  }
76
76
  }
77
77
 
78
+ if (rawArgs[0] === "mcp") {
79
+ const { runMcpCli } = await import("./mcp-server.mjs");
80
+ try {
81
+ await runMcpCli(rawArgs.slice(1));
82
+ process.exit(0);
83
+ } catch (error) {
84
+ console.error(error.message || String(error));
85
+ process.exit(1);
86
+ }
87
+ }
88
+
78
89
  const cli = parseArgs(rawArgs);
79
90
 
80
91
  try {
@@ -388,7 +399,7 @@ async function runPair(cliOptions) {
388
399
  async function runEnable(cliOptions) {
389
400
  const target = String(cliOptions.enableTarget || "").trim().toLowerCase();
390
401
  if (!target) {
391
- throw new Error("Usage: viveworker enable <claude|a2a|moltbook|scout> [...]");
402
+ throw new Error("Usage: viveworker enable <claude|a2a|moltbook|scout|mcp> [...]");
392
403
  }
393
404
 
394
405
  switch (target) {
@@ -406,11 +417,248 @@ async function runEnable(cliOptions) {
406
417
  case "scout":
407
418
  await runEnableScout(cliOptions);
408
419
  return;
420
+ case "mcp":
421
+ await runEnableMcp(cliOptions);
422
+ return;
409
423
  default:
410
424
  throw new Error(`Unknown feature: ${target}`);
411
425
  }
412
426
  }
413
427
 
428
+ async function runEnableMcp(cliOptions) {
429
+ const target = normalizeMcpInstallTarget(cliOptions.mcpTarget || "all");
430
+ const allTargets = target === "all" ? ["claude", "cursor", "codex"] : [target];
431
+ const entries = [];
432
+ const config = mcpServerConfigSnippet();
433
+
434
+ for (const item of allTargets) {
435
+ const entry = await prepareMcpConfigChange(item, config, { explicit: target !== "all" });
436
+ if (entry) entries.push(entry);
437
+ }
438
+
439
+ if (entries.length === 0) {
440
+ console.log("");
441
+ console.log("No supported MCP client config was found.");
442
+ console.log("Run `npx viveworker mcp config` to print a manual config snippet.");
443
+ return;
444
+ }
445
+
446
+ console.log("");
447
+ console.log("viveworker MCP setup");
448
+ console.log("");
449
+ console.log("The following MCP client config changes will be applied:");
450
+ for (const entry of entries) {
451
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}`);
452
+ }
453
+ console.log("");
454
+ console.log("MCP server entry:");
455
+ console.log(JSON.stringify({ mcpServers: { viveworker: config } }, null, 2));
456
+
457
+ if (cliOptions.mcpDryRun) {
458
+ console.log("");
459
+ console.log("Dry run only. No files were changed.");
460
+ return;
461
+ }
462
+
463
+ const changedEntries = entries.filter((entry) => entry.changed);
464
+ if (changedEntries.length === 0) {
465
+ console.log("");
466
+ console.log("MCP config is already up to date.");
467
+ return;
468
+ }
469
+
470
+ if (!cliOptions.yes) {
471
+ const confirmed = await confirmCli("Write these MCP config changes? [y/N] ");
472
+ if (!confirmed) {
473
+ console.log("Cancelled.");
474
+ return;
475
+ }
476
+ }
477
+
478
+ for (const entry of changedEntries) {
479
+ await fs.mkdir(path.dirname(entry.path), { recursive: true });
480
+ await fs.writeFile(entry.path, entry.nextText, entry.mode || "utf8");
481
+ }
482
+
483
+ console.log("");
484
+ console.log("MCP config updated.");
485
+ console.log("Restart the target app so it reloads the MCP server list.");
486
+ }
487
+
488
+ function normalizeMcpInstallTarget(value) {
489
+ const target = String(value || "all").trim().toLowerCase();
490
+ if (["claude", "cursor", "codex", "all"].includes(target)) {
491
+ return target;
492
+ }
493
+ throw new Error("Use --target claude, --target cursor, --target codex, or --target all.");
494
+ }
495
+
496
+ function mcpServerConfigSnippet() {
497
+ return {
498
+ command: "npx",
499
+ args: ["viveworker", "mcp"],
500
+ };
501
+ }
502
+
503
+ async function prepareMcpConfigChange(target, serverConfig, { explicit = false } = {}) {
504
+ const descriptor = mcpConfigDescriptor(target);
505
+ if (!descriptor) {
506
+ return null;
507
+ }
508
+ if (!explicit && !(await fileExists(descriptor.parentDir))) {
509
+ return null;
510
+ }
511
+ const currentText = await readOptionalText(descriptor.path);
512
+ const result = descriptor.kind === "toml"
513
+ ? upsertCodexMcpConfig(currentText, serverConfig)
514
+ : upsertJsonMcpConfig(currentText, serverConfig);
515
+ return {
516
+ target,
517
+ label: descriptor.label,
518
+ path: descriptor.path,
519
+ status: result.changed
520
+ ? (currentText.trim() ? "update" : "create")
521
+ : "unchanged",
522
+ changed: result.changed,
523
+ nextText: result.text,
524
+ };
525
+ }
526
+
527
+ function mcpConfigDescriptor(target) {
528
+ if (target === "claude") {
529
+ const override = process.env.VIVEWORKER_MCP_CLAUDE_CONFIG_FILE || "";
530
+ const defaultPath = path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
531
+ const configPath = resolvePath(override || defaultPath);
532
+ return {
533
+ kind: "json",
534
+ label: "Claude Desktop",
535
+ path: configPath,
536
+ parentDir: path.dirname(configPath),
537
+ };
538
+ }
539
+ if (target === "cursor") {
540
+ const override = process.env.VIVEWORKER_MCP_CURSOR_CONFIG_FILE || "";
541
+ const defaultPath = path.join(os.homedir(), ".cursor", "mcp.json");
542
+ const configPath = resolvePath(override || defaultPath);
543
+ return {
544
+ kind: "json",
545
+ label: "Cursor",
546
+ path: configPath,
547
+ parentDir: path.dirname(configPath),
548
+ };
549
+ }
550
+ if (target === "codex") {
551
+ const override = process.env.VIVEWORKER_MCP_CODEX_CONFIG_FILE || "";
552
+ const defaultPath = path.join(os.homedir(), ".codex", "config.toml");
553
+ const configPath = resolvePath(override || defaultPath);
554
+ return {
555
+ kind: "toml",
556
+ label: "Codex",
557
+ path: configPath,
558
+ parentDir: path.dirname(configPath),
559
+ };
560
+ }
561
+ return null;
562
+ }
563
+
564
+ function upsertJsonMcpConfig(currentText, serverConfig) {
565
+ let data = {};
566
+ if (currentText.trim()) {
567
+ try {
568
+ data = JSON.parse(currentText);
569
+ } catch (error) {
570
+ throw new Error(`Unable to parse existing MCP JSON config: ${error.message}`);
571
+ }
572
+ }
573
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
574
+ data = {};
575
+ }
576
+ const previous = JSON.stringify(data);
577
+ const mcpServers = data.mcpServers && typeof data.mcpServers === "object" && !Array.isArray(data.mcpServers)
578
+ ? data.mcpServers
579
+ : {};
580
+ data.mcpServers = {
581
+ ...mcpServers,
582
+ viveworker: serverConfig,
583
+ };
584
+ const text = `${JSON.stringify(data, null, 2)}\n`;
585
+ return {
586
+ changed: JSON.stringify(data) !== previous,
587
+ text,
588
+ };
589
+ }
590
+
591
+ function upsertCodexMcpConfig(currentText, serverConfig) {
592
+ const block = [
593
+ "[mcp_servers.viveworker]",
594
+ `command = ${tomlString(serverConfig.command)}`,
595
+ `args = [${serverConfig.args.map((arg) => tomlString(arg)).join(", ")}]`,
596
+ "",
597
+ ].join("\n");
598
+ const withoutExisting = removeTomlTable(currentText, "mcp_servers.viveworker").replace(/\s+$/u, "");
599
+ const text = `${withoutExisting ? `${withoutExisting}\n\n` : ""}${block}`;
600
+ return {
601
+ changed: normalizeConfigText(text) !== normalizeConfigText(currentText),
602
+ text,
603
+ };
604
+ }
605
+
606
+ function removeTomlTable(text, tableName) {
607
+ const lines = String(text || "").split(/\r?\n/u);
608
+ const out = [];
609
+ let skipping = false;
610
+ const header = `[${tableName}]`;
611
+ for (const line of lines) {
612
+ const trimmed = line.trim();
613
+ if (trimmed === header) {
614
+ skipping = true;
615
+ continue;
616
+ }
617
+ if (skipping && /^\[[^\]]+\]\s*$/u.test(trimmed)) {
618
+ skipping = false;
619
+ }
620
+ if (!skipping) {
621
+ out.push(line);
622
+ }
623
+ }
624
+ return out.join("\n");
625
+ }
626
+
627
+ function tomlString(value) {
628
+ return JSON.stringify(String(value || ""));
629
+ }
630
+
631
+ function normalizeConfigText(value) {
632
+ return String(value || "").replace(/\r\n/gu, "\n").trim();
633
+ }
634
+
635
+ async function readOptionalText(filePath) {
636
+ try {
637
+ return await fs.readFile(filePath, "utf8");
638
+ } catch (error) {
639
+ if (error?.code === "ENOENT") {
640
+ return "";
641
+ }
642
+ throw error;
643
+ }
644
+ }
645
+
646
+ async function confirmCli(prompt) {
647
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
648
+ throw new Error("Refusing to write MCP config without an interactive terminal. Re-run with --yes or --dry-run.");
649
+ }
650
+ const rl = createReadlineInterface({
651
+ input: process.stdin,
652
+ output: process.stdout,
653
+ });
654
+ try {
655
+ const answer = await rl.question(prompt);
656
+ return /^y(es)?$/iu.test(String(answer || "").trim());
657
+ } finally {
658
+ rl.close();
659
+ }
660
+ }
661
+
414
662
  async function runEnableClaude(cliOptions) {
415
663
  const setup = await loadExistingSetup(cliOptions);
416
664
  const settingsFile = resolvePath(
@@ -1397,6 +1645,9 @@ function parseArgs(argv) {
1397
1645
  autoScoutHarness: "auto",
1398
1646
  autoScoutSubmolts: "",
1399
1647
  autoScoutMaxDaily: 0,
1648
+ mcpTarget: "",
1649
+ mcpDryRun: false,
1650
+ yes: false,
1400
1651
  };
1401
1652
 
1402
1653
  if (argv[0] && !argv[0].startsWith("-")) {
@@ -1552,6 +1803,13 @@ function parseArgs(argv) {
1552
1803
  } else if (arg === "--auto-scout-max-daily") {
1553
1804
  parsed.autoScoutMaxDaily = Number(next) || 0;
1554
1805
  index += 1;
1806
+ } else if (arg === "--target") {
1807
+ parsed.mcpTarget = next;
1808
+ index += 1;
1809
+ } else if (arg === "--dry-run") {
1810
+ parsed.mcpDryRun = true;
1811
+ } else if (arg === "--yes" || arg === "-y") {
1812
+ parsed.yes = true;
1555
1813
  } else if (arg === "--help" || arg === "-h") {
1556
1814
  parsed.command = "help";
1557
1815
  } else {
@@ -1579,6 +1837,8 @@ ${t(locale, "cli.help.commands")}
1579
1837
  ${t(locale, "cli.help.status")}
1580
1838
  ${t(locale, "cli.help.doctor")}
1581
1839
  ${t(locale, "cli.help.update")}
1840
+ mcp Start the stdio MCP server
1841
+ mcp config Print MCP client config snippets
1582
1842
 
1583
1843
  ${t(locale, "cli.help.commonOptions")}
1584
1844
  --port <n>
@@ -1607,6 +1867,7 @@ ${t(locale, "cli.help.featureOptions")}
1607
1867
  ${t(locale, "cli.help.enableA2a")}
1608
1868
  ${t(locale, "cli.help.enableMoltbook")}
1609
1869
  ${t(locale, "cli.help.enableScout")}
1870
+ enable mcp --target <claude|cursor|codex|all> [--dry-run|--yes]
1610
1871
  ${t(locale, "cli.help.doctorFix")}
1611
1872
  `);
1612
1873
  }
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: viveworker-control-plane
3
+ description: Route AI agent operations through the user's viveworker mobile control plane. Use when Codex has access to the viveworker MCP tools and needs to notify the phone, ask the user a question, request human approval, share a workspace deliverable through File Share, hand off context to another Codex/Claude/inbox thread, or delegate a bounded task to a registered A2A target. Especially use for externally visible, irreversible, sensitive, or cross-agent actions.
4
+ ---
5
+
6
+ # viveworker Control Plane
7
+
8
+ Use viveworker as the mobile control surface around agent work. Keep normal local work moving, but route decisions, confirmations, handoffs, deliverables, and delegation through the paired phone when that improves safety or flow.
9
+
10
+ ## First Check
11
+
12
+ - If tool availability is uncertain, call `viveworker_status` first.
13
+ - If viveworker MCP is unavailable, do not pretend the action happened. Tell the user to run `npx viveworker enable mcp --target codex` or add the config from `npx viveworker mcp config`.
14
+ - Do not use viveworker for every routine local read/edit. Use it when a human decision, mobile notification, external handoff, or durable record is useful.
15
+
16
+ ## First-Run Onboarding
17
+
18
+ Use this flow when the user asks to set up viveworker, pair a phone, install the control plane, or "make viveworker work here".
19
+
20
+ 1. Check whether MCP tools are available.
21
+ - If available, call `viveworker_status` first and use the result to decide what is missing.
22
+ - If unavailable, explain that the MCP entry is not loaded yet and guide the user through `npx viveworker enable mcp --target codex`, then ask them to restart Codex.
23
+ 2. If the bridge is not running or no local setup exists, ask for confirmation before running or recommending `npx viveworker setup`.
24
+ - Do not auto-run setup silently. It can install local certificates, write config, register launchd services, and change agent hooks.
25
+ - If the user explicitly asks you to execute setup, use the normal shell tool with clear confirmation, not MCP.
26
+ 3. If setup exists but no phone is trusted, guide the user to run `npx viveworker pair` and open the pairing URL on the phone.
27
+ 4. Ask the user to allow notifications from the Home Screen app when prompted.
28
+ 5. Re-check with `viveworker_status`.
29
+ 6. Send a final smoke notification with `viveworker_notify`.
30
+ 7. Summarize what is ready: bridge, pairing, notifications, Remote connection, File Share, A2A, and Moltbook where applicable.
31
+
32
+ Keep onboarding calm and step-by-step. Do not make the user debug config files unless the status output points there.
33
+
34
+ ## Tool Selection
35
+
36
+ - Use `viveworker_notify` for informational milestones that should appear on the phone or timeline, such as "build finished", "review ready", or "agent is blocked".
37
+ - Use `viveworker_ask` when the next step depends on a human preference, missing requirement, or choice that should not be guessed.
38
+ - Use `viveworker_request_approval` before actions that are externally visible, hard to undo, risky, sensitive, delegated, paid, or likely to surprise the user.
39
+ - Use `viveworker_share_file` when the user wants a workspace deliverable shared as a limited File Share URL. Good fits include `.html`, `.htm`, `.pdf`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, and `.csv`.
40
+ - Use `viveworker_thread_share` when context should move to another Codex session, Claude session, or viveworker inbox while preserving a human-visible handoff.
41
+ - Use `viveworker_send_a2a_task` only for registered A2A target aliases and bounded tasks with clear acceptance criteria.
42
+
43
+ ## Approval And Question Style
44
+
45
+ - Keep phone prompts short and concrete.
46
+ - Include the action, expected result, and specific risk.
47
+ - Include relevant file refs, but never include secrets, tokens, private keys, or unnecessary file contents.
48
+ - Treat timeout, rejection, or transport failure as "not approved". Do not continue as if approval was granted.
49
+ - After the tool returns, summarize the decision or returned artifact to the user.
50
+
51
+ ## File Share Rules
52
+
53
+ - Share only files inside the workspace root.
54
+ - Do not share `.env`, credential files, private keys, `.ssh`, `.aws`, `.gnupg`, or secret-looking paths.
55
+ - Prefer sharing final deliverables, not internal source files, unless the user explicitly asks.
56
+ - For unsupported file types, explain the accepted types and suggest exporting to HTML, PDF, image, or CSV first.
57
+ - Do not use File Share to bypass a user's explicit request not to upload or publish content.
58
+
59
+ ## Thread Share Rules
60
+
61
+ - Use `targetConversationId` when the user names a specific thread.
62
+ - Use `targetTool` when the user says "send this to Codex", "share with Claude", or "put this in the inbox" without a specific thread ID.
63
+ - Send a compact handoff: current goal, decisions made, blockers, and concrete next action.
64
+ - Attach `contextFiles` only when they help the receiver act.
65
+
66
+ ## A2A Delegation Rules
67
+
68
+ - Use only aliases from `~/.viveworker/a2a-targets.json`; never inline API keys in tool arguments.
69
+ - Keep delegated tasks self-contained and limited in scope.
70
+ - Include success criteria, constraints, and any deadline or budget.
71
+ - Ask for approval before sending, even if the task looks harmless.
72
+ - Do not send secrets, private repo data, or local paths unless the user clearly approves that exposure.
73
+
74
+ ## Examples
75
+
76
+ Ask before a risky action:
77
+
78
+ ```json
79
+ {
80
+ "title": "Approve release publish",
81
+ "message": "Publish viveworker 0.8.0 to npm. Risk: this makes the package public and hard to fully undo.",
82
+ "approvalKind": "release"
83
+ }
84
+ ```
85
+
86
+ Share a deliverable:
87
+
88
+ ```json
89
+ {
90
+ "path": "/workspace/report.html",
91
+ "workspaceRoot": "/workspace",
92
+ "expiresDays": "3"
93
+ }
94
+ ```
95
+
96
+ Delegate through A2A:
97
+
98
+ ```json
99
+ {
100
+ "target": "reviewer",
101
+ "instruction": "Review the remote connection security notes and return only concrete risks with file references.",
102
+ "metadata": { "scope": "security-review" }
103
+ }
104
+ ```
@@ -0,0 +1,12 @@
1
+ interface:
2
+ display_name: "viveworker Control Plane"
3
+ short_description: "Route AI agent decisions through viveworker"
4
+ default_prompt: "Use $viveworker-control-plane to route approvals, questions, file shares, thread handoffs, and A2A delegation through viveworker."
5
+ dependencies:
6
+ tools:
7
+ - type: "mcp"
8
+ value: "viveworker"
9
+ description: "Local viveworker stdio MCP server"
10
+ transport: "stdio"
11
+ policy:
12
+ allow_implicit_invocation: true
@@ -0,0 +1,67 @@
1
+ # viveworker MCP guide for Claude
2
+
3
+ Use viveworker as the mobile control plane for this Claude session when the user wants phone-based questions, approvals, file sharing, thread sharing, or A2A delegation.
4
+
5
+ ## Setup
6
+
7
+ For Claude Desktop, the user should install the MCP entry once:
8
+
9
+ ```bash
10
+ npx viveworker enable mcp --target claude
11
+ ```
12
+
13
+ Then restart Claude Desktop.
14
+
15
+ For Claude Code, the MCP config is separate from Claude Desktop. Install it once with:
16
+
17
+ ```bash
18
+ claude mcp add --scope user viveworker -- npx viveworker mcp
19
+ ```
20
+
21
+ Then restart the Claude Code session. If the tools are not available, ask the user to run the setup command for the Claude surface they are using.
22
+
23
+ ## Tools
24
+
25
+ - `viveworker_status` checks bridge, pairing, Remote connection, A2A, File Share, and Moltbook state.
26
+ - `viveworker_notify` sends an informational phone notification and records a timeline entry.
27
+ - `viveworker_ask` asks the paired phone a question and waits for the answer.
28
+ - `viveworker_request_approval` asks the phone to approve or reject a proposed action.
29
+ - `viveworker_share_file` uploads a workspace file to File Share after phone approval.
30
+ - `viveworker_thread_share` shares context into another Codex / Claude / inbox thread.
31
+ - `viveworker_send_a2a_task` sends a task to a registered A2A target after phone approval.
32
+
33
+ ## When to use viveworker
34
+
35
+ - If the user says "ask me on my phone", "スマホに聞いて", or a short decision blocks progress, use `viveworker_ask`.
36
+ - If the user asks you to proceed with a risky, external, irreversible, payment-related, or user-visible action, use `viveworker_request_approval`.
37
+ - If the user asks for a report, prototype, screenshot, PDF, CSV, or standalone HTML to become a shareable link, use `viveworker_share_file`.
38
+ - If the user says "share this with Codex/Claude", "Aの内容をBに共有して", or wants context handed to another session, use `viveworker_thread_share`.
39
+ - If the user wants another registered agent to do work, use `viveworker_send_a2a_task`.
40
+
41
+ ## Prompting rules
42
+
43
+ - Keep phone prompts short and concrete.
44
+ - Include the action, why it matters, expected outcome, and clear choices.
45
+ - Do not send secrets, private keys, `.env` content, credentials, or unnecessary file contents.
46
+ - Treat timeout, rejection, or missing response as not approved.
47
+ - Do not spam the phone for routine local reads or low-risk edits unless the user asked for mobile confirmation.
48
+
49
+ ## Thread sharing
50
+
51
+ Thread Share is useful for handoffs such as:
52
+
53
+ - "Tell Codex what we just decided."
54
+ - "Share this Claude result with the Codex thread."
55
+ - "Aの内容をBに共有して。"
56
+
57
+ Send a compact summary with the decision, relevant context, and the exact next action expected from the recipient.
58
+
59
+ Claude may not automatically notice newly shared messages from another session. If the user says something arrived or asks you to pull shared context, inspect the viveworker timeline/inbox surface available in the current environment, or ask the user to open the relevant shared item.
60
+
61
+ ## Safety defaults
62
+
63
+ - MCP is a control-plane surface, not a shell executor.
64
+ - Do not run shell strings through MCP.
65
+ - File Share and A2A task sending require phone approval.
66
+ - Use registered A2A aliases only; do not ask the user to paste API keys into prompts.
67
+ - Prefer `viveworker_status` before troubleshooting a missing notification, stale pairing, or remote connectivity issue.
package/web/app.css CHANGED
@@ -77,6 +77,20 @@ code {
77
77
  background: rgba(156, 181, 197, 0.12);
78
78
  }
79
79
 
80
+ /*
81
+ * Reset the inline-`<code>` look when the element is wrapped by `<pre>`.
82
+ * Without this, a multi-line code block ends up with the per-`<code>`
83
+ * padding + rounded background applied to each visual line individually
84
+ * (the inline element is line-broken and the browser paints the background
85
+ * once per line), which made fenced code blocks look like a stack of
86
+ * inline code spans on the phone PWA.
87
+ */
88
+ pre code {
89
+ padding: 0;
90
+ border-radius: 0;
91
+ background: transparent;
92
+ }
93
+
80
94
  #app {
81
95
  width: 100vw;
82
96
  max-width: 100vw;
@@ -2904,6 +2918,41 @@ code {
2904
2918
  max-width: 100%;
2905
2919
  display: block;
2906
2920
  overflow-x: auto;
2921
+ border-collapse: collapse;
2922
+ margin: 0.4rem 0 0.6rem;
2923
+ font-size: 0.92rem;
2924
+ line-height: 1.45;
2925
+ }
2926
+
2927
+ .detail-body.markdown table thead {
2928
+ background: rgba(255, 255, 255, 0.03);
2929
+ }
2930
+
2931
+ .detail-body.markdown table th,
2932
+ .detail-body.markdown table td {
2933
+ border: 1px solid rgba(156, 181, 197, 0.16);
2934
+ padding: 0.42rem 0.62rem;
2935
+ text-align: left;
2936
+ vertical-align: top;
2937
+ white-space: normal;
2938
+ word-break: break-word;
2939
+ }
2940
+
2941
+ .detail-body.markdown table th {
2942
+ font-weight: 600;
2943
+ color: var(--text);
2944
+ }
2945
+
2946
+ .detail-body.markdown table td {
2947
+ color: var(--muted);
2948
+ }
2949
+
2950
+ .detail-body.markdown table th code,
2951
+ .detail-body.markdown table td code {
2952
+ background: rgba(255, 255, 255, 0.05);
2953
+ padding: 0.05rem 0.32rem;
2954
+ border-radius: 6px;
2955
+ font-size: 0.86em;
2907
2956
  }
2908
2957
 
2909
2958
  .detail-card {
package/web/app.js CHANGED
@@ -5,10 +5,10 @@ import {
5
5
  savePairingState as saveRemotePairingState,
6
6
  clearPairingState as clearRemotePairingState,
7
7
  } from "./remote-pairing/pairing-state.js";
8
- import { getRoutingTelemetry, routedFetch } from "./remote-pairing/api-router.js?v=20260428-remote-token-refresh";
8
+ const APP_BUILD_ID = "__VIVEWORKER_APP_BUILD_ID__";
9
+ const { getRoutingTelemetry, routedFetch } = await import(`./remote-pairing/api-router.js?v=${encodeURIComponent(APP_BUILD_ID)}`);
9
10
 
10
11
  const DESKTOP_BREAKPOINT = 980;
11
- const APP_BUILD_ID = "20260428-remote-token-refresh";
12
12
  const INSTALL_BANNER_DISMISS_KEY = "viveworker-install-banner-dismissed-v2";
13
13
  const PUSH_BANNER_DISMISS_KEY = "viveworker-push-banner-dismissed-v1";
14
14
  const INITIAL_DETECTED_LOCALE = detectBrowserLocale();
@@ -1425,6 +1425,7 @@ function normalizeProviderClient(value) {
1425
1425
  if (normalized === "moltbook") return "moltbook";
1426
1426
  if (normalized === "a2a") return "a2a";
1427
1427
  if (normalized === "viveworker") return "viveworker";
1428
+ if (normalized === "mcp") return "mcp";
1428
1429
  return "codex";
1429
1430
  }
1430
1431
 
@@ -1434,6 +1435,7 @@ function providerDisplayName(provider) {
1434
1435
  if (p === "moltbook") return "Moltbook";
1435
1436
  if (p === "a2a") return "A2A";
1436
1437
  if (p === "viveworker") return L("common.appName");
1438
+ if (p === "mcp") return "MCP";
1437
1439
  return L("common.codex");
1438
1440
  }
1439
1441
 
@@ -1894,6 +1896,47 @@ async function revokeTrustedDevice(deviceId) {
1894
1896
  await renderShell();
1895
1897
  }
1896
1898
 
1899
+ /**
1900
+ * `renderShell()` rebuilds the entire `#app` subtree by reassigning
1901
+ * `innerHTML`, which destroys every <pre> element in the DOM — including
1902
+ * any horizontal scroll position the user dragged into a wide code block.
1903
+ * On a polling interval, that means a long line of code keeps snapping
1904
+ * back to the start while the reader is mid-line.
1905
+ *
1906
+ * `snapshotCodeBlockScrolls()` records each `.markdown pre`'s scrollLeft
1907
+ * keyed by its trimmed textContent (so the key survives a fresh render
1908
+ * regardless of position in the DOM tree). `restoreCodeBlockScrolls()`
1909
+ * walks the new DOM and restores any scrollLeft we still have a key for.
1910
+ *
1911
+ * Content-keyed matching is intentional: if the underlying code text
1912
+ * changes mid-scroll, the new <pre> is logically different and we let it
1913
+ * start at scrollLeft=0 rather than landing the reader somewhere unrelated.
1914
+ */
1915
+ function snapshotCodeBlockScrolls() {
1916
+ if (typeof document === "undefined") return null;
1917
+ const blocks = document.querySelectorAll(".markdown pre");
1918
+ if (blocks.length === 0) return null;
1919
+ const map = new Map();
1920
+ for (const pre of blocks) {
1921
+ const key = pre.textContent ? pre.textContent.trim() : "";
1922
+ if (!key) continue;
1923
+ if (pre.scrollLeft === 0 && pre.scrollTop === 0) continue;
1924
+ map.set(key, { scrollLeft: pre.scrollLeft, scrollTop: pre.scrollTop });
1925
+ }
1926
+ return map.size > 0 ? map : null;
1927
+ }
1928
+
1929
+ function restoreCodeBlockScrolls(snapshot) {
1930
+ if (!snapshot || typeof document === "undefined") return;
1931
+ for (const pre of document.querySelectorAll(".markdown pre")) {
1932
+ const key = pre.textContent ? pre.textContent.trim() : "";
1933
+ const saved = key ? snapshot.get(key) : null;
1934
+ if (!saved) continue;
1935
+ if (saved.scrollLeft) pre.scrollLeft = saved.scrollLeft;
1936
+ if (saved.scrollTop) pre.scrollTop = saved.scrollTop;
1937
+ }
1938
+ }
1939
+
1897
1940
  async function renderShell() {
1898
1941
  syncVisualViewportMetrics();
1899
1942
  const desktop = isDesktopLayout();
@@ -1914,6 +1957,8 @@ async function renderShell() {
1914
1957
  .filter(Boolean)
1915
1958
  .join(" ");
1916
1959
 
1960
+ const codeBlockScrollSnapshot = snapshotCodeBlockScrolls();
1961
+
1917
1962
  app.innerHTML = `
1918
1963
  <div class="${shellClassName}">
1919
1964
  ${desktop ? renderDesktopHeader(detail) : renderMobileTopBar(detail)}
@@ -1939,6 +1984,10 @@ async function renderShell() {
1939
1984
  applyPendingListScrollRestore();
1940
1985
  applyPendingSettingsSubpageScrollReset();
1941
1986
  applyPendingSettingsScrollRestore();
1987
+ // Reapply any horizontal scroll the user dragged into a code block before
1988
+ // this re-render. Done after the imperative scroll resets above so they
1989
+ // can't fight each other.
1990
+ restoreCodeBlockScrolls(codeBlockScrollSnapshot);
1942
1991
  requestAnimationFrame(dismissBootSplash);
1943
1992
  }
1944
1993
 
@@ -0,0 +1 @@
1
+ export const APP_BUILD_ID = "20260430-mcp-control-plane";
Binary file
Binary file
Binary file