@deepseek-insight/dsi 0.10.0 → 0.10.2

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 (125) hide show
  1. package/.agents/skills/glass-dsi/scripts/_connect.mjs +68 -9
  2. package/.agents/skills/glass-dsi/scripts/glass.mjs +6 -5
  3. package/.agents/skills/glass-url/scripts/_connect.mjs +59 -10
  4. package/.agents/skills/glass-url/scripts/glass-url.mjs +5 -4
  5. package/README.md +44 -15
  6. package/bin/dsi.mjs +44 -7
  7. package/bin/lib/dev-mode.mjs +8 -5
  8. package/bin/lib/glass-browser-bootstrap.mjs +132 -0
  9. package/bin/lib/mcp-banner.mjs +26 -0
  10. package/bin/lib/zai-patch.mjs +3 -1
  11. package/build/client/_app/immutable/assets/0.B2rul4Mk.css +2 -0
  12. package/build/client/_app/immutable/assets/2.glyfXGzi.css +1 -0
  13. package/build/client/_app/immutable/chunks/{Bu2owQS4.js → BodLd2is.js} +1 -1
  14. package/build/client/_app/immutable/chunks/Cy4hfhN-.js +1 -0
  15. package/build/client/_app/immutable/chunks/D17XAauH.js +2 -0
  16. package/build/client/_app/immutable/chunks/FY34R_78.js +1 -0
  17. package/build/client/_app/immutable/entry/{app.DjLAWmJH.js → app.ClaEF2lH.js} +2 -2
  18. package/build/client/_app/immutable/entry/start.CU6iOT39.js +1 -0
  19. package/build/client/_app/immutable/nodes/{1.w6Hz6-dJ.js → 1.CSTMQTZ7.js} +1 -1
  20. package/build/client/_app/immutable/nodes/{2.CFmqEODJ.js → 2.CdbJFHUF.js} +197 -199
  21. package/build/client/_app/version.json +1 -1
  22. package/build/handler.js +5 -5
  23. package/build/index.js +5 -5
  24. package/build/server/chunks/chunks/{db.js-f2PmBjyD.js → db.js-BGSxIVb3.js} +2 -2
  25. package/build/server/chunks/chunks/{db2.js-DkwY64WO.js → db2.js-vQNFcVO7.js} +2 -2
  26. package/build/server/chunks/chunks/{dsh-connection.js-BW07Aicl.js → dsh-connection.js-Dcdxqp52.js} +27 -7
  27. package/build/server/chunks/chunks/{exports.js-DH_tjtdv.js → exports.js-D5zA1U7A.js} +42 -4
  28. package/build/server/chunks/chunks/{git-probe.js-CQqTV7XX.js → git-probe.js-BIieYqLU.js} +2 -2
  29. package/build/server/chunks/chunks/{insight-config.js-C5-Rhh5A.js → insight-config.js-ylYA2CXm.js} +2 -2
  30. package/build/server/chunks/chunks/{internal2.js-DRFhzPda.js → internal2.js-XxZ8wzRE.js} +3 -3
  31. package/build/server/chunks/chunks/{registry.js-CWnRQ4vx.js → registry.js-ClhrzR6O.js} +145 -56
  32. package/build/server/chunks/chunks/{registry2.js-CFzpvj9c.js → registry2.js-BZfrwsOy.js} +20 -1
  33. package/build/server/chunks/chunks/{settings-document.js-DxKURfc_.js → settings-document.js-BIxaivJ1.js} +23 -8
  34. package/build/server/chunks/chunks/{state.js-kZHonU4N.js → state.js-D5zsYmjz.js} +3 -3
  35. package/build/server/chunks/chunks/{watcher.js-DvlNn8X0.js → watcher.js-Bcm29AuA.js} +4 -4
  36. package/build/server/chunks/entries/endpoints/api/a2a/_id_/{_server.ts.js-DItDXV23.js → _server.ts.js-BbIEpZB5.js} +4 -4
  37. package/build/server/chunks/entries/endpoints/api/a2a/{_server.ts.js-Hor8QZb1.js → _server.ts.js-Daho1I-l.js} +6 -6
  38. package/build/server/chunks/entries/endpoints/api/a2a/register/{_server.ts.js-CEvvsa8B.js → _server.ts.js-C5X77Uef.js} +6 -6
  39. package/build/server/chunks/entries/endpoints/api/browser/_id_/activate/{_server.ts.js-sXqf-okw.js → _server.ts.js-B-Ns5pyR.js} +4 -4
  40. package/build/server/chunks/entries/endpoints/api/browser/_id_/close/{_server.ts.js-Byj9ApJh.js → _server.ts.js-CzXe-sTr.js} +4 -4
  41. package/build/server/chunks/entries/endpoints/api/browser/_id_/closetab/{_server.ts.js-DOIsrzvg.js → _server.ts.js-Ct465_rt.js} +4 -4
  42. package/build/server/chunks/entries/endpoints/api/browser/_id_/element/{_server.ts.js-Cq3gmRfE.js → _server.ts.js-6mdytkvM.js} +4 -4
  43. package/build/server/chunks/entries/endpoints/api/browser/_id_/history/{_server.ts.js-BGSLIV0t.js → _server.ts.js-BwUPVQGx.js} +4 -4
  44. package/build/server/chunks/entries/endpoints/api/browser/_id_/input/{_server.ts.js-8slca75-.js → _server.ts.js-8Nq3JxSo.js} +4 -4
  45. package/build/server/chunks/entries/endpoints/api/browser/_id_/mcp/{_server.ts.js-CpbdTYds.js → _server.ts.js-B9eKtj1-.js} +4 -4
  46. package/build/server/chunks/entries/endpoints/api/browser/_id_/navigate/{_server.ts.js-Bls9LtLY.js → _server.ts.js-CHJs9Qbl.js} +4 -4
  47. package/build/server/chunks/entries/endpoints/api/browser/_id_/opentab/{_server.ts.js-DqNi_iW_.js → _server.ts.js-B32Ow0Y2.js} +4 -4
  48. package/build/server/chunks/entries/endpoints/api/browser/_id_/page/{_server.ts.js-DuXWwo7i.js → _server.ts.js-Cpv4BolN.js} +4 -4
  49. package/build/server/chunks/entries/endpoints/api/browser/_id_/stream/{_server.ts.js-DXfqBDEw.js → _server.ts.js-DJqJImSI.js} +4 -4
  50. package/build/server/chunks/entries/endpoints/api/browser/_id_/tabs/{_server.ts.js-Bgnl8lYs.js → _server.ts.js-Bm6IAZF6.js} +4 -4
  51. package/build/server/chunks/entries/endpoints/api/browser/_id_/viewport/{_server.ts.js-FHmQVbrv.js → _server.ts.js-B1VVzfCk.js} +4 -4
  52. package/build/server/chunks/entries/endpoints/api/browser/_id_/zoom/{_server.ts.js-BV4cBtSA.js → _server.ts.js-CoUpqHeq.js} +4 -4
  53. package/build/server/chunks/entries/endpoints/api/browser/{_server.ts.js-4vgkwh7v.js → _server.ts.js-D9hK0CG8.js} +8 -5
  54. package/build/server/chunks/entries/endpoints/api/config/{_server.ts.js-C_9kwANR.js → _server.ts.js-DqPIv-gn.js} +4 -4
  55. package/build/server/chunks/entries/endpoints/api/dsh/directory/{_server.ts.js-h1OTHcL4.js → _server.ts.js-DzFVVhO_.js} +4 -4
  56. package/build/server/chunks/entries/endpoints/api/dsh/jobs/{_server.ts.js-B6nih9Sd.js → _server.ts.js-DvuZUgbK.js} +4 -4
  57. package/build/server/chunks/entries/endpoints/api/dsh/jobs/follow/{_server.ts.js-Cf17SVmD.js → _server.ts.js-D6brRlYZ.js} +4 -4
  58. package/build/server/chunks/entries/endpoints/api/dsh/pick-directory/{_server.ts.js-DjNwMbdS.js → _server.ts.js-8nNlcikK.js} +4 -4
  59. package/build/server/chunks/entries/endpoints/api/dsh/presets/{_server.ts.js-Cz5aaEhC.js → _server.ts.js-C8AZBt63.js} +4 -4
  60. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/attachment/{_server.ts.js-BFBy5O9Y.js → _server.ts.js-BFfr6uJh.js} +4 -4
  61. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/cancel/{_server.ts.js-CgEdCo3T.js → _server.ts.js-DSSgbD87.js} +4 -4
  62. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/catalog/{_server.ts.js-Bo_aEnHh.js → _server.ts.js-DYtrYC2P.js} +4 -4
  63. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/changes/{_server.ts.js-Bd2G_GaH.js → _server.ts.js-HxI5M4jg.js} +4 -4
  64. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/command/{_server.ts.js-DL64FyRw.js → _server.ts.js-Ch0dMm33.js} +4 -4
  65. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/events/{_server.ts.js-MhOGB66D.js → _server.ts.js-BG5C--Ok.js} +4 -4
  66. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/fork/{_server.ts.js-MjPgXgGO.js → _server.ts.js-B64YyaFI.js} +4 -4
  67. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/goal/{_server.ts.js-BUjDae5H.js → _server.ts.js-0RtL-9Zr.js} +4 -4
  68. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/history/{_server.ts.js-dTKd7Hsv.js → _server.ts.js-C4k-RTIw.js} +4 -4
  69. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/models/{_server.ts.js-B82rvKoI.js → _server.ts.js-CU38DxMt.js} +4 -4
  70. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/path/{_server.ts.js-Cqijsi-c.js → _server.ts.js-BXphf5ux.js} +3 -3
  71. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/permission/{_server.ts.js-CL_vVv2p.js → _server.ts.js-DQ5FG9Pc.js} +4 -4
  72. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/prompt/{_server.ts.js-WS4W9Ly4.js → _server.ts.js-In3bXhUO.js} +4 -4
  73. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/rename/{_server.ts.js-B72KGRGh.js → _server.ts.js-Bd0KF8Qm.js} +4 -4
  74. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/respond/{_server.ts.js-DK5xb2b5.js → _server.ts.js-4Y-KAw2r.js} +4 -4
  75. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/select-model/{_server.ts.js-lhT87EQu.js → _server.ts.js-zXYA1AJD.js} +4 -4
  76. package/build/server/chunks/entries/endpoints/api/dsh/sessions/{_server.ts.js-CMhfhbkN.js → _server.ts.js-vh7lFV0n.js} +4 -4
  77. package/build/server/chunks/entries/endpoints/api/dsh/subagents/{_server.ts.js-BLvcFsg6.js → _server.ts.js-DH7OEWm0.js} +4 -4
  78. package/build/server/chunks/entries/endpoints/api/dsh/workspace-file/{_server.ts.js-D_rw2Uq6.js → _server.ts.js-CDwgBp7D.js} +4 -4
  79. package/build/server/chunks/entries/endpoints/api/dsh/workspace-tree/{_server.ts.js-DfXpMMpw.js → _server.ts.js-CtFxC_yV.js} +4 -4
  80. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/{_server.ts.js-2mJCJVxk.js → _server.ts.js-DRNzGdAj.js} +4 -4
  81. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/delete/{_server.ts.js-BCx0Lzjf.js → _server.ts.js-DIjNfN0d.js} +4 -4
  82. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/rename/{_server.ts.js-DECaSjwp.js → _server.ts.js-CtV7JhrK.js} +4 -4
  83. package/build/server/chunks/entries/endpoints/api/prompts/_id_/{_server.ts.js-i_ciJzlb.js → _server.ts.js-Cg9zHbUP.js} +4 -4
  84. package/build/server/chunks/entries/endpoints/api/prompts/{_server.ts.js-BO1pScn7.js → _server.ts.js-DOHmlKvl.js} +4 -4
  85. package/build/server/chunks/entries/endpoints/api/prompts/use/{_server.ts.js-Buhpf7WE.js → _server.ts.js-COVyy224.js} +4 -4
  86. package/build/server/chunks/entries/endpoints/api/settings/{_server.ts.js-DngUQzcT.js → _server.ts.js-HjpfRAaT.js} +3 -3
  87. package/build/server/chunks/entries/endpoints/api/terminal/_id_/delete/_server.ts.js-D9UU6xR6.js +42 -0
  88. package/build/server/chunks/entries/endpoints/api/terminal/_id_/output/{_server.ts.js-DLETP4yQ.js → _server.ts.js-BqJRMRsb.js} +2 -2
  89. package/build/server/chunks/entries/endpoints/api/terminal/_id_/reattach/{_server.ts.js-uWl6oxPS.js → _server.ts.js-D6bMXeOk.js} +2 -2
  90. package/build/server/chunks/entries/endpoints/api/terminal/_id_/send/{_server.ts.js-DYNmXaAd.js → _server.ts.js-BRgsqPFM.js} +2 -2
  91. package/build/server/chunks/entries/endpoints/api/terminal/_id_/stream/{_server.ts.js-C5m3Ktho.js → _server.ts.js-EWo4sSmg.js} +2 -2
  92. package/build/server/chunks/entries/endpoints/api/terminal/{_server.ts.js-DoTNvcOa.js → _server.ts.js-v1ixRdpN.js} +4 -4
  93. package/build/server/chunks/entries/endpoints/api/terminal/stream/_server.ts.js-BQOQyT7o.js +185 -0
  94. package/build/server/chunks/entries/endpoints/api/workspace/file-write/{_server.ts.js-BzYOJSkI.js → _server.ts.js-CjU6bLOf.js} +5 -5
  95. package/build/server/chunks/entries/endpoints/api/workspace/git-events/{_server.ts.js-xcNesei9.js → _server.ts.js-CYU0btiH.js} +5 -5
  96. package/build/server/chunks/entries/endpoints/api/workspace/git-file-head/{_server.ts.js-DsJWYdxt.js → _server.ts.js-Du9YcwLw.js} +5 -5
  97. package/build/server/chunks/entries/endpoints/api/workspace/git-map/{_server.ts.js-CNCku9BR.js → _server.ts.js-BdV_KDjb.js} +5 -5
  98. package/build/server/chunks/entries/endpoints/api/workspace/git-status/{_server.ts.js-nLsNjOxj.js → _server.ts.js-C-C84Yzu.js} +5 -5
  99. package/build/server/chunks/entries/endpoints/api/workspace/write-gate/{_server.ts.js-urQk2-ds.js → _server.ts.js-D9uFvZya.js} +5 -5
  100. package/build/server/chunks/entries/{hooks.server.js-CU2RE5vs.js → hooks.server.js-YJQlamhk.js} +2 -2
  101. package/build/server/chunks/entries/pages/{_error.svelte.js-BKhpJygh.js → _error.svelte.js-TrOSxb0e.js} +4 -4
  102. package/build/server/chunks/entries/pages/{_page.server.ts.js-BK3Y6eff.js → _page.server.ts.js-BFDPRDEm.js} +2 -2
  103. package/build/server/chunks/entries/pages/{_page.svelte.js-BlbGdGej.js → _page.svelte.js-BUuXoaje.js} +706 -158
  104. package/build/server/chunks/{handler-BtG6aVza.js → handler-BOqY82LA.js} +3 -3
  105. package/build/server/chunks/{index.js-rGfUdWOY.js → index.js-M1GginY2.js} +3 -44
  106. package/build/server/chunks/{manifest.js-DwQ7BEt1.js → manifest.js-BEmwisii.js} +81 -67
  107. package/build/server/chunks/nodes/{0.js-Cum-Y-us.js → 0.js-Do4ZKaKe.js} +3 -3
  108. package/build/server/chunks/nodes/1.js-CRbdbfof.js +9 -0
  109. package/build/server/chunks/nodes/{2.js-ButkUErW.js → 2.js-BFX_zPfJ.js} +8 -8
  110. package/package.json +1 -1
  111. package/skills/dsi-drive-terminal/SKILL.md +23 -1
  112. package/skills/dsi-spec/SKILL.md +8 -0
  113. package/skills/dsi-task/SKILL.md +34 -0
  114. package/skills/dsi-token-spent/SKILL.md +122 -0
  115. package/template/.dsi/prompts.sqlite +0 -0
  116. package/template/.dsi/prompts.sqlite-shm +0 -0
  117. package/template/.dsi/prompts.sqlite-wal +0 -0
  118. package/build/client/_app/immutable/assets/0.uhYTRapi.css +0 -2
  119. package/build/client/_app/immutable/assets/2.D0QOh44D.css +0 -1
  120. package/build/client/_app/immutable/chunks/0Uk2rkmL.js +0 -1
  121. package/build/client/_app/immutable/chunks/BaH6C3xD.js +0 -2
  122. package/build/client/_app/immutable/chunks/DXba8Dom.js +0 -1
  123. package/build/client/_app/immutable/entry/start.D-t6KuoK.js +0 -1
  124. package/build/server/chunks/nodes/1.js-D0EwPqp_.js +0 -9
  125. /package/build/client/_app/immutable/nodes/{0.DeDuCaDD.js → 0.CYP0vr1_.js} +0 -0
@@ -26,13 +26,74 @@
26
26
  */
27
27
  import { chromium } from 'playwright-core';
28
28
 
29
- const DSI = 'http://127.0.0.1:5175';
29
+ /**
30
+ * @typedef {Object} GlassSession
31
+ * @property {string} id
32
+ * @property {number} [port]
33
+ * @property {string|null} [ownerSessionId]
34
+ * @property {string} [createdAt]
35
+ * @typedef {Object} PickResult
36
+ * @property {GlassSession|null|undefined} session
37
+ * @property {boolean} [degraded]
38
+ * @property {string} [note]
39
+ * @property {string} [error]
40
+ */
41
+
42
+ // ── The Floor Seam (ADR "The Floor Seam" 2026-11-05, D1-D3) ────────────────
43
+ // DSI_UI env verbatim when set; else parallel probe of both known floor
44
+ // bases (5175 dev, 5174 build) with the D7 identity tiebreak. Tab identity
45
+ // is an origin compare against the RESOLVED base — never a port literal.
46
+
47
+ const DEFAULT_FLOOR_BASES = ['http://127.0.0.1:5175', 'http://127.0.0.1:5174'];
48
+
49
+ /** Candidate bases: env seam wins; else dev first, build second. */
50
+ export function floorCandidates(env = process.env) {
51
+ const ui = env.DSI_UI;
52
+ if (ui && /^http:\/\//.test(ui)) return [ui.replace(/\/$/, '')];
53
+ return [...DEFAULT_FLOOR_BASES];
54
+ }
55
+
56
+ /** Resolve the live floor — one answer wins; both → pickSession per floor,
57
+ * my-glass floor wins, double-claim dies loud; none → die naming both. */
58
+ /** @param {{ env?: Record<string, string|undefined>, fetchImpl?: typeof fetch, identity?: string|undefined, pickSessionFn?: (s: GlassSession[]|null|undefined, id: string|undefined) => PickResult, dieFn?: (msg: string) => never, timeoutMs?: number }} [opts] @returns {Promise<{ base: string, reg: { sessions: GlassSession[] } }>} — never resolves undefined: every fall-through path dies. */
59
+ export async function resolveDsiBase({ env = process.env, fetchImpl = fetch, identity = undefined, pickSessionFn = pickSession, dieFn = die, timeoutMs = 3000 } = {}) {
60
+ const candidates = floorCandidates(env);
61
+ const probes = await Promise.all(candidates.map(async (base) => {
62
+ try {
63
+ const res = await fetchImpl(base + '/api/browser', { signal: AbortSignal.timeout(timeoutMs) });
64
+ if (!res.ok) return { base, reg: null };
65
+ return { base, reg: await res.json() };
66
+ } catch { return { base, reg: null }; }
67
+ }));
68
+ const answered = probes.filter((p) => p.reg?.sessions);
69
+ if (answered.length === 0) {
70
+ dieFn('no DSI floor answers — probed ' + candidates.join(', ') +
71
+ ' (dev: dsi dsh --sync --dev --playwright-mcp · build: dsi dsh --sync · or set DSI_UI=<floor url>)');
72
+ }
73
+ if (answered.length === 1) return answered[0];
74
+ const claims = answered.map((p) => ({ ...p, verdict: pickSessionFn(p.reg.sessions, identity) }));
75
+ const mine = claims.filter((c) => c.verdict.session);
76
+ if (mine.length === 1) return mine[0];
77
+ if (mine.length > 1) {
78
+ dieFn('a glass owned by you lives on BOTH floors (' + mine.map((m) => m.base).join(', ') +
79
+ ') — set DSI_UI=<floor url> to name the floor; refusing to guess (D7)');
80
+ }
81
+ return dieFn('both floors answer but neither holds YOUR glass (' + answered.map((m) => m.base).join(', ') +
82
+ ') — click the Browser chip in THIS conversation');
83
+ }
30
84
 
85
+ /** @param {string} url @param {string} base @returns {boolean} True when a tab url's ORIGIN is the resolved floor's origin (D2). */
86
+ export function isFloorTab(url, base) {
87
+ try { return new URL(url).origin === new URL(base).origin; } catch { return false; }
88
+ }
89
+
90
+ /** @param {string} msg @returns {never} */
31
91
  export function die(msg) {
32
92
  console.error(JSON.stringify({ error: msg, fallback: 'SKILL.md Step 1 — ask the OPERATOR to click the Browser chip IN THE RIGHT CONVERSATION, then catch the session via GET /api/browser. If DSH_SESSION_ID is absent from env, pass --owner-session <id> (the session id you already know from context).' }));
33
93
  process.exit(1);
34
94
  }
35
95
 
96
+ /** @param {string[]} argv @returns {string|undefined} */
36
97
  export function identityFrom(argv) {
37
98
  const flag = argv.indexOf('--owner-session');
38
99
  if (flag !== -1 && argv[flag + 1]) return argv[flag + 1];
@@ -41,6 +102,7 @@ export function identityFrom(argv) {
41
102
  return undefined;
42
103
  }
43
104
 
105
+ /** @param {GlassSession[]|null|undefined} sessions @param {string|undefined} envId @returns {PickResult} */
44
106
  export function pickSession(sessions, envId) {
45
107
  const list = sessions ?? [];
46
108
  if (envId) {
@@ -61,21 +123,18 @@ export function pickSession(sessions, envId) {
61
123
  return { session: null, error: 'multiple live glasses and no explicit identity — pass --owner-session <id> (or GLASS_OWNER_SESSION) from the session id you already know; refusing to guess (D7)' };
62
124
  }
63
125
 
126
+ /** @returns {Promise<{ session: GlassSession, browser: import('playwright-core').Browser, ctx: import('playwright-core').BrowserContext, base: string }>} */
64
127
  export async function connect() {
65
- let reg;
66
- try {
67
- reg = await (await fetch(DSI + '/api/browser')).json();
68
- } catch {
69
- die('DSI dev server not reachable on 5175 — floor not up (dsi dsh --sync --dev --playwright-mcp)');
70
- }
128
+ const { base, reg } = await resolveDsiBase({ env: process.env, identity: identityFrom(process.argv) });
71
129
  const { session, degraded, note, error } = pickSession(reg.sessions, identityFrom(process.argv));
72
130
  if (!session) die(error ?? 'no glass session resolved — operator must click the Browser chip');
73
131
  if (degraded || note) console.error(JSON.stringify({ warn: note ?? 'degraded', sessionId: session.id }));
74
132
  const browser = await chromium.connectOverCDP('http://127.0.0.1:' + session.port).catch(() => null);
75
133
  if (!browser) die('CDP connect failed on port ' + session.port);
76
- return { session, browser, ctx: browser.contexts()[0] };
134
+ return { session, browser, ctx: browser.contexts()[0], base };
77
135
  }
78
136
 
137
+ /** @param {import('playwright-core').BrowserContext} ctx @param {string} substr @returns {import('playwright-core').Page|undefined} */
79
138
  export function pick(ctx, substr) {
80
139
  return ctx.pages().find((p) => p.url().includes(substr));
81
- }
140
+ }
@@ -20,18 +20,19 @@
20
20
  * On any failure it prints the manual fallback hint (SKILL.md Step 1 —
21
21
  * the operator clicks the Browser chip; agents never open sessions).
22
22
  */
23
- import { connect, die } from './_connect.mjs';
23
+ import { connect, die, isFloorTab } from './_connect.mjs';
24
24
 
25
- const DSI = 'http://127.0.0.1:5175';
26
25
  const [cmd, ...args] = process.argv.slice(2);
27
26
 
28
- const { session, browser, ctx } = await connect();
27
+ // The Floor Seam (ADR 2026-11-05): connect resolves the live floor (DSI_UI
28
+ // env or probe); the DSI tab is its own ORIGIN — never a port literal.
29
+ const { session, browser, ctx, base } = await connect();
29
30
 
30
- let page = ctx.pages().find((p) => p.url().includes('5175'));
31
+ let page = ctx.pages().find((p) => isFloorTab(p.url(), base));
31
32
  if (cmd === 'open') {
32
33
  if (!page) {
33
34
  page = await ctx.newPage();
34
- await page.goto(args[0] ?? DSI, { waitUntil: 'domcontentloaded' }).catch(() => {});
35
+ await page.goto(args[0] ?? base, { waitUntil: 'domcontentloaded' }).catch(() => {});
35
36
  } else if (args[0] && args[0] !== page.url()) {
36
37
  await page.goto(args[0], { waitUntil: 'domcontentloaded' }).catch(() => {});
37
38
  }
@@ -39,7 +39,61 @@ import { chromium } from 'playwright-core';
39
39
  * @property {string} [error]
40
40
  */
41
41
 
42
- const DSI = 'http://127.0.0.1:5175';
42
+ // ── The Floor Seam (ADR "The Floor Seam" 2026-11-05, D1-D3) ────────────────
43
+ // The drivers dial the floor they SEE: DSI_UI env verbatim when set, else a
44
+ // parallel probe of the two known floor bases (5175 dev, 5174 build) with the
45
+ // D7 identity resolver as tiebreak. No port literal may survive in tab
46
+ // filtering — isFloorTab compares origins against the RESOLVED base (D2).
47
+
48
+ const DEFAULT_FLOOR_BASES = ['http://127.0.0.1:5175', 'http://127.0.0.1:5174'];
49
+
50
+ /** Candidate bases: the env seam wins verbatim (validated http), else both
51
+ * known floor bases — dev first so a dev-only machine keeps today's shape. */
52
+ export function floorCandidates(env = process.env) {
53
+ const ui = env.DSI_UI;
54
+ if (ui && /^http:\/\//.test(ui)) return [ui.replace(/\/$/, '')];
55
+ return [...DEFAULT_FLOOR_BASES];
56
+ }
57
+
58
+ /** Resolve the live floor: fetch /api/browser on every candidate in
59
+ * PARALLEL (3 s timeout each), then — one answer wins; both answer →
60
+ * pickSession per floor with the caller's identity, the floor holding the
61
+ * caller's OWN glass wins, a double-claim dies loud (D7 stands); none
62
+ * answer → die naming BOTH ports and BOTH launch commands. */
63
+ /** @param {{ env?: Record<string, string|undefined>, fetchImpl?: typeof fetch, identity?: string|undefined, pickSessionFn?: (s: GlassSession[]|null|undefined, id: string|undefined) => PickResult, dieFn?: (msg: string) => never, timeoutMs?: number }} [opts] @returns {Promise<{ base: string, reg: { sessions: GlassSession[] } }>} — never resolves undefined: every fall-through path dies. */
64
+ export async function resolveDsiBase({ env = process.env, fetchImpl = fetch, identity = undefined, pickSessionFn = pickSession, dieFn = die, timeoutMs = 3000 } = {}) {
65
+ const candidates = floorCandidates(env);
66
+ const probes = await Promise.all(candidates.map(async (base) => {
67
+ try {
68
+ const res = await fetchImpl(base + '/api/browser', { signal: AbortSignal.timeout(timeoutMs) });
69
+ if (!res.ok) return { base, reg: null };
70
+ return { base, reg: await res.json() };
71
+ } catch { return { base, reg: null }; }
72
+ }));
73
+ const answered = probes.filter((p) => p.reg?.sessions);
74
+ if (answered.length === 0) {
75
+ dieFn('no DSI floor answers — probed ' + candidates.join(', ') +
76
+ ' (dev: dsi dsh --sync --dev --playwright-mcp · build: dsi dsh --sync · or set DSI_UI=<floor url>)');
77
+ }
78
+ if (answered.length === 1) return answered[0];
79
+ // Both floors live — the caller's own glass picks the floor (D7 tiebreak).
80
+ const claims = answered.map((p) => ({ ...p, verdict: pickSessionFn(p.reg.sessions, identity) }));
81
+ const mine = claims.filter((c) => c.verdict.session);
82
+ if (mine.length === 1) return mine[0];
83
+ if (mine.length > 1) {
84
+ dieFn('a glass owned by you lives on BOTH floors (' + mine.map((m) => m.base).join(', ') +
85
+ ') — set DSI_UI=<floor url> to name the floor; refusing to guess (D7)');
86
+ }
87
+ return dieFn('both floors answer but neither holds YOUR glass (' + answered.map((m) => m.base).join(', ') +
88
+ ') — click the Browser chip in THIS conversation');
89
+ }
90
+
91
+ /** True when a tab url's ORIGIN is the resolved floor's origin (D2): the
92
+ * DSI tab is the floor's own page, whatever port it landed on. */
93
+ /** @param {string} url @param {string} base @returns {boolean} */
94
+ export function isFloorTab(url, base) {
95
+ try { return new URL(url).origin === new URL(base).origin; } catch { return false; }
96
+ }
43
97
 
44
98
  /** @param {string} msg @returns {never} */
45
99
  export function die(msg) {
@@ -77,23 +131,18 @@ export function pickSession(sessions, envId) {
77
131
  return { session: null, error: 'multiple live glasses and no explicit identity — pass --owner-session <id> (or GLASS_OWNER_SESSION) from the session id you already know; refusing to guess (D7)' };
78
132
  }
79
133
 
80
- /** @returns {Promise<{ session: GlassSession, browser: import('playwright-core').Browser, ctx: import('playwright-core').BrowserContext }>} */
134
+ /** @returns {Promise<{ session: GlassSession, browser: import('playwright-core').Browser, ctx: import('playwright-core').BrowserContext, base: string }>} */
81
135
  export async function connect() {
82
- let reg;
83
- try {
84
- reg = await (await fetch(DSI + '/api/browser')).json();
85
- } catch {
86
- die('DSI dev server not reachable on 5175 — floor not up (dsi dsh --sync --dev --playwright-mcp)');
87
- }
136
+ const { base, reg } = await resolveDsiBase({ env: process.env, identity: identityFrom(process.argv) });
88
137
  const { session, degraded, note, error } = pickSession(reg.sessions, identityFrom(process.argv));
89
138
  if (!session) die(error ?? 'no glass session resolved — operator must click the Browser chip');
90
139
  if (degraded || note) console.error(JSON.stringify({ warn: note ?? 'degraded', sessionId: session.id }));
91
140
  const browser = await chromium.connectOverCDP('http://127.0.0.1:' + session.port).catch(() => null);
92
141
  if (!browser) die('CDP connect failed on port ' + session.port);
93
- return { session, browser, ctx: browser.contexts()[0] };
142
+ return { session, browser, ctx: browser.contexts()[0], base };
94
143
  }
95
144
 
96
145
  /** @param {import('playwright-core').BrowserContext} ctx @param {string} substr @returns {import('playwright-core').Page|undefined} */
97
146
  export function pick(ctx, substr) {
98
147
  return ctx.pages().find((p) => p.url().includes(substr));
99
- }
148
+ }
@@ -23,22 +23,23 @@
23
23
  * EXIT DISCIPLINE: this script always process.exit()s — a driver that
24
24
  * played media once hung its caller for a full timeout (RCA trap 5).
25
25
  */
26
- import { connect, die, pick } from './_connect.mjs';
26
+ import { connect, die, pick, isFloorTab } from './_connect.mjs';
27
27
  import { norm, parseEvalArgs, matchAllTerms } from './_args.mjs';
28
28
 
29
29
  const argv = process.argv.slice(2);
30
- const { session, browser, ctx } = await connect();
30
+ const { session, browser, ctx, base } = await connect();
31
31
  const out = (v) => { console.log(JSON.stringify(v, null, 1)); return process.exit(0); };
32
32
 
33
33
  // target tab: url-substring when given; else last http(s) page that is
34
- // neither the glass placeholder nor :5175 (contract, not position — D2).
34
+ // neither the glass placeholder nor the FLOOR's own page — origin compare
35
+ // against the resolved base (The Floor Seam ADR D2; never a port literal).
35
36
  const target = (substr) => {
36
37
  if (substr) {
37
38
  const p = pick(ctx, substr);
38
39
  if (!p) die('no tab matching "' + substr + '" — run: pages');
39
40
  return p;
40
41
  }
41
- const pages = ctx.pages().filter((p) => p.url().startsWith('http') && !p.url().includes(':5175'));
42
+ const pages = ctx.pages().filter((p) => p.url().startsWith('http') && !isFloorTab(p.url(), base));
42
43
  if (!pages.length) die('no non-DSI tab to drive — run: glass-url.mjs open <url>');
43
44
  return pages[pages.length - 1];
44
45
  };
package/README.md CHANGED
@@ -1,12 +1,37 @@
1
- # DeepSeek Insight
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mitmplay/dsh-insight/refs/heads/main/docs/images/dsi-insight-pich.png"></img>
3
+ </p>
2
4
 
3
- > [DeepSeek Insight](https://github.com/mitmplay/dsh-insight) (**DSI**) **See your multiple agents run on the floor (agent, sub-agent, etc)**, each **conversation on the floor are updated instantly**.
5
+ <p align="center">
6
+ A different style opinionate <strong>DSH web client</strong> - aka <strong>DSI</strong>
7
+ <p>
8
+ <p align="center">
9
+ Screenshot: Sidebar, Workspace-explorer, Agent, Glass-browser, Subagent
10
+ <p>
4
11
 
5
- > A `Control room` built on [Deepseek Harness's](https://github.com/deepseek-ai/deepseek-harness) (**DSH**) **records**: any (running) session, show side by side on the floor; **Lineage tree** of **parent to sub-agents**; Clean conversation with **tool-call(s) chips** with status **+** auto-expand **thingking chips**.
12
+ #### Where you can see mulltiple **agent** / **sub-agent**, plus+ non conversation panels:
13
+ - keys panel: `Agent`, `Subagent`, `Workspace Explorer`, `Glass Browser`, `Terminal`,
14
+ - management panel: `Prompt`, `Skill`, `Plugins` and `Configs` Manager`
6
15
 
7
- > A `Prompt Input` with **Slash-command(/)**, **Macros(!)** and **Auto-complete(?)**. Prompt-manager you can search it and to manage macros & autocomplete; user-prompts worth keeping is one-click-away tobe reuse.
16
+ > [!NOTE]
17
+ > ["in a nutshell" - DSI](https://github.com/mitmplay/dsh-insight) make `Harness` present multiple conversations (`agent` or `sub-agent`), `keys` and `management` panels **side-by-side** `INSIDE Container` that can be **re-size** or **scale**.
8
18
 
9
- ## Slash commands are available to execute various operations:
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ npx @deepseek-insight/dsi@latest dsh --sync --zai
23
+ # Open http://localhost:5174
24
+ ```
25
+
26
+ | Argument | What it does |
27
+ |---|---|
28
+ | `dsh` | starts **DSH** (`dsh web`) and **DSI** together, as one pair and runs it |
29
+ | `--sync` | copies DSH token to DSI config — **one window, one Ctrl-C stops both** |
30
+ | `--zai` | **optional.** It bootstraps the GLM (Z.AI) provider into the harness settings (`~/.dsh/settings.yaml`). **If you do not have a GLM account, simply leave it off** — DSI will use whatever provider your harness is already configured with |
31
+
32
+ When it is up, your browser opens the page by itself (default `http://127.0.0.1:5174`).
33
+
34
+ ## Various operations can be Enter in Composer:
10
35
  - `/dsi-terminal` — Open a terminal session
11
36
  - `/dsi-prompts` — Open prompt manager
12
37
  - `/dsi-plugins` — Open plugin manager
@@ -48,22 +73,26 @@ The workspace explorer is DSI's window into the folder your agents are working i
48
73
 
49
74
  ---
50
75
 
51
- ## Quick Start
76
+ ### Ports & the browser brain, in plain words
52
77
 
53
- ```bash
54
- npx @deepseek-insight/dsi@latest dsh --sync --zai
55
- # Open http://localhost:5174
56
- ```
78
+ One "floor" is a pair: **DSH** (the harness host) and **DSI** (the page you watch). Each sits on its own port, and each port can be moved — handy when you want a second floor beside one that is already running.
57
79
 
58
- - **`npx @deepseek-insight/dsi@latest`** — get latest DSI.
59
- - `dsh` — starts **DSH** (`dsh web`) and **DSI** together, as one pair and runs it.
60
- - `--sync` — copies DSH token to DSI config — **one window, one Ctrl-C stops both**.
61
- - `--zai` — **optional.** It bootstraps the GLM (Z.AI) provider into the harness settings (`~/.dsh/settings.yaml`). **If you do not have a GLM account, simply leave it off** — DSI will use whatever provider your harness is already configured with.
80
+ | What | Default | How to move it | Plain meaning |
81
+ |---|---|---|---|
82
+ | DSH host | `3080` | `--port <n>` | the harness brain of the floor |
83
+ | DSI page | `5174` | `PORT=<n>` env var | the page you open in the browser |
84
+ | Browser brain | `9515` | `DSI_MCP_PORT=<n>` env var | the MCP endpoint agents use to drive the glass browser |
62
85
 
86
+ **The browser brain.** By default the glass browser's assistant (the MCP brain that lets an agent click and type in the browser) runs as its **own separate helper process** — if it fails, you lose only the glass panel. Adding `--playwright-mcp` moves that brain **inside the DSI page server**: one less background process and a faster start, but a brain failure can now touch the page server itself. DSI prints a banner saying so whenever you use the flag — drop the flag to go back to the helper process.
63
87
 
88
+ **Example — a second, self-contained floor beside a live one** (nothing on the running floor is touched):
64
89
 
90
+ ```bash
91
+ PORT=5176 DSI_MCP_PORT=19515 npx @deepseek-insight/dsi@latest dsh --sync --playwright-mcp --port 3081
92
+ # Page: http://localhost:5176 · host: 3081 · brain: http://127.0.0.1:19515/sse
93
+ ```
65
94
 
66
- When it is up, your browser opens the page by itself (default `http://127.0.0.1:5174`).
95
+ **Second floors are fully drivable.** At boot, a floor stamps its own addresses (`DSI_UI`, `DSI_MCP_URL`) into everything it starts — so agents in *that* floor's conversations drive *that* floor's glass automatically, and the preset brain entry follows the stamped port. Nothing on the primary floor changes: without a stamp (a bare `dsh web` boot), the brain falls back to `9515` and the drivers probe `5175` → `5174` exactly as before. One conversation = one glass owner; the driver refuses to touch another conversation's glass, and a session busy in the live floor shows `writer-held` if you try to prompt it from the second window (watch it there, prompt it on its own floor).
67
96
 
68
97
  ## License
69
98
 
package/bin/dsi.mjs CHANGED
@@ -33,7 +33,9 @@ import { parse, stringify } from 'yaml';
33
33
  import { mirrorSkills } from './lib/mirror-skills.mjs';
34
34
  import { dshLauncherCmd, parsePluginArgs, takeoverFloorPorts, hostBounceDecision, floorUiPort, parseDevFloorPids, launchUrl, tokenFromSettings } from './lib/plugin-chain.mjs';
35
35
  import { parseDevArg, devModeEnv, modeFromBoolean, resolveBounceMode, extractInspectArgs, parseInspectArgsFromEnv } from './lib/dev-mode.mjs';
36
+ import { brainBannerLines } from './lib/mcp-banner.mjs';
36
37
  import { addZaiProvider as addZaiPatch, zaiPatchPath } from './lib/zai-patch.mjs';
38
+ import { ensureGlassBrowser } from './lib/glass-browser-bootstrap.mjs';
37
39
 
38
40
  // Layman-facing help: wrapped near 76 columns, continuation lines indented,
39
41
  // the recommended one-liner first. --zai is optional on purpose — no GLM
@@ -177,15 +179,32 @@ if (command === 'dsh') {
177
179
  const rest = process.argv.slice(4);
178
180
  const withZai = rest.includes('--zai');
179
181
  const withOv = rest.includes('--ov');
180
- // The Glass Browser (ADR-0029 D7): --playwright-mcp opts the DEV floor
181
- // into the in-process MCP brain — a dsi-side flag, stripped before the
182
- // host spawn (dsh web would refuse it) and carried to vite as an env
183
- // marker, the exact idiom of DSI_DEV_IN_PROCESS.
182
+ // The Glass Browser (ADR-0029 D7, amendment A6 D7c): --playwright-mcp
183
+ // opts ANY floor into the in-process MCP brain — a dsi-side flag,
184
+ // stripped before the host spawn (dsh web would refuse it) and carried
185
+ // to the server as an env marker (DSI_MCP_IN_PROCESS, dev-mode.mjs).
184
186
  const withPlaywrightMcp = rest.includes('--playwright-mcp');
185
187
  const restNoMcp = withPlaywrightMcp ? rest.filter((a) => a !== '--playwright-mcp') : rest;
188
+ // A6 D7d: say out loud what the flag chose, in plain language, BEFORE
189
+ // any child spawns — an unannounced behavior change is a silent trap.
190
+ if (withPlaywrightMcp) {
191
+ const { mode: bannerMode } = parseDevArg(restNoMcp);
192
+ for (const line of brainBannerLines(bannerMode !== 'build')) console.log(line);
193
+ }
186
194
  if (withZai) addZaiProvider();
187
195
  if (withOv) runOvPlugin();
188
196
  runSkillsMirror();
197
+ // The Glass Bootstrap (ADR 2026-11-05, D1): probe the glass browser
198
+ // BEFORE the pair spawns — system Chrome first, cached chromium second,
199
+ // a one-time pinned fetch last (soft failure; DSI_SKIP_BROWSER_INSTALL=1
200
+ // skips). The bounce chain never fetches — only this first-run branch.
201
+ const glass = ensureGlassBrowser({
202
+ packageRoot,
203
+ spawnSync,
204
+ env: process.env,
205
+ log: (line) => console.log(line)
206
+ });
207
+ if (glass.action === 'fetched') console.log(`dsi dsh: chromium ${glass.reason} — the glass browser is ready`);
189
208
  // --dev parse (D4): 'in-process' | 'pair' consumed as a value; bare
190
209
  // --dev stays 'pair'. dsi-side flags stripped before the host spawn.
191
210
  const { mode, rest: hostArgs0 } = parseDevArg(restNoMcp);
@@ -365,6 +384,18 @@ function setDshAuthToken(token) {
365
384
  * falls back to the registry's latest tag). An exact pin, not a range: a
366
385
  * range drifts to whatever the registry resolves, and the pin exists
367
386
  * precisely because latest can change the API contract under DSI. */
387
+ /** The Stamped Floor (ADR 2026-11-05, D1): every floor announces its own
388
+ * addresses into the env it gives its children — the UI base (the Floor
389
+ * Seam D1a seam, finally with a writer) and the brain URL (the preset's
390
+ * !!js expression reads DSI_MCP_URL with a 9515 bare-boot fallback). The
391
+ * launcher is the only party that knows both ports at spawn time. */
392
+ function stampedFloorEnv(uiPort) {
393
+ return {
394
+ DSI_UI: 'http://127.0.0.1:' + uiPort,
395
+ DSI_MCP_URL: 'http://127.0.0.1:' + (process.env.DSI_MCP_PORT ?? 9515) + '/sse'
396
+ };
397
+ }
398
+
368
399
  function dshWebVersion() {
369
400
  const doc = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
370
401
  const version = doc?.dsh?.webVersion;
@@ -487,9 +518,9 @@ async function runSyncedPair(extraArgs, dev = 'build', execArgv = [], opts = {})
487
518
  // host lifecycle (vite close hook stops it; one Ctrl-C, no orphan).
488
519
  const host = inProcess ? null : spawn('/bin/sh', [script, ...extraArgs], {
489
520
  stdio: 'inherit',
490
- env: { ...process.env, DSH_WEB_VERSION: dshWebVersion() }
521
+ env: { ...process.env, DSH_WEB_VERSION: dshWebVersion(), ...stampedFloorEnv(uiPort) }
491
522
  });
492
- const ui = mode === 'build' ? startDsiServer(uiPort, dshBase) : startDsiDevServer(uiPort, dshBase, mode, execArgv, opts);
523
+ const ui = mode === 'build' ? startDsiServer(uiPort, dshBase, opts) : startDsiDevServer(uiPort, dshBase, mode, execArgv, opts);
493
524
  // The browser follows the UI: the built page (5174) — or, in --dev mode,
494
525
  // the dev server (5175). Gated on the HOST actually listening (bug
495
526
  // 2026-10-17): the fixed 2s timer lost every race the host booted slower
@@ -754,6 +785,7 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined, mode = 'pa
754
785
  DSI_DEV: '1',
755
786
  // D4/D6: the mode rides as env, never past the CLI to the host.
756
787
  ...devModeEnv(mode, { playwrightMcp: opts.playwrightMcp === true }),
788
+ ...stampedFloorEnv(process.env.PORT ?? defaultPort),
757
789
  // The Faithful Bounce (ADR 2026-10-13, D1): the inspector flags are
758
790
  // part of the launch record — stamped so every restorer (the bounce
759
791
  // boot via ps -E, the delayed re-serve via inherited env) can put
@@ -764,7 +796,7 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined, mode = 'pa
764
796
  });
765
797
  }
766
798
 
767
- function startDsiServer(defaultPort = '5174', dshBase = undefined) {
799
+ function startDsiServer(defaultPort = '5174', dshBase = undefined, opts = {}) {
768
800
  const serverEntry = path.join(packageRoot, 'build', 'index.js');
769
801
  if (!existsSync(serverEntry)) {
770
802
  console.error(`dsi: no build found at ${serverEntry} — run \`pnpm run build\` first`);
@@ -779,6 +811,11 @@ function startDsiServer(defaultPort = '5174', dshBase = undefined) {
779
811
  // First-start bootstrap seam: the server clones template/.dsi to ~/.dsi
780
812
  // when the operator home is absent (src/lib/server/settings-document.ts).
781
813
  DSI_TEMPLATE_PATH: process.env.DSI_TEMPLATE_PATH ?? path.join(packageRoot, 'template', '.dsi'),
814
+ // A6 D7c: the brain marker rides EVERY floor, build included — the
815
+ // vite child got it via startDsiDevServer's devModeEnv spread; the
816
+ // built server was silently dropping it (caught by the live gate).
817
+ ...devModeEnv('build', { playwrightMcp: opts.playwrightMcp === true }),
818
+ ...stampedFloorEnv(process.env.PORT ?? defaultPort),
782
819
  ...(dshBase !== undefined ? { DSH_BASE_URL: dshBase } : {})
783
820
  }
784
821
  });
@@ -54,9 +54,15 @@ export function parseDevArg(rest) {
54
54
  * @returns {Record<string, string>}
55
55
  */
56
56
  export function devModeEnv(mode, opts = {}) {
57
- if (mode === 'build') return {};
58
57
  /** @type {Record<string, string>} */
59
- const env = { DSI_DEV: '1', DSI_DEV_MODE: mode };
58
+ const env = {};
59
+ // ADR-0029 amendment A6 D7c: the glass brain's placement is the operator's
60
+ // choice on EVERY floor — build included. DSI_DEV/DSI_DEV_MODE stay
61
+ // strictly dev-only (the bounce chain probes them as dev-floor markers).
62
+ if (opts.playwrightMcp) env.DSI_MCP_IN_PROCESS = '1';
63
+ if (mode === 'build') return env;
64
+ env.DSI_DEV = '1';
65
+ env.DSI_DEV_MODE = mode;
60
66
  if (mode === 'in-process') {
61
67
  env.DSI_DEV_IN_PROCESS = '1';
62
68
  // ADR-0025 Wave 3 (real-floor gate finding, 2026-10-14): the bounce
@@ -67,9 +73,6 @@ export function devModeEnv(mode, opts = {}) {
67
73
  // that carries the handle, so in-process floors default to it.
68
74
  env.DSH_HOST_INTROSPECT = '1';
69
75
  }
70
- // ADR-0029 D7: the glass browser's MCP brain runs in-process (opt-in,
71
- // dev floor only — the tandem/production floors keep the stdio child)
72
- if (opts.playwrightMcp) env.DSI_MCP_IN_PROCESS = '1';
73
76
  return env;
74
77
  }
75
78
 
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Glass browser bootstrap (ADR "The Glass Bootstrap" 2026-11-05, D1-D4):
3
+ * decide whether the floor's glass browser has a binary it can launch, and
4
+ * fetch the pinned chromium once when it does not. Pure decision logic —
5
+ * every effect (spawnSync, env, log, platform) is injected, so the unit
6
+ * pins exercise the full decision tree without a network or a real cache.
7
+ *
8
+ * Probe order IS the launch order (D2): registry.ts tries system Google
9
+ * Chrome (channel: 'chrome') FIRST and falls back to the bundled chromium,
10
+ * so a machine with Chrome must never pay for a chromium download.
11
+ *
12
+ * Soft failure is the contract (D4): a failed or skipped fetch warns and
13
+ * returns — the floor boots; only the glass is affected, and the launch
14
+ * site's honest error (D5) names the cure.
15
+ */
16
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import path from 'node:path';
19
+
20
+ /** Chrome candidates probed on PATH (Linux shape). macOS has no google-chrome
21
+ * on PATH — the .app bundle is the truth (checked separately). */
22
+ const CHROME_PATH_CANDIDATES = ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'];
23
+
24
+ /** The pinned playwright family, derived from this package's own
25
+ * playwright-core dependency (D3: no second hardcoded pin may exist).
26
+ * Returns null when the pin cannot be read — the caller then warns and
27
+ * skips the fetch; a null pin never spawns npx. */
28
+ /** @param {string} packageRoot @returns {string | null} */
29
+ export function pinnedPlaywrightVersion(packageRoot) {
30
+ try {
31
+ const doc = /** @type {any} */ (JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')));
32
+ const version = doc?.dependencies?.['playwright-core'];
33
+ return typeof version === 'string' && /^\d+\.\d+\.\d+/.test(version) ? version : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** True when a system Google/Chromium browser is resolvable: PATH probes on
40
+ * Linux, the .app bundle on macOS (where PATH probing is always blind). */
41
+ /** @param {(cmd: string, args?: string[], opts?: { stdio?: 'pipe' | 'inherit'; timeout?: number }) => { status: number | null }} spawnSync
42
+ * @param {string} [platform]
43
+ * @param {(p: string) => boolean} [exists]
44
+ * @returns {boolean} */
45
+ export function findSystemChrome(spawnSync, platform = process.platform, exists = existsSync) {
46
+ if (platform === 'darwin') {
47
+ return exists('/Applications/Google Chrome.app');
48
+ }
49
+ for (const name of CHROME_PATH_CANDIDATES) {
50
+ try {
51
+ if (spawnSync('which', [name], { stdio: 'pipe' }).status === 0) return true;
52
+ } catch {
53
+ // no which (or no sh) — the next candidate still probes
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
59
+ /** True when ANY chromium-family build sits in the playwright cache (either
60
+ * platform root). The npx fetch is the revision authority — this probe is
61
+ * the cheap hint that avoids re-downloading what is already present. */
62
+ /** @param {Record<string, string | undefined>} [env]
63
+ * @param {(p: string) => string[]} [readdir]
64
+ * @returns {boolean} */
65
+ export function playwrightCacheHasChromium(env = process.env, readdir = readdirSync) {
66
+ const home = env.HOME ?? homedir();
67
+ const roots = [
68
+ path.join(home, 'Library', 'Caches', 'ms-playwright'),
69
+ path.join(home, '.cache', 'ms-playwright')
70
+ ];
71
+ for (const root of roots) {
72
+ try {
73
+ if (readdir(root).some((entry) => entry.startsWith('chromium'))) return true;
74
+ } catch {
75
+ // unreadable/absent root — the next root still probes
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+
81
+ /**
82
+ * The D2 decision tree. Sync by construction: every step is a stat, a
83
+ * readdir, or one spawnSync child. Returns a verdict the launcher logs:
84
+ * { action: 'skip', reason } — a browser exists (or the hatch fired)
85
+ * { action: 'fetched', reason } — the pinned chromium landed in the cache
86
+ * { action: 'warned', reason } — no browser and the fetch failed/skipped;
87
+ * the floor still boots (D4)
88
+ */
89
+ /** @param {{
90
+ * packageRoot: string,
91
+ * spawnSync: (cmd: string, args?: string[], opts?: { stdio?: 'pipe' | 'inherit'; timeout?: number }) => { status: number | null },
92
+ * env?: Record<string, string | undefined>,
93
+ * log?: (msg: string) => void,
94
+ * platform?: string
95
+ * }} options
96
+ * @returns {{ action: 'skip' | 'fetched' | 'warned', reason: string }} */
97
+ export function ensureGlassBrowser({ packageRoot, spawnSync, env = process.env, log = () => {}, platform = process.platform }) {
98
+ if (env.DSI_SKIP_BROWSER_INSTALL === '1') {
99
+ log('dsi dsh: glass browser check skipped (DSI_SKIP_BROWSER_INSTALL=1)');
100
+ return { action: 'skip', reason: 'env-escape-hatch' };
101
+ }
102
+ if (findSystemChrome(spawnSync, platform)) {
103
+ return { action: 'skip', reason: 'system-chrome' };
104
+ }
105
+ if (playwrightCacheHasChromium(env)) {
106
+ return { action: 'skip', reason: 'cached-chromium' };
107
+ }
108
+ const pin = pinnedPlaywrightVersion(packageRoot);
109
+ if (pin === null) {
110
+ log('dsi dsh: glass browser — no playwright-core pin readable in package.json, skipping the chromium fetch');
111
+ return { action: 'warned', reason: 'no-pin' };
112
+ }
113
+ // The loud line comes BEFORE the fetch child (D1): the silent-minutes
114
+ // bug (2026-11-06) must never repeat on this stage.
115
+ log(`dsi dsh: glass browser missing — downloading chromium (~150 MB, one-time, playwright@${pin})…`);
116
+ let result;
117
+ try {
118
+ result = spawnSync('npx', ['--yes', `playwright@${pin}`, 'install', 'chromium'], {
119
+ stdio: 'inherit',
120
+ timeout: 300000
121
+ });
122
+ } catch (error) {
123
+ const message = /** @type {any} */ (error)?.message ?? error;
124
+ log(`dsi dsh: chromium fetch failed (${message}) — the floor still boots; the glass needs: npx playwright@${pin} install chromium`);
125
+ return { action: 'warned', reason: 'fetch-error' };
126
+ }
127
+ if ((result?.status ?? 1) === 0) {
128
+ return { action: 'fetched', reason: `playwright@${pin}` };
129
+ }
130
+ log(`dsi dsh: chromium fetch failed (exit ${result?.status}) — the floor still boots; the glass needs: npx playwright@${pin} install chromium`);
131
+ return { action: 'warned', reason: 'fetch-failed' };
132
+ }
@@ -0,0 +1,26 @@
1
+ // mcp-banner — A6 D7d: when --playwright-mcp is present, say out loud what
2
+ // the operator just chose, in plain language, BEFORE any child spawns.
3
+ // Extracted as a module so the exact text is unit-pinned (T3-T).
4
+
5
+ /** @param {boolean} devFloor - true when a --dev mode was chosen
6
+ * @returns {string[]} banner lines (never empty; the caller prints each) */
7
+ export function brainBannerLines(devFloor) {
8
+ const blast = devFloor
9
+ ? 'a brain failure can now touch this dev floor server.'
10
+ : 'a brain failure can now touch this page server — not just the glass.';
11
+ return [
12
+ '',
13
+ '────────── playwright-mcp: in-process browser brain ──────────',
14
+ 'You asked for the browser-assist brain to run INSIDE the DSI page',
15
+ 'server instead of as its own separate background helper.',
16
+ '',
17
+ 'What that means:',
18
+ ' + one less background process on your machine, faster start',
19
+ ' - ' + blast,
20
+ ' Before, it could only lose the glass panel, never the server.',
21
+ '',
22
+ 'Drop the --playwright-mcp flag to go back to the old helper process.',
23
+ '───────────────────────────────────────────────────────────────',
24
+ ''
25
+ ];
26
+ }