@hellcoder/companion 0.116.0 → 0.116.1-preview.20260814041931.d191f06

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.
Files changed (49) hide show
  1. package/dist/assets/{AgentsPage-BBi92OuO.js → AgentsPage-BAalV1v6.js} +5 -5
  2. package/dist/assets/CronManager-B2Vo1YmB.js +1 -0
  3. package/dist/assets/{DashboardPage-BvuDoRlp.js → DashboardPage-BOvDiP1W.js} +1 -1
  4. package/dist/assets/{IntegrationsPage-CK_ThzWg.js → IntegrationsPage-kOXLH5KU.js} +1 -1
  5. package/dist/assets/{LinearOAuthSettingsPage-InTf-E7u.js → LinearOAuthSettingsPage-H-uUJ-ub.js} +1 -1
  6. package/dist/assets/{LinearSettingsPage-BwjC9Vd-.js → LinearSettingsPage-CD_x4AGr.js} +1 -1
  7. package/dist/assets/MagicUIDashboard-9Jgiv4rb.js +1119 -0
  8. package/dist/assets/MagicUIView-BLEcS9XO.js +1 -0
  9. package/dist/assets/{Playground-DlzomEOu.js → Playground-QLrd9q16.js} +10 -9
  10. package/dist/assets/{PromptsPage-hRZhLUYR.js → PromptsPage-11eJ6ak7.js} +1 -1
  11. package/dist/assets/{RunsPage-zsVmbFoi.js → RunsPage-Bz3udIL2.js} +1 -1
  12. package/dist/assets/{SandboxManager-Bq_aXA1z.js → SandboxManager-Dv47pbC6.js} +1 -1
  13. package/dist/assets/SettingsPage-C_2ftG-C.js +1 -0
  14. package/dist/assets/{TailscalePage-fE7Z9uiF.js → TailscalePage-DzMX_7XA.js} +1 -1
  15. package/dist/assets/index-DSCRqqpK.css +1 -0
  16. package/dist/assets/index-DspbwgSL.js +140 -0
  17. package/dist/assets/{sw-register-Cjt2BQTW.js → sw-register-CarYQEeI.js} +1 -1
  18. package/dist/index.html +2 -2
  19. package/dist/sw.js +1 -1
  20. package/package.json +4 -1
  21. package/server/async-message-queue.ts +38 -0
  22. package/server/auto-namer.test.ts +8 -0
  23. package/server/claude-sdk-adapter.ts +1 -36
  24. package/server/event-bus-types.ts +32 -1
  25. package/server/index.ts +7 -0
  26. package/server/linear-agent-bridge.ts +6 -72
  27. package/server/linear-connections.test.ts +8 -0
  28. package/server/magic-ui-ops.test.ts +239 -0
  29. package/server/magic-ui-ops.ts +365 -0
  30. package/server/magic-ui-settings.test.ts +56 -0
  31. package/server/magic-ui-settings.ts +38 -0
  32. package/server/magic-ui-store.ts +94 -0
  33. package/server/magic-ui-types.ts +138 -0
  34. package/server/magic-ui-watcher.test.ts +288 -0
  35. package/server/magic-ui-watcher.ts +623 -0
  36. package/server/message-extract.ts +79 -0
  37. package/server/routes/settings-routes.ts +22 -0
  38. package/server/routes.test.ts +59 -0
  39. package/server/session-types.ts +15 -1
  40. package/server/settings-manager.test.ts +6 -0
  41. package/server/settings-manager.ts +24 -1
  42. package/server/ws-bridge-browser-ingest.ts +1 -0
  43. package/server/ws-bridge-codex.test.ts +12 -0
  44. package/server/ws-bridge-controls.ts +9 -0
  45. package/server/ws-bridge.ts +65 -0
  46. package/dist/assets/CronManager-DxLaNh9T.js +0 -1
  47. package/dist/assets/SettingsPage-BvrrlEKj.js +0 -1
  48. package/dist/assets/index--SnXduR0.js +0 -139
  49. package/dist/assets/index-DcQxHfE7.css +0 -1
@@ -0,0 +1,79 @@
1
+ // ─── Shared BrowserIncomingMessage extraction helpers ────────────────────────
2
+ //
3
+ // Pure functions for pulling text / tool activity out of the bridge's
4
+ // message stream. Used by consumers that tap companionBus message events
5
+ // (linear-agent-bridge, magic-ui-watcher).
6
+
7
+ import type { BrowserIncomingMessage } from "./session-types.js";
8
+
9
+ /** Content blocks of an assistant message, or null. */
10
+ export function getAssistantContent(msg: BrowserIncomingMessage): unknown[] | null {
11
+ if (msg.type !== "assistant") return null;
12
+ // Assistant messages carry content blocks at msg.message.content
13
+ const raw = msg as Record<string, unknown>;
14
+ const message = raw.message;
15
+ if (!message || typeof message !== "object") return null;
16
+ const content = (message as Record<string, unknown>).content;
17
+ return Array.isArray(content) ? content : null;
18
+ }
19
+
20
+ /** Extract text from assistant message content blocks */
21
+ export function extractTextFromAssistant(msg: BrowserIncomingMessage): string {
22
+ const content = getAssistantContent(msg);
23
+ if (!content) return "";
24
+ return content
25
+ .filter((b): b is { type: string; text: string } =>
26
+ typeof b === "object" && b !== null && (b as Record<string, unknown>).type === "text" && typeof (b as Record<string, unknown>).text === "string")
27
+ .map((b) => b.text)
28
+ .join("\n");
29
+ }
30
+
31
+ /** Extract text deltas from stream events. */
32
+ export function extractTextDeltaFromStreamEvent(msg: BrowserIncomingMessage): string {
33
+ if (msg.type !== "stream_event") return "";
34
+ const event = msg.event as Record<string, unknown> | undefined;
35
+ if (!event || event.type !== "content_block_delta") return "";
36
+ const delta = event.delta as Record<string, unknown> | undefined;
37
+ if (!delta || delta.type !== "text_delta" || typeof delta.text !== "string") return "";
38
+ return delta.text;
39
+ }
40
+
41
+ /** Extract all tool use blocks from assistant message content (with raw input for plan extraction) */
42
+ export function extractToolUses(msg: BrowserIncomingMessage): Array<{ id?: string; name: string; input: string; rawInput?: Record<string, unknown> }> {
43
+ const content = getAssistantContent(msg);
44
+ if (!content) return [];
45
+ return content
46
+ .filter((b): b is { type: string; id?: string; name: string; input?: Record<string, unknown> } =>
47
+ typeof b === "object" && b !== null
48
+ && (b as Record<string, unknown>).type === "tool_use"
49
+ && typeof (b as Record<string, unknown>).name === "string")
50
+ .map((toolBlock) => ({
51
+ id: typeof toolBlock.id === "string" ? toolBlock.id : undefined,
52
+ name: toolBlock.name,
53
+ input: toolBlock.input ? JSON.stringify(toolBlock.input).slice(0, 200) : "",
54
+ rawInput: toolBlock.input,
55
+ }));
56
+ }
57
+
58
+ /** Extract tool_result blocks from assistant message content. */
59
+ export function extractToolResults(msg: BrowserIncomingMessage): Array<{ tool_use_id: string; content: string }> {
60
+ const content = getAssistantContent(msg);
61
+ if (!content) return [];
62
+ return content
63
+ .filter((b): b is { type: string; tool_use_id: string; content?: unknown } =>
64
+ typeof b === "object" && b !== null
65
+ && (b as Record<string, unknown>).type === "tool_result"
66
+ && typeof (b as Record<string, unknown>).tool_use_id === "string")
67
+ .map((block) => ({
68
+ tool_use_id: block.tool_use_id,
69
+ content: typeof block.content === "string"
70
+ ? block.content.slice(0, 500)
71
+ : Array.isArray(block.content)
72
+ ? (block.content as Array<{ type?: string; text?: string }>)
73
+ .filter((c) => c.type === "text" && typeof c.text === "string")
74
+ .map((c) => c.text)
75
+ .join("\n")
76
+ .slice(0, 500)
77
+ : "",
78
+ }));
79
+ }
@@ -3,6 +3,7 @@ import { DASHBOARD_MODEL_OPTIONS, DEFAULT_ANTHROPIC_MODEL, getSettings, updateSe
3
3
  import { linearCache } from "../linear-cache.js";
4
4
  import { listConnections } from "../linear-connections.js";
5
5
  import { hasContainerCodexAuth } from "../codex-container-auth.js";
6
+ import { isClaudeCliAvailable } from "../claude-cli-runner.js";
6
7
 
7
8
  export function registerSettingsRoutes(api: Hono): void {
8
9
  api.get("/settings", (c) => {
@@ -30,6 +31,9 @@ export function registerSettingsRoutes(api: Hono): void {
30
31
  dashboardModel: settings.dashboardModel,
31
32
  dashboardRunHour: settings.dashboardRunHour,
32
33
  dashboardMaxSessionsPerRun: settings.dashboardMaxSessionsPerRun,
34
+ magicUiEnabled: settings.magicUiEnabled,
35
+ magicUiModel: settings.magicUiModel,
36
+ claudeCliAvailable: isClaudeCliAvailable(),
33
37
  publicUrl: settings.publicUrl,
34
38
  updateChannel: settings.updateChannel,
35
39
  dockerAutoUpdate: settings.dockerAutoUpdate,
@@ -100,6 +104,12 @@ export function registerSettingsRoutes(api: Hono): void {
100
104
  ) {
101
105
  return c.json({ error: "dashboardMaxSessionsPerRun must be an integer between 1 and 200" }, 400);
102
106
  }
107
+ if (body.magicUiEnabled !== undefined && typeof body.magicUiEnabled !== "boolean") {
108
+ return c.json({ error: "magicUiEnabled must be a boolean" }, 400);
109
+ }
110
+ if (body.magicUiModel !== undefined && !DASHBOARD_MODEL_OPTIONS.includes(body.magicUiModel)) {
111
+ return c.json({ error: `magicUiModel must be one of: ${DASHBOARD_MODEL_OPTIONS.join(", ")}` }, 400);
112
+ }
103
113
  if (body.publicUrl !== undefined) {
104
114
  if (typeof body.publicUrl !== "string") {
105
115
  return c.json({ error: "publicUrl must be a string" }, 400);
@@ -164,6 +174,7 @@ export function registerSettingsRoutes(api: Hono): void {
164
174
  || body.aiValidationAutoDeny !== undefined
165
175
  || body.dashboardEnabled !== undefined || body.dashboardModel !== undefined
166
176
  || body.dashboardRunHour !== undefined || body.dashboardMaxSessionsPerRun !== undefined
177
+ || body.magicUiEnabled !== undefined || body.magicUiModel !== undefined
167
178
  || body.publicUrl !== undefined
168
179
  || body.updateChannel !== undefined
169
180
  || body.dockerAutoUpdate !== undefined
@@ -270,6 +281,14 @@ export function registerSettingsRoutes(api: Hono): void {
270
281
  typeof body.dashboardMaxSessionsPerRun === "number"
271
282
  ? body.dashboardMaxSessionsPerRun
272
283
  : undefined,
284
+ magicUiEnabled:
285
+ typeof body.magicUiEnabled === "boolean"
286
+ ? body.magicUiEnabled
287
+ : undefined,
288
+ magicUiModel:
289
+ typeof body.magicUiModel === "string"
290
+ ? body.magicUiModel
291
+ : undefined,
273
292
  publicUrl:
274
293
  typeof body.publicUrl === "string"
275
294
  ? body.publicUrl.trim().replace(/\/+$/, "")
@@ -331,6 +350,9 @@ export function registerSettingsRoutes(api: Hono): void {
331
350
  dashboardModel: settings.dashboardModel,
332
351
  dashboardRunHour: settings.dashboardRunHour,
333
352
  dashboardMaxSessionsPerRun: settings.dashboardMaxSessionsPerRun,
353
+ magicUiEnabled: settings.magicUiEnabled,
354
+ magicUiModel: settings.magicUiModel,
355
+ claudeCliAvailable: isClaudeCliAvailable(),
334
356
  publicUrl: settings.publicUrl,
335
357
  updateChannel: settings.updateChannel,
336
358
  dockerAutoUpdate: settings.dockerAutoUpdate,
@@ -106,6 +106,8 @@ vi.mock("./settings-manager.js", () => ({
106
106
  dashboardModel: "claude-haiku-4-5",
107
107
  dashboardRunHour: 3,
108
108
  dashboardMaxSessionsPerRun: 30,
109
+ magicUiEnabled: false,
110
+ magicUiModel: "claude-haiku-4-5",
109
111
  publicUrl: "",
110
112
  updateChannel: "stable",
111
113
  dockerAutoUpdate: false,
@@ -1154,6 +1156,8 @@ describe("GET /api/sessions/:id/archive-info", () => {
1154
1156
  dashboardModel: "claude-haiku-4-5",
1155
1157
  dashboardRunHour: 3,
1156
1158
  dashboardMaxSessionsPerRun: 30,
1159
+ magicUiEnabled: false,
1160
+ magicUiModel: "claude-haiku-4-5",
1157
1161
  publicUrl: "",
1158
1162
  updateChannel: "stable",
1159
1163
  dockerAutoUpdate: false,
@@ -1533,6 +1537,8 @@ describe("GET /api/settings", () => {
1533
1537
  dashboardModel: "claude-haiku-4-5",
1534
1538
  dashboardRunHour: 3,
1535
1539
  dashboardMaxSessionsPerRun: 30,
1540
+ magicUiEnabled: false,
1541
+ magicUiModel: "claude-haiku-4-5",
1536
1542
  publicUrl: "",
1537
1543
  updateChannel: "stable",
1538
1544
  dockerAutoUpdate: false,
@@ -1570,6 +1576,9 @@ describe("GET /api/settings", () => {
1570
1576
  dashboardModel: "claude-haiku-4-5",
1571
1577
  dashboardRunHour: 3,
1572
1578
  dashboardMaxSessionsPerRun: 30,
1579
+ magicUiEnabled: false,
1580
+ magicUiModel: "claude-haiku-4-5",
1581
+ claudeCliAvailable: false,
1573
1582
  publicUrl: "",
1574
1583
  updateChannel: "stable",
1575
1584
  dockerAutoUpdate: false,
@@ -1607,6 +1616,8 @@ describe("GET /api/settings", () => {
1607
1616
  dashboardModel: "claude-haiku-4-5",
1608
1617
  dashboardRunHour: 3,
1609
1618
  dashboardMaxSessionsPerRun: 30,
1619
+ magicUiEnabled: false,
1620
+ magicUiModel: "claude-haiku-4-5",
1610
1621
  publicUrl: "",
1611
1622
  updateChannel: "stable",
1612
1623
  dockerAutoUpdate: false,
@@ -1644,6 +1655,9 @@ describe("GET /api/settings", () => {
1644
1655
  dashboardModel: "claude-haiku-4-5",
1645
1656
  dashboardRunHour: 3,
1646
1657
  dashboardMaxSessionsPerRun: 30,
1658
+ magicUiEnabled: false,
1659
+ magicUiModel: "claude-haiku-4-5",
1660
+ claudeCliAvailable: false,
1647
1661
  publicUrl: "",
1648
1662
  updateChannel: "stable",
1649
1663
  dockerAutoUpdate: false,
@@ -1682,6 +1696,8 @@ describe("GET /api/settings", () => {
1682
1696
  dashboardModel: "claude-haiku-4-5",
1683
1697
  dashboardRunHour: 3,
1684
1698
  dashboardMaxSessionsPerRun: 30,
1699
+ magicUiEnabled: false,
1700
+ magicUiModel: "claude-haiku-4-5",
1685
1701
  publicUrl: "https://example.com",
1686
1702
  updateChannel: "stable",
1687
1703
  dockerAutoUpdate: false,
@@ -1728,6 +1744,8 @@ describe("PUT /api/settings", () => {
1728
1744
  dashboardModel: "claude-haiku-4-5",
1729
1745
  dashboardRunHour: 3,
1730
1746
  dashboardMaxSessionsPerRun: 30,
1747
+ magicUiEnabled: false,
1748
+ magicUiModel: "claude-haiku-4-5",
1731
1749
  publicUrl: "",
1732
1750
  updateChannel: "stable",
1733
1751
  dockerAutoUpdate: false,
@@ -1787,6 +1805,9 @@ describe("PUT /api/settings", () => {
1787
1805
  dashboardModel: "claude-haiku-4-5",
1788
1806
  dashboardRunHour: 3,
1789
1807
  dashboardMaxSessionsPerRun: 30,
1808
+ magicUiEnabled: false,
1809
+ magicUiModel: "claude-haiku-4-5",
1810
+ claudeCliAvailable: false,
1790
1811
  publicUrl: "",
1791
1812
  updateChannel: "stable",
1792
1813
  dockerAutoUpdate: false,
@@ -1824,6 +1845,8 @@ describe("PUT /api/settings", () => {
1824
1845
  dashboardModel: "claude-haiku-4-5",
1825
1846
  dashboardRunHour: 3,
1826
1847
  dashboardMaxSessionsPerRun: 30,
1848
+ magicUiEnabled: false,
1849
+ magicUiModel: "claude-haiku-4-5",
1827
1850
  publicUrl: "",
1828
1851
  updateChannel: "stable",
1829
1852
  dockerAutoUpdate: false,
@@ -1878,6 +1901,8 @@ describe("PUT /api/settings", () => {
1878
1901
  dashboardModel: "claude-haiku-4-5",
1879
1902
  dashboardRunHour: 3,
1880
1903
  dashboardMaxSessionsPerRun: 30,
1904
+ magicUiEnabled: false,
1905
+ magicUiModel: "claude-haiku-4-5",
1881
1906
  publicUrl: "",
1882
1907
  updateChannel: "stable",
1883
1908
  dockerAutoUpdate: false,
@@ -2032,6 +2057,8 @@ describe("PUT /api/settings", () => {
2032
2057
  dashboardModel: "claude-haiku-4-5",
2033
2058
  dashboardRunHour: 3,
2034
2059
  dashboardMaxSessionsPerRun: 30,
2060
+ magicUiEnabled: false,
2061
+ magicUiModel: "claude-haiku-4-5",
2035
2062
  publicUrl: "https://my-server.com",
2036
2063
  updateChannel: "stable",
2037
2064
  dockerAutoUpdate: false,
@@ -2292,6 +2319,8 @@ describe("GET /api/linear/issues", () => {
2292
2319
  dashboardModel: "claude-haiku-4-5",
2293
2320
  dashboardRunHour: 3,
2294
2321
  dashboardMaxSessionsPerRun: 30,
2322
+ magicUiEnabled: false,
2323
+ magicUiModel: "claude-haiku-4-5",
2295
2324
  publicUrl: "",
2296
2325
  updateChannel: "stable",
2297
2326
  dockerAutoUpdate: false,
@@ -2336,6 +2365,8 @@ describe("GET /api/linear/issues", () => {
2336
2365
  dashboardModel: "claude-haiku-4-5",
2337
2366
  dashboardRunHour: 3,
2338
2367
  dashboardMaxSessionsPerRun: 30,
2368
+ magicUiEnabled: false,
2369
+ magicUiModel: "claude-haiku-4-5",
2339
2370
  publicUrl: "",
2340
2371
  updateChannel: "stable",
2341
2372
  dockerAutoUpdate: false,
@@ -2433,6 +2464,8 @@ describe("GET /api/linear/issues", () => {
2433
2464
  dashboardModel: "claude-haiku-4-5",
2434
2465
  dashboardRunHour: 3,
2435
2466
  dashboardMaxSessionsPerRun: 30,
2467
+ magicUiEnabled: false,
2468
+ magicUiModel: "claude-haiku-4-5",
2436
2469
  publicUrl: "",
2437
2470
  updateChannel: "stable",
2438
2471
  dockerAutoUpdate: false,
@@ -2537,6 +2570,8 @@ describe("GET /api/linear/issues", () => {
2537
2570
  dashboardModel: "claude-haiku-4-5",
2538
2571
  dashboardRunHour: 3,
2539
2572
  dashboardMaxSessionsPerRun: 30,
2573
+ magicUiEnabled: false,
2574
+ magicUiModel: "claude-haiku-4-5",
2540
2575
  publicUrl: "",
2541
2576
  updateChannel: "stable",
2542
2577
  dockerAutoUpdate: false,
@@ -2606,6 +2641,8 @@ describe("GET /api/linear/connection", () => {
2606
2641
  dashboardModel: "claude-haiku-4-5",
2607
2642
  dashboardRunHour: 3,
2608
2643
  dashboardMaxSessionsPerRun: 30,
2644
+ magicUiEnabled: false,
2645
+ magicUiModel: "claude-haiku-4-5",
2609
2646
  publicUrl: "",
2610
2647
  updateChannel: "stable",
2611
2648
  dockerAutoUpdate: false,
@@ -2650,6 +2687,8 @@ describe("GET /api/linear/connection", () => {
2650
2687
  dashboardModel: "claude-haiku-4-5",
2651
2688
  dashboardRunHour: 3,
2652
2689
  dashboardMaxSessionsPerRun: 30,
2690
+ magicUiEnabled: false,
2691
+ magicUiModel: "claude-haiku-4-5",
2653
2692
  publicUrl: "",
2654
2693
  updateChannel: "stable",
2655
2694
  dockerAutoUpdate: false,
@@ -2716,6 +2755,8 @@ describe("POST /api/linear/issues/:id/transition", () => {
2716
2755
  dashboardModel: "claude-haiku-4-5",
2717
2756
  dashboardRunHour: 3,
2718
2757
  dashboardMaxSessionsPerRun: 30,
2758
+ magicUiEnabled: false,
2759
+ magicUiModel: "claude-haiku-4-5",
2719
2760
  publicUrl: "",
2720
2761
  updateChannel: "stable",
2721
2762
  dockerAutoUpdate: false,
@@ -2764,6 +2805,8 @@ describe("POST /api/linear/issues/:id/transition", () => {
2764
2805
  dashboardModel: "claude-haiku-4-5",
2765
2806
  dashboardRunHour: 3,
2766
2807
  dashboardMaxSessionsPerRun: 30,
2808
+ magicUiEnabled: false,
2809
+ magicUiModel: "claude-haiku-4-5",
2767
2810
  publicUrl: "",
2768
2811
  updateChannel: "stable",
2769
2812
  dockerAutoUpdate: false,
@@ -2811,6 +2854,8 @@ describe("POST /api/linear/issues/:id/transition", () => {
2811
2854
  dashboardModel: "claude-haiku-4-5",
2812
2855
  dashboardRunHour: 3,
2813
2856
  dashboardMaxSessionsPerRun: 30,
2857
+ magicUiEnabled: false,
2858
+ magicUiModel: "claude-haiku-4-5",
2814
2859
  publicUrl: "",
2815
2860
  updateChannel: "stable",
2816
2861
  dockerAutoUpdate: false,
@@ -2860,6 +2905,8 @@ describe("POST /api/linear/issues/:id/transition", () => {
2860
2905
  dashboardModel: "claude-haiku-4-5",
2861
2906
  dashboardRunHour: 3,
2862
2907
  dashboardMaxSessionsPerRun: 30,
2908
+ magicUiEnabled: false,
2909
+ magicUiModel: "claude-haiku-4-5",
2863
2910
  publicUrl: "",
2864
2911
  updateChannel: "stable",
2865
2912
  dockerAutoUpdate: false,
@@ -2943,6 +2990,8 @@ describe("POST /api/linear/issues/:id/transition", () => {
2943
2990
  dashboardModel: "claude-haiku-4-5",
2944
2991
  dashboardRunHour: 3,
2945
2992
  dashboardMaxSessionsPerRun: 30,
2993
+ magicUiEnabled: false,
2994
+ magicUiModel: "claude-haiku-4-5",
2946
2995
  publicUrl: "",
2947
2996
  updateChannel: "stable",
2948
2997
  dockerAutoUpdate: false,
@@ -3005,6 +3054,8 @@ describe("GET /api/linear/projects", () => {
3005
3054
  dashboardModel: "claude-haiku-4-5",
3006
3055
  dashboardRunHour: 3,
3007
3056
  dashboardMaxSessionsPerRun: 30,
3057
+ magicUiEnabled: false,
3058
+ magicUiModel: "claude-haiku-4-5",
3008
3059
  publicUrl: "",
3009
3060
  updateChannel: "stable",
3010
3061
  dockerAutoUpdate: false,
@@ -3049,6 +3100,8 @@ describe("GET /api/linear/projects", () => {
3049
3100
  dashboardModel: "claude-haiku-4-5",
3050
3101
  dashboardRunHour: 3,
3051
3102
  dashboardMaxSessionsPerRun: 30,
3103
+ magicUiEnabled: false,
3104
+ magicUiModel: "claude-haiku-4-5",
3052
3105
  publicUrl: "",
3053
3106
  updateChannel: "stable",
3054
3107
  dockerAutoUpdate: false,
@@ -3123,6 +3176,8 @@ describe("GET /api/linear/project-issues", () => {
3123
3176
  dashboardModel: "claude-haiku-4-5",
3124
3177
  dashboardRunHour: 3,
3125
3178
  dashboardMaxSessionsPerRun: 30,
3179
+ magicUiEnabled: false,
3180
+ magicUiModel: "claude-haiku-4-5",
3126
3181
  publicUrl: "",
3127
3182
  updateChannel: "stable",
3128
3183
  dockerAutoUpdate: false,
@@ -3167,6 +3222,8 @@ describe("GET /api/linear/project-issues", () => {
3167
3222
  dashboardModel: "claude-haiku-4-5",
3168
3223
  dashboardRunHour: 3,
3169
3224
  dashboardMaxSessionsPerRun: 30,
3225
+ magicUiEnabled: false,
3226
+ magicUiModel: "claude-haiku-4-5",
3170
3227
  publicUrl: "",
3171
3228
  updateChannel: "stable",
3172
3229
  dockerAutoUpdate: false,
@@ -3256,6 +3313,8 @@ describe("GET /api/linear/project-issues", () => {
3256
3313
  dashboardModel: "claude-haiku-4-5",
3257
3314
  dashboardRunHour: 3,
3258
3315
  dashboardMaxSessionsPerRun: 30,
3316
+ magicUiEnabled: false,
3317
+ magicUiModel: "claude-haiku-4-5",
3259
3318
  publicUrl: "",
3260
3319
  updateChannel: "stable",
3261
3320
  dockerAutoUpdate: false,
@@ -400,6 +400,8 @@ export type BrowserOutgoingMessage =
400
400
  | { type: "mcp_reconnect"; serverName: string; client_msg_id?: string }
401
401
  | { type: "mcp_set_servers"; servers: Record<string, McpServerConfig>; client_msg_id?: string }
402
402
  | { type: "set_ai_validation"; aiValidationEnabled?: boolean | null; aiValidationAutoApprove?: boolean | null; aiValidationAutoDeny?: boolean | null; client_msg_id?: string }
403
+ | { type: "set_magic_ui"; magicUiActive?: boolean | null; client_msg_id?: string }
404
+ | { type: "magic_ui_sync"; client_msg_id?: string }
403
405
  | { type: "end_session"; reason?: string; client_msg_id?: string }
404
406
  | { type: "stop_task"; task_id: string; client_msg_id?: string }
405
407
  | { type: "update_environment_variables"; variables: Record<string, string>; client_msg_id?: string };
@@ -442,7 +444,12 @@ export type BrowserIncomingMessageBase =
442
444
  | { type: "session_phase"; phase: SessionPhase; previousPhase: SessionPhase }
443
445
  | { type: "prompt_suggestion"; suggestions: string[] }
444
446
  | { type: "streamlined_text"; text: string }
445
- | { type: "streamlined_tool_use_summary"; tool_summary: string };
447
+ | { type: "streamlined_tool_use_summary"; tool_summary: string }
448
+ // MagicUI: the server always sends the full canonical dashboard state
449
+ // (small by construction — capped slots/logs). The iframe runtime diffs
450
+ // against its DOM via per-slot updatedAt, so no client-side reducer and no
451
+ // version-gap protocol is needed; late joiners are trivially correct.
452
+ | { type: "magic_ui_state"; state: import("./magic-ui-types.js").MagicUiDashboardState };
446
453
 
447
454
  export type BrowserIncomingMessage = BrowserIncomingMessageBase & { seq?: number };
448
455
 
@@ -508,6 +515,13 @@ export interface SessionState {
508
515
  aiValidationAutoApprove?: boolean | null;
509
516
  /** Per-session auto-deny override. null/undefined = use global default */
510
517
  aiValidationAutoDeny?: boolean | null;
518
+ /**
519
+ * Per-session MagicUI opt-in. null/undefined = use the global
520
+ * magicUiEnabled default. When effective-true (and the Claude CLI is
521
+ * available), browsers render the MagicUI dashboard view and the server
522
+ * runs a Haiku watcher for this session.
523
+ */
524
+ magicUiActive?: boolean | null;
511
525
  /** If this session is linked to a Linear agent session */
512
526
  linearSessionId?: string;
513
527
  }
@@ -49,6 +49,8 @@ describe("settings-manager", () => {
49
49
  dashboardModel: "claude-haiku-4-5",
50
50
  dashboardRunHour: 3,
51
51
  dashboardMaxSessionsPerRun: 30,
52
+ magicUiEnabled: false,
53
+ magicUiModel: "claude-haiku-4-5",
52
54
  publicUrl: "",
53
55
  updateChannel: "stable",
54
56
  dockerAutoUpdate: false,
@@ -117,6 +119,8 @@ describe("settings-manager", () => {
117
119
  dashboardModel: "claude-haiku-4-5",
118
120
  dashboardRunHour: 3,
119
121
  dashboardMaxSessionsPerRun: 30,
122
+ magicUiEnabled: false,
123
+ magicUiModel: "claude-haiku-4-5",
120
124
  publicUrl: "",
121
125
  updateChannel: "stable",
122
126
  dockerAutoUpdate: false,
@@ -211,6 +215,8 @@ describe("settings-manager", () => {
211
215
  dashboardModel: "claude-haiku-4-5",
212
216
  dashboardRunHour: 3,
213
217
  dashboardMaxSessionsPerRun: 30,
218
+ magicUiEnabled: false,
219
+ magicUiModel: "claude-haiku-4-5",
214
220
  publicUrl: "",
215
221
  updateChannel: "stable",
216
222
  dockerAutoUpdate: false,
@@ -20,6 +20,12 @@ export const DASHBOARD_MODEL_OPTIONS = [
20
20
  "claude-sonnet-5",
21
21
  ] as const;
22
22
  export const DEFAULT_DASHBOARD_RUN_HOUR = 3;
23
+
24
+ /**
25
+ * The MagicUI watcher streams many small turns per session, so it defaults to
26
+ * the cheapest capable model. Shares the dashboard model option list.
27
+ */
28
+ export const DEFAULT_MAGIC_UI_MODEL = DEFAULT_DASHBOARD_MODEL;
23
29
  export const DEFAULT_DASHBOARD_MAX_SESSIONS_PER_RUN = 30;
24
30
 
25
31
  export type UpdateChannel = "stable" | "prerelease";
@@ -83,6 +89,14 @@ export interface CompanionSettings {
83
89
  dashboardRunHour: number;
84
90
  /** Cost safety valve: max sessions summarized per run. */
85
91
  dashboardMaxSessionsPerRun: number;
92
+ /**
93
+ * Makes the MagicUI session view available to users (default off). Users
94
+ * still opt in per session; see SessionState.magicUiActive. Requires the
95
+ * Claude CLI login — the watcher runs on the subscription, never an API key.
96
+ */
97
+ magicUiEnabled: boolean;
98
+ /** Model used by the MagicUI watcher. Cheap-by-default (Haiku). */
99
+ magicUiModel: string;
86
100
  publicUrl: string;
87
101
  updateChannel: UpdateChannel;
88
102
  dockerAutoUpdate: boolean;
@@ -176,6 +190,8 @@ let settings: CompanionSettings = {
176
190
  dashboardModel: DEFAULT_DASHBOARD_MODEL,
177
191
  dashboardRunHour: DEFAULT_DASHBOARD_RUN_HOUR,
178
192
  dashboardMaxSessionsPerRun: DEFAULT_DASHBOARD_MAX_SESSIONS_PER_RUN,
193
+ magicUiEnabled: false,
194
+ magicUiModel: DEFAULT_MAGIC_UI_MODEL,
179
195
  publicUrl: "",
180
196
  updateChannel: "stable",
181
197
  dockerAutoUpdate: false,
@@ -231,6 +247,11 @@ function normalize(raw: Partial<CompanionSettings> | null | undefined): Companio
231
247
  && raw.dashboardMaxSessionsPerRun >= 1 && raw.dashboardMaxSessionsPerRun <= 200
232
248
  ? raw.dashboardMaxSessionsPerRun
233
249
  : DEFAULT_DASHBOARD_MAX_SESSIONS_PER_RUN,
250
+ magicUiEnabled: typeof raw?.magicUiEnabled === "boolean" ? raw.magicUiEnabled : false,
251
+ magicUiModel:
252
+ typeof raw?.magicUiModel === "string" && raw.magicUiModel.trim()
253
+ ? raw.magicUiModel.trim()
254
+ : DEFAULT_MAGIC_UI_MODEL,
234
255
  publicUrl: typeof raw?.publicUrl === "string" ? raw.publicUrl.trim().replace(/\/+$/, "") : "",
235
256
  updateChannel: raw?.updateChannel === "prerelease" ? "prerelease" : "stable",
236
257
  dockerAutoUpdate: typeof raw?.dockerAutoUpdate === "boolean" ? raw.dockerAutoUpdate : false,
@@ -272,7 +293,7 @@ export function getSettings(): CompanionSettings {
272
293
  }
273
294
 
274
295
  export function updateSettings(
275
- patch: Partial<Pick<CompanionSettings, "anthropicApiKey" | "anthropicModel" | "claudeCodeOAuthToken" | "openaiApiKey" | "onboardingCompleted" | "linearApiKey" | "linearAutoTransition" | "linearAutoTransitionStateId" | "linearAutoTransitionStateName" | "linearArchiveTransition" | "linearArchiveTransitionStateId" | "linearArchiveTransitionStateName" | "linearOAuthClientId" | "linearOAuthClientSecret" | "linearOAuthWebhookSecret" | "linearOAuthAccessToken" | "linearOAuthRefreshToken" | "aiValidationEnabled" | "aiValidationAutoApprove" | "aiValidationAutoDeny" | "dashboardEnabled" | "dashboardModel" | "dashboardRunHour" | "dashboardMaxSessionsPerRun" | "publicUrl" | "updateChannel" | "dockerAutoUpdate" | "proactiveKeepaliveEnabled" | "keepaliveDetachedSessions" | "wedgeKillEnabled" | "silenceProbeEnabled" | "claudeTransport" | "cliBridgeMode" | "claudeBridgeMode" | "claudeBridgeIngressUrl" | "claudeCompatBannerDismissedVersion">>,
296
+ patch: Partial<Pick<CompanionSettings, "anthropicApiKey" | "anthropicModel" | "claudeCodeOAuthToken" | "openaiApiKey" | "onboardingCompleted" | "linearApiKey" | "linearAutoTransition" | "linearAutoTransitionStateId" | "linearAutoTransitionStateName" | "linearArchiveTransition" | "linearArchiveTransitionStateId" | "linearArchiveTransitionStateName" | "linearOAuthClientId" | "linearOAuthClientSecret" | "linearOAuthWebhookSecret" | "linearOAuthAccessToken" | "linearOAuthRefreshToken" | "aiValidationEnabled" | "aiValidationAutoApprove" | "aiValidationAutoDeny" | "dashboardEnabled" | "dashboardModel" | "dashboardRunHour" | "dashboardMaxSessionsPerRun" | "magicUiEnabled" | "magicUiModel" | "publicUrl" | "updateChannel" | "dockerAutoUpdate" | "proactiveKeepaliveEnabled" | "keepaliveDetachedSessions" | "wedgeKillEnabled" | "silenceProbeEnabled" | "claudeTransport" | "cliBridgeMode" | "claudeBridgeMode" | "claudeBridgeIngressUrl" | "claudeCompatBannerDismissedVersion">>,
276
297
  ): CompanionSettings {
277
298
  ensureLoaded();
278
299
  settings = normalize({
@@ -300,6 +321,8 @@ export function updateSettings(
300
321
  dashboardModel: patch.dashboardModel ?? settings.dashboardModel,
301
322
  dashboardRunHour: patch.dashboardRunHour ?? settings.dashboardRunHour,
302
323
  dashboardMaxSessionsPerRun: patch.dashboardMaxSessionsPerRun ?? settings.dashboardMaxSessionsPerRun,
324
+ magicUiEnabled: patch.magicUiEnabled ?? settings.magicUiEnabled,
325
+ magicUiModel: patch.magicUiModel ?? settings.magicUiModel,
303
326
  publicUrl: patch.publicUrl ?? settings.publicUrl,
304
327
  updateChannel: patch.updateChannel ?? settings.updateChannel,
305
328
  dockerAutoUpdate: patch.dockerAutoUpdate ?? settings.dockerAutoUpdate,
@@ -66,6 +66,7 @@ export const IDEMPOTENT_BROWSER_MESSAGE_TYPES: ReadonlySet<string> = new Set([
66
66
  "mcp_reconnect",
67
67
  "mcp_set_servers",
68
68
  "set_ai_validation",
69
+ "set_magic_ui",
69
70
  ]);
70
71
 
71
72
  /**
@@ -136,6 +136,8 @@ describe("attachCodexAdapterHandlers", () => {
136
136
  dashboardModel: "claude-haiku-4-5",
137
137
  dashboardRunHour: 3,
138
138
  dashboardMaxSessionsPerRun: 30,
139
+ magicUiEnabled: false,
140
+ magicUiModel: "claude-haiku-4-5",
139
141
  publicUrl: "",
140
142
  updateChannel: "stable",
141
143
  dockerAutoUpdate: false,
@@ -1130,6 +1132,8 @@ describe("attachCodexAdapterHandlers", () => {
1130
1132
  dashboardModel: "claude-haiku-4-5",
1131
1133
  dashboardRunHour: 3,
1132
1134
  dashboardMaxSessionsPerRun: 30,
1135
+ magicUiEnabled: false,
1136
+ magicUiModel: "claude-haiku-4-5",
1133
1137
  publicUrl: "",
1134
1138
  updateChannel: "stable",
1135
1139
  dockerAutoUpdate: false,
@@ -1314,6 +1318,8 @@ describe("attachCodexAdapterHandlers", () => {
1314
1318
  dashboardModel: "claude-haiku-4-5",
1315
1319
  dashboardRunHour: 3,
1316
1320
  dashboardMaxSessionsPerRun: 30,
1321
+ magicUiEnabled: false,
1322
+ magicUiModel: "claude-haiku-4-5",
1317
1323
  publicUrl: "",
1318
1324
  updateChannel: "stable",
1319
1325
  dockerAutoUpdate: false,
@@ -1368,6 +1374,8 @@ describe("attachCodexAdapterHandlers", () => {
1368
1374
  dashboardModel: "claude-haiku-4-5",
1369
1375
  dashboardRunHour: 3,
1370
1376
  dashboardMaxSessionsPerRun: 30,
1377
+ magicUiEnabled: false,
1378
+ magicUiModel: "claude-haiku-4-5",
1371
1379
  publicUrl: "",
1372
1380
  updateChannel: "stable",
1373
1381
  dockerAutoUpdate: false,
@@ -1487,6 +1495,8 @@ describe("attachCodexAdapterHandlers", () => {
1487
1495
  dashboardModel: "claude-haiku-4-5",
1488
1496
  dashboardRunHour: 3,
1489
1497
  dashboardMaxSessionsPerRun: 30,
1498
+ magicUiEnabled: false,
1499
+ magicUiModel: "claude-haiku-4-5",
1490
1500
  publicUrl: "",
1491
1501
  updateChannel: "stable",
1492
1502
  dockerAutoUpdate: false,
@@ -1625,6 +1635,8 @@ describe("attachCodexAdapterHandlers", () => {
1625
1635
  dashboardModel: "claude-haiku-4-5",
1626
1636
  dashboardRunHour: 3,
1627
1637
  dashboardMaxSessionsPerRun: 30,
1638
+ magicUiEnabled: false,
1639
+ magicUiModel: "claude-haiku-4-5",
1628
1640
  publicUrl: "",
1629
1641
  updateChannel: "stable",
1630
1642
  dockerAutoUpdate: false,
@@ -1,5 +1,14 @@
1
1
  import type { Session } from "./ws-bridge-types.js";
2
2
 
3
+ export function handleSetMagicUi(
4
+ session: Session,
5
+ msg: { magicUiActive?: boolean | null },
6
+ ): void {
7
+ if (msg.magicUiActive !== undefined) {
8
+ session.state.magicUiActive = msg.magicUiActive;
9
+ }
10
+ }
11
+
3
12
  export function handleSetAiValidation(
4
13
  session: Session,
5
14
  msg: {