feat: MIST Living System — Chrome Extension Agentic Core - #1
feat: MIST Living System — Chrome Extension Agentic Core#1Mellowambience wants to merge 1 commit into
Conversation
- Add MV3 manifest (new-tab, side-panel, background service worker) - MIST brain: memory, adaptation, growth, repair engines - tRPC WebSocket bridge to clawd - Subspace new-tab page wired to live MIST backend - Content script for proactive page analysis - Sidebar panel (persistent across tabs) - Tool registry (dynamic, self-expanding) - OmniInput agentic routing (no-prefix = MIST decides) - ContextDock with MistPageRead proactive intel panel - Updated package.json with Chrome extension deps This is the living system commit. MIST now has memory, adaptation, growth, and self-repair built into the shell.
📝 WalkthroughWalkthroughA comprehensive Chrome extension implementing the MIST (Memory, Adaptation, Growth, Repair) living system architecture. Introduces backend service worker orchestration with IndexedDB memory, WebSocket bridge to clawd service, autonomous tool discovery engine, pattern-based routing, content scripts for page analysis and action execution, sidebar and new-tab UI components, and self-healing repair mechanisms. Changes
Sequence DiagramssequenceDiagram
participant Bridge as MISTBridge
participant Growth as MISTGrowth
participant Memory as MISTMemory
participant Repair as MISTRepair
Note over Growth: discoverTool Flow
Growth->>Bridge: detectAPI(pageContext)
Bridge-->>Growth: apiSpec
Growth->>Bridge: generateTool(apiSpec)
Bridge-->>Growth: toolSpec
Growth->>Growth: _testTool(toolSpec)
Growth->>Bridge: executeTool(name, params)
Bridge-->>Growth: result
Growth->>Growth: registerTool(toolSpec)
Growth->>Memory: updatePattern(tool, success)
Memory-->>Growth: ✓
Growth-->>Growth: return {discovered: true, tool}
sequenceDiagram
participant ServiceWorker as Service Worker
participant Repair as MISTRepair
participant Bridge as MISTBridge
participant Memory as MISTMemory
Note over Repair: Repair Watchdog Flow
ServiceWorker->>Repair: runCheck()
par Health Checks
Repair->>Bridge: ping()
Bridge-->>Repair: response
and
Repair->>Memory: recall(query, 1)
Memory-->>Repair: results
end
alt Health OK
Repair->>Memory: logOutcome(repair_success)
Memory-->>Repair: ✓
else Health Failed
Repair->>Bridge: connect()
Bridge-->>Repair: ✓ or error
Repair->>Memory: logOutcome(repair_attempt)
alt Failures > Threshold
Repair->>ServiceWorker: BRIDGE_DOWN badge update
ServiceWorker-->>Repair: ✓
end
end
sequenceDiagram
participant Sidebar as Sidebar UI
participant ServiceWorker as Service Worker
participant Bridge as MISTBridge
participant ContentScript as Content Script
participant Adaptation as MISTAdaptation
Note over Sidebar: Omni-Input & Action Execution Flow
Sidebar->>ServiceWorker: OMNI_INPUT (text)
ServiceWorker->>Bridge: routeIntent(text)
Bridge-->>ServiceWorker: {target, action}
ServiceWorker->>Bridge: chat/executeTool(...)
Bridge-->>ServiceWorker: response
alt ExecuteTool Action
ServiceWorker->>ContentScript: MIST_PAGE_ACTION
ContentScript-->>ServiceWorker: {success, result}
ServiceWorker->>Adaptation: logOutcome(action, outcome)
Adaptation-->>ServiceWorker: ✓
end
ServiceWorker-->>Sidebar: response text
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6eedac42d9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| output: { | ||
| entryFileNames: (chunk) => { | ||
| if (chunk.name === 'service_worker') return 'background/service_worker.js'; | ||
| if (chunk.name.startsWith('content_')) return `content/${chunk.name.replace('content_', '')}.js`; |
There was a problem hiding this comment.
Emit ghostline content script at manifest path
The content-script naming rule emits content/${chunk.name.replace('content_', '')}.js, so the content_ghostline entry is output as content/ghostline.js, but the manifest points to content/ghostline_overlay.js. When loading the built dist/ extension, that manifest path does not exist, so the Ghostline detector is not injected (and can block extension loading in Chrome because a declared content script file is missing).
Useful? React with 👍 / 👎.
| }); | ||
|
|
||
| // Dynamic import of React app (built separately via webpack/vite) | ||
| import('./SubspaceApp.js').then(({ default: init }) => init(document.getElementById('subspace-app'))); |
There was a problem hiding this comment.
Replace unresolved SubspaceApp dynamic import
This imports ./SubspaceApp.js, but there is no SubspaceApp module in this commit (repo-wide search only finds this reference). That leaves the new-tab entrypoint unable to initialize: bundling fails on unresolved import, or the page throws a module-load error at runtime if it slips through.
Useful? React with 👍 / 👎.
| this.ws.send(JSON.stringify(message)); | ||
| } else { | ||
| // Queue message — repair engine will reconnect and flush | ||
| this.messageQueue.push(message); |
There was a problem hiding this comment.
Remove timed-out requests from reconnect queue
When offline, requests are queued here, but the timeout handler only deletes pendingRequests; it never removes the queued message. After reconnect, the stale message is still flushed and executed even though the caller already got a timeout (and may have retried), which can duplicate side-effecting operations like tool execution/build actions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (5)
content/ghostline_overlay.js-42-51 (1)
42-51:⚠️ Potential issue | 🟡 MinorTracker blocking is not implemented—only a flag is set.
The
GHOSTLINE_ACTIONhandler setswindow._mistTrackerBlocked = truebut nothing reads this flag or actually blocks trackers. The comment says "best effort via override" but there's no override logic.Either implement actual blocking (e.g., using
MutationObserverto remove tracker scripts) or clarify in the comment that this is a placeholder for future implementation.Would you like me to generate an implementation that uses
MutationObserverto block tracker script injections?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@content/ghostline_overlay.js` around lines 42 - 51, The handler for GHOSTLINE_ACTION currently only sets window._mistTrackerBlocked without any blocking behavior; update the GHOSTLINE_ACTION 'block_trackers' branch (the window.addEventListener('message' callback and its block_trackers case) to either implement real blocking or mark as placeholder: implement a MutationObserver that watches for added <script> elements and inline script content matching common tracker domains/patterns and removes or neutralizes them when window._mistTrackerBlocked is true (attach observer when flag set and disconnect on session end), or if you choose the placeholder route, replace the comment with an explicit TODO and log that this is a stub so it’s not misleading. Ensure references include window._mistTrackerBlocked, 'GHOSTLINE_ACTION', and the block_trackers action so reviewers can find the change.subspace/index.html-7-8 (1)
7-8:⚠️ Potential issue | 🟡 MinorDuplicate CSS links—one will 404 in the built extension.
Two stylesheet links reference the same file:
../src/subspace/subspace.css— points to source directory, won't exist indist/subspace.css— correct relative path after Vite buildsThe first link will cause a 404 error in the loaded extension and should be removed.
🐛 Proposed fix
- <link rel="stylesheet" href="../src/subspace/subspace.css" /> <link rel="stylesheet" href="subspace.css" />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subspace/index.html` around lines 7 - 8, Remove the duplicate, incorrect stylesheet link that points to the source dir so the extension won't 404: delete the <link rel="stylesheet" href="../src/subspace/subspace.css" /> entry in subspace/index.html and keep only the build-correct <link rel="stylesheet" href="subspace.css" />; ensure there are no other references to ../src/subspace/subspace.css in this file.subspace/subspace.js-12-17 (1)
12-17:⚠️ Potential issue | 🟡 MinorBridge alert is shown but never hidden on recovery.
When
BRIDGE_DOWNis received, the alert becomes visible. However, there's no corresponding message type to clear the alert when the bridge reconnects, leaving the alert permanently visible until page refresh.Consider adding a
BRIDGE_UPhandler or similar to clear the alert.♻️ Suggested addition
if (message.type === 'BRIDGE_DOWN') { const alert = document.querySelector('.bridge-alert'); if (alert) { alert.textContent = message.message; alert.classList.add('visible'); } } + if (message.type === 'BRIDGE_UP') { + const alert = document.querySelector('.bridge-alert'); + if (alert) { + alert.classList.remove('visible'); + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subspace/subspace.js` around lines 12 - 17, The BRIDGE_DOWN case makes the .bridge-alert visible but there is no handler to clear it on recovery; add a BRIDGE_UP (or BRIDGE_RECOVERED) branch in the same message handling logic (where message.type is checked) that selects the same '.bridge-alert' element and removes the 'visible' class and clears or resets alert.textContent; ensure this logic lives alongside the existing BRIDGE_DOWN handling so alerts are hidden when the bridge reconnects.tools/registry.js-62-79 (1)
62-79:⚠️ Potential issue | 🟡 MinorInconsistent return shape—matched routes include
tool/action, default does not.When a tool matches, the function returns
{ target, tool, action }. When no match is found, it returns only{ target: 'mist_chat' }. Consumers must handle both shapes, which can lead to bugs if they expecttooloractionto always be present.🐛 Proposed fix for consistent return shape
// Default: send to MIST chat - return { target: 'mist_chat' }; + return { target: 'mist_chat', tool: null, action: null }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/registry.js` around lines 62 - 79, The routeIntent function returns {target, tool, action} on a match but only {target: 'mist_chat'} by default; change the default return in routeIntent to include the same keys (e.g., return { target: 'mist_chat', tool: null, action: null }) so consumers always receive a consistent shape; update any references expecting BUILTIN_TOOLS, routeIntent, or the 'mist_chat' target to handle null tool/action if needed.sidebar/index.html-125-126 (1)
125-126:⚠️ Potential issue | 🟡 MinorRemove the hardcoded user name from the default greeting.
Every install will render
"Hi Mars"until the runtime replaces it, which makes the sidebar feel personalized to the wrong user. Keep the bootstrap text generic and inject a real name only when you actually have one.Suggested fix
- <div class="message mist">Hi Mars. I'm already looking at this page. What do you need?</div> + <div class="message mist">I'm already looking at this page. What do you need?</div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sidebar/index.html` around lines 125 - 126, The default greeting currently hardcodes "Hi Mars" inside the chat message element (the div with class "message mist" inside the element with id "chat-area"); replace that hardcoded name with a generic bootstrap string like "Hi there" or "Hello — what do you need?" and update the runtime injection logic to only insert a real user name into that element when a validated name is available (do not render the injected name by default). Ensure the change targets the div.message.mist (the bootstrap message) and the code path that replaces its content at runtime.
🧹 Nitpick comments (5)
content/ghostline_overlay.js (1)
19-34: Potential duplicate tracker entries when a script has both a trackersrcand inline analytics code.A script element matching a tracker domain (lines 23-27) could also contain
_ga/fbq/gtagin its textContent, resulting in duplicate entries in thetrackersarray.♻️ Proposed fix to prevent duplicates
for (const script of scripts) { const src = script.src || ''; + let matched = false; for (const tracker of KNOWN_TRACKER_DOMAINS) { if (src.includes(tracker)) { trackers.push({ type: 'script', tracker, src }); + matched = true; break; } } // Inline script scan - const content = script.textContent || ''; - if (content.includes('_ga') || content.includes('fbq') || content.includes('gtag')) { - trackers.push({ type: 'inline', tracker: 'analytics_pixel' }); + if (!matched) { + const content = script.textContent || ''; + if (content.includes('_ga') || content.includes('fbq') || content.includes('gtag')) { + trackers.push({ type: 'inline', tracker: 'analytics_pixel' }); + } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@content/ghostline_overlay.js` around lines 19 - 34, When scanning scripts in the for loop, a single <script> can produce two entries (one from the src match against KNOWN_TRACKER_DOMAINS and one from the inline check for _ga/fbq/gtag); update the loop that iterates over document.scripts (the code that pushes into trackers) to record whether a src-based tracker was already added for that script (e.g., a boolean foundSrcTracker set when pushing { type: 'script', tracker, src } in the KNOWN_TRACKER_DOMAINS check) and only perform the inline textContent check and push for { type: 'inline', tracker: 'analytics_pixel' } if foundSrcTracker is false (or otherwise ensure uniqueness per script) so you never push duplicate entries for the same script element.subspace/subspace.css (1)
27-27: Remove quotes around font family name to satisfy Stylelint.Stylelint flags
"Inter"as unnecessarily quoted. While CSS allows quotes on font names, the project's Stylelint configuration expects unquoted names when they don't contain special characters.📝 Proposed fix
- font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; + font-family: Inter, 'SF Pro Display', system-ui, sans-serif;Note:
'SF Pro Display'can remain quoted since it contains spaces.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subspace/subspace.css` at line 27, The font-family declaration currently quotes "Inter", which Stylelint flags; update the font-family line (the font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; declaration) to remove quotes around Inter so it becomes Inter (leave 'SF Pro Display' quoted because it contains spaces), ensuring the line reads Inter, 'SF Pro Display', system-ui, sans-serif to satisfy Stylelint.tools/registry.js (1)
65-75: Keyword matching is order-dependent and may produce unexpected results with overlapping phrases.The first-match-wins approach means tool ordering in
BUILTIN_TOOLSaffects routing. For example, if a user types "save to github repo", andsave_to_drive(with keyword "save this") appears beforegithub_create, the wrong tool could match.Consider scoring matches by keyword length or specificity, or ensure longer/more specific keywords are checked first.
♻️ Suggested approach: prefer longest keyword match
export function routeIntent(text) { const lower = text.toLowerCase(); + let bestMatch = null; + let bestKeywordLength = 0; for (const tool of BUILTIN_TOOLS) { for (const keyword of tool.keywords) { - if (lower.includes(keyword)) { - return { - target: tool.target, - tool: tool.name, - action: tool.name - }; + if (lower.includes(keyword) && keyword.length > bestKeywordLength) { + bestMatch = { + target: tool.target, + tool: tool.name, + action: tool.name + }; + bestKeywordLength = keyword.length; } } } + + if (bestMatch) return bestMatch; // Default: send to MIST chat - return { target: 'mist_chat' }; + return { target: 'mist_chat', tool: null, action: null }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/registry.js` around lines 65 - 75, The current first-match-wins loop over BUILTIN_TOOLS (the nested for-loops checking if lower.includes(keyword)) causes order-dependent routing; change it to collect all matching (tool, keyword) candidates and pick the one with the longest (most specific) keyword before returning. Specifically, in the matching logic around BUILTIN_TOOLS and the lower.includes(keyword) checks, gather matches into an array with fields {tool, keyword, target}, then select the entry with the longest keyword.length (tie-breaker: longest tool.name or preserve existing order) and return its target/tool/action instead of returning on the first match.docs/MIST_LIVING_SYSTEM.md (1)
44-67: Add a language specifier to the fenced code block.The file map block should have a language specifier for consistent formatting and linting compliance. Since this is a directory structure, use
textorplaintext.📝 Proposed fix
-``` +```text manifest.json ← MV3 shell (new-tab + sidebar + background)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/MIST_LIVING_SYSTEM.md` around lines 44 - 67, Add a language specifier to the fenced code block that holds the directory listing (the block beginning with the manifest.json line); change the opening fence from ``` to ```text so the directory structure is treated as plaintext for consistent formatting and linting (i.e., update the fenced block that contains manifest.json, background/, subspace/, sidebar/, content/, tools/, and vite.ext.config.ts).vite.ext.config.ts (1)
23-29: MissingchunkFileNamesandassetFileNamesmay scatter build artifacts unpredictably.Without explicit chunk and asset naming, shared code and assets (CSS, images) may end up in the root of
dist/with hashed names, making the extension structure harder to debug and potentially breaking relative paths in the manifest.♻️ Proposed fix to control output structure
output: { entryFileNames: (chunk) => { if (chunk.name === 'service_worker') return 'background/service_worker.js'; if (chunk.name.startsWith('content_')) return `content/${chunk.name.replace('content_', '')}.js`; return '[name]/[name].js'; - } + }, + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]' }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@vite.ext.config.ts` around lines 23 - 29, The rollup output currently only customizes output.entryFileNames which can leave shared chunks and assets dumped into the dist root; add explicit output.chunkFileNames and output.assetFileNames patterns alongside the existing entryFileNames to keep shared code and assets in predictable subfolders (e.g., use a chunkFileNames pattern that routes dynamic/shared chunks into a js/ or common/ folder and an assetFileNames pattern that routes CSS/images into css/ and assets/ folders), and ensure these new patterns work with the existing conditional logic for entryFileNames (service_worker and content_* names) so generated chunks and assets maintain the extension's folder structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@background/adaptation.js`:
- Around line 11-23: The logOutcome method currently doesn't persist the routing
key (intent/task type) that getRoutingRecommendation(inten t) later queries;
update logOutcome to extract and store the originating routing key (e.g., intent
or taskType) alongside the outcome and pattern when calling this.memory.remember
(and ensure the same key is included in the object passed to
this.memory.updatePattern if that API expects it), so that
getRoutingRecommendation can find entries by that routing key rather than
relying on the intent appearing inside the outcome payload; reference the
logOutcome function, this._extractPattern, this.memory.remember, and
this.memory.updatePattern when making the change.
In `@background/clawd_bridge.js`:
- Around line 32-39: The onmessage handler currently calls resolver(data.result
|| data.error) which incorrectly treats errors as successful results; change the
pendingRequests entries to store {resolve, reject} when creating the request and
update the ws.onmessage handler (the function assigned to this.ws.onmessage and
the lookup this.pendingRequests.get(data.id)) to call entry.resolve(data.result)
when a result exists and entry.reject(data.error) when data.error exists, then
delete the pendingRequests entry in both branches; ensure callers that create
the promise use the new shape for pendingRequests so they receive rejections on
errors.
- Around line 65-81: The timeout only deletes pendingRequests but leaves the
message in messageQueue, allowing stale requests to be sent later; modify
_send() to associate the request id with the queued entry (e.g., push an object
{id, message} instead of message) or ensure the timeout handler also removes the
queued message by filtering messageQueue for that id; also update the
flush/flushQueued/send-queue logic to check pendingRequests.has(id) before
sending so messages whose pendingRequests were deleted on timeout are skipped.
In `@background/growth.js`:
- Around line 81-86: registerTool currently overwrites existing entries by
calling this.registry.set(toolSpec.name, ...); change it to first check whether
this.registry.has(toolSpec.name) and refuse to register duplicates (either throw
an error or log/warn and return) so built-in tools like "summarize" or
"github_create" cannot be replaced; update the registerTool implementation (the
guard around toolSpec.name before calling this.registry.set) and ensure any
caller of registerTool handles the new duplicate-case behavior appropriately.
- Around line 89-93: _testTool currently calls this.bridge.executeTool without a
dry-run flag so calls propagate to mist.executeTool and perform real side
effects; change _testTool to pass a dry-run indicator (e.g., merge
toolSpec.test_params with {dryRun: true} or {dry_run: true}) when calling
this.bridge.executeTool, and ensure background/clawd_bridge.js and
mist.executeTool honor that flag to avoid performing writes (convert
side-effecting branches to no-ops when dryRun is set).
In `@background/memory.js`:
- Around line 82-96: The updatePattern function currently resolves as soon as
store.put() is queued and never rejects on failures; change it to wait for the
IndexedDB transaction to finish and to surface errors: after creating the
transaction and doing get/put, attach getReq.onerror (and capture putReq =
store.put(existing) and optionally putReq.onerror) to call reject with the
error, and remove the immediate resolve inside getReq.onsuccess; instead resolve
with the final existing object from tx.oncomplete, and call reject from
tx.onerror/tx.onabort so callers only get success when the write actually
committed; reference updatePattern, this.db, this.STORE_PATTERNS, getReq,
putReq, and tx.
In `@background/repair.js`:
- Around line 20-24: runCheck calls repairBridge('memory', ...) but repairBridge
directly invokes this.memory.recall/remember, which will throw if IndexedDB is
broken; change repairBridge to avoid directly calling this.memory.* methods
without guarding: instead check memory availability first (e.g., an
isAvailableMemory or try a safe feature-detect for IndexedDB), wrap any
this.memory.recall(...) and this.memory.remember(...) calls in try/catch, and
fall back to the non-IndexedDB repair path or report the error to the user;
update the code paths referenced by repairBridge, runCheck, and the anonymous
functions passed to checkBridge so that memory operations are only executed
inside guarded blocks (use temporary wrappers like
safeMemoryRecall/safeMemoryRemember or feature-detect before calling this.memory
methods).
In `@background/service_worker.js`:
- Around line 158-161: The code is persisting raw tool inputs and outputs
(params and result) via memory.remember, which may store sensitive prompts,
credentials, or PII; before calling memory.remember in the block that handles
tool.execute and adaptation.logOutcome, sanitize or redact sensitive fields from
params and result (or replace them with a minimal metadata summary or hash),
e.g., call a sanitizeToolIO(params) and sanitizeToolIO(result) helper and store
only the sanitized/summary objects and a timestamp; ensure references to
tool.execute, adaptation.logOutcome, memory.remember, and the variables
params/result are updated to use the sanitized versions so raw content is never
written to IndexedDB.
- Around line 116-118: The GET_MEMORY route is calling
MISTMemory.recall(message.query, message.context) but recall expects a numeric
limit as the second arg; change the route to pass a numeric limit (e.g.
message.limit) instead of message.context, validating/parsing it to an integer
with a sensible default if missing, and keep message.context (if needed) as a
separate parameter/place in the call site that accepts context (or pass context
via the correct argument name in MISTMemory methods); update the GET_MEMORY
handler so it invokes MISTMemory.recall(message.query, parsedLimit) rather than
forwarding message.context.
- Around line 83-85: The current listener uses handleMessage(message,
sender).then(sendResponse) which never calls sendResponse on rejection; update
the chrome.runtime.onMessage.addListener callback to ensure sendResponse is
always invoked by handling promise rejections from handleMessage — either append
.catch(err => sendResponse({ error: err?.message || String(err) })) or use an
async wrapper with try/catch and call sendResponse with an error object on
failure; reference the chrome.runtime.onMessage.addListener, handleMessage, and
sendResponse symbols when making the change.
In `@content/action_executor.js`:
- Around line 34-37: The default branch currently constructs an error result for
unknown actions but then calls sendResponse({ success: true, result }) — change
the logic so unknown/unsupported actions return success: false; either update
the default case to call sendResponse with success: false (or set a boolean like
`success = !result.error` and use that when calling sendResponse), referencing
the default branch in the switch and the sendResponse(...) invocation in
action_executor.js so downstream callers receive success: false for unsupported
actions.
In `@content/ghostline_overlay.js`:
- Around line 36-39: The current fingerprinting detection builds a
fingerprinting array based solely on the existence of browser APIs
(AudioContext, RTCPeerConnection) which yields false positives; update the logic
in the fingerprinting detection (the fingerprinting array and checks around
AudioContext and RTCPeerConnection) to detect actual usage patterns instead of
mere presence — e.g., instrument or monkey-patch
AudioContext.prototype.getChannelData and related methods to record real calls,
and listen for RTCPeerConnection events (onicecandidate, createDataChannel,
setLocalDescription) or override
RTCPeerConnection.prototype.createOffer/createAnswer to detect ICE candidate
gathering or SDP generation; alternatively remove the simple existence checks
and leave a TODO/nop until robust usage-based detection (getChannelData/ice
candidate events) is implemented.
In `@manifest.json`:
- Around line 21-39: The manifest currently injects content scripts
(content/page_reader.js, content/ghostline_overlay.js,
content/action_executor.js) and requests host_permissions "<all_urls>", which is
overly broad; update the manifest to remove or narrow the content_scripts
"matches" from "<all_urls>" to specific origins or patterns you actually support
(or remove the auto-injection entirely), drop the blanket host_permissions
"<all_urls>", and instead rely on "activeTab" and optional/host_permissions
requested at runtime or use chrome.scripting.executeScript after a user gesture
to run the privileged scripts (page_reader/action_executor) so that broad
read/modify access is not granted by default.
In `@sidebar/index.html`:
- Around line 76-100: The compose textarea (class .sidebar-input / the textarea
element currently using only placeholder) and the send control (class .send-btn
/ the button showing "→") need explicit accessible names and a visible keyboard
focus state: add descriptive aria-label attributes (e.g., aria-label="Message"
on the textarea and aria-label="Send message" on the send button) to ensure
screen readers announce them, and add CSS focus-visible rules
(.sidebar-input:focus-visible and .send-btn:focus-visible) that apply a clear
high-contrast outline or box-shadow so keyboard users can see focus (keep
existing :focus styles but add a distinct :focus-visible rule for
accessibility).
In `@sidebar/sidebar.js`:
- Around line 35-38: The OMNI input message omits the active tabId, so update
the send flow around chrome.runtime.sendMessage({ type: 'OMNI_INPUT', text }) to
include the current active tab id: query the active tab (e.g. via
chrome.tabs.query({ active: true, currentWindow: true })) and add tabId to the
payload ({ type: 'OMNI_INPUT', text, tabId }), falling back gracefully if no tab
is found; change the code that constructs the message in sidebar.js where text
is used so the background router receives both text and tabId.
- Around line 13-19: The listener currently applies PAGE_CONTEXT_UPDATE
unconditionally; modify chrome.runtime.onMessage.addListener to ignore updates
from non-active tabs by checking message.tabId against the id of the currently
visible tab before applying the summary. If your sidebar already stores the
active tab id (e.g., currentTabId or activeTabId), return early when
message.tabId !== currentTabId; otherwise obtain the active tab id via
chrome.tabs.query({active:true, lastFocusedWindow:true}) and compare to
message.tabId, only updating pageSummary.textContent when they match.
In `@subspace/subspace.js`:
- Around line 21-22: Dynamic import of SubspaceApp.js currently has no error
handling so failures are silent; update the import('./SubspaceApp.js').then(({
default: init }) => init(document.getElementById('subspace-app'))) flow to catch
rejection, log the error (e.g., console.error or use existing logger) and render
a simple fallback UI into the same mount node
(document.getElementById('subspace-app')) indicating the load failure;
specifically add a .catch handler that receives the error, logs it with context
like "Failed to load SubspaceApp" and sets innerText/HTML of the mount element
to a user-visible message so the tab doesn't remain blank.
In `@vite.ext.config.ts`:
- Around line 14-30: The service_worker entry in rollupOptions.input can be
split into chunks; update rollupOptions.output for the service worker to prevent
code splitting by setting inlineDynamicImports: true (or add a manualChunks
function to keep service_worker as a single chunk) and ensure the manifest.json
background entry declares "type": "module" so MV3 service workers load as ES
modules; modify the output config (alongside entryFileNames logic) to include
inlineDynamicImports or explicit manualChunks targeting the service_worker chunk
and verify manifest background.type is set to "module".
---
Minor comments:
In `@content/ghostline_overlay.js`:
- Around line 42-51: The handler for GHOSTLINE_ACTION currently only sets
window._mistTrackerBlocked without any blocking behavior; update the
GHOSTLINE_ACTION 'block_trackers' branch (the window.addEventListener('message'
callback and its block_trackers case) to either implement real blocking or mark
as placeholder: implement a MutationObserver that watches for added <script>
elements and inline script content matching common tracker domains/patterns and
removes or neutralizes them when window._mistTrackerBlocked is true (attach
observer when flag set and disconnect on session end), or if you choose the
placeholder route, replace the comment with an explicit TODO and log that this
is a stub so it’s not misleading. Ensure references include
window._mistTrackerBlocked, 'GHOSTLINE_ACTION', and the block_trackers action so
reviewers can find the change.
In `@sidebar/index.html`:
- Around line 125-126: The default greeting currently hardcodes "Hi Mars" inside
the chat message element (the div with class "message mist" inside the element
with id "chat-area"); replace that hardcoded name with a generic bootstrap
string like "Hi there" or "Hello — what do you need?" and update the runtime
injection logic to only insert a real user name into that element when a
validated name is available (do not render the injected name by default). Ensure
the change targets the div.message.mist (the bootstrap message) and the code
path that replaces its content at runtime.
In `@subspace/index.html`:
- Around line 7-8: Remove the duplicate, incorrect stylesheet link that points
to the source dir so the extension won't 404: delete the <link rel="stylesheet"
href="../src/subspace/subspace.css" /> entry in subspace/index.html and keep
only the build-correct <link rel="stylesheet" href="subspace.css" />; ensure
there are no other references to ../src/subspace/subspace.css in this file.
In `@subspace/subspace.js`:
- Around line 12-17: The BRIDGE_DOWN case makes the .bridge-alert visible but
there is no handler to clear it on recovery; add a BRIDGE_UP (or
BRIDGE_RECOVERED) branch in the same message handling logic (where message.type
is checked) that selects the same '.bridge-alert' element and removes the
'visible' class and clears or resets alert.textContent; ensure this logic lives
alongside the existing BRIDGE_DOWN handling so alerts are hidden when the bridge
reconnects.
In `@tools/registry.js`:
- Around line 62-79: The routeIntent function returns {target, tool, action} on
a match but only {target: 'mist_chat'} by default; change the default return in
routeIntent to include the same keys (e.g., return { target: 'mist_chat', tool:
null, action: null }) so consumers always receive a consistent shape; update any
references expecting BUILTIN_TOOLS, routeIntent, or the 'mist_chat' target to
handle null tool/action if needed.
---
Nitpick comments:
In `@content/ghostline_overlay.js`:
- Around line 19-34: When scanning scripts in the for loop, a single <script>
can produce two entries (one from the src match against KNOWN_TRACKER_DOMAINS
and one from the inline check for _ga/fbq/gtag); update the loop that iterates
over document.scripts (the code that pushes into trackers) to record whether a
src-based tracker was already added for that script (e.g., a boolean
foundSrcTracker set when pushing { type: 'script', tracker, src } in the
KNOWN_TRACKER_DOMAINS check) and only perform the inline textContent check and
push for { type: 'inline', tracker: 'analytics_pixel' } if foundSrcTracker is
false (or otherwise ensure uniqueness per script) so you never push duplicate
entries for the same script element.
In `@docs/MIST_LIVING_SYSTEM.md`:
- Around line 44-67: Add a language specifier to the fenced code block that
holds the directory listing (the block beginning with the manifest.json line);
change the opening fence from ``` to ```text so the directory structure is
treated as plaintext for consistent formatting and linting (i.e., update the
fenced block that contains manifest.json, background/, subspace/, sidebar/,
content/, tools/, and vite.ext.config.ts).
In `@subspace/subspace.css`:
- Line 27: The font-family declaration currently quotes "Inter", which Stylelint
flags; update the font-family line (the font-family: 'Inter', 'SF Pro Display',
system-ui, sans-serif; declaration) to remove quotes around Inter so it becomes
Inter (leave 'SF Pro Display' quoted because it contains spaces), ensuring the
line reads Inter, 'SF Pro Display', system-ui, sans-serif to satisfy Stylelint.
In `@tools/registry.js`:
- Around line 65-75: The current first-match-wins loop over BUILTIN_TOOLS (the
nested for-loops checking if lower.includes(keyword)) causes order-dependent
routing; change it to collect all matching (tool, keyword) candidates and pick
the one with the longest (most specific) keyword before returning. Specifically,
in the matching logic around BUILTIN_TOOLS and the lower.includes(keyword)
checks, gather matches into an array with fields {tool, keyword, target}, then
select the entry with the longest keyword.length (tie-breaker: longest tool.name
or preserve existing order) and return its target/tool/action instead of
returning on the first match.
In `@vite.ext.config.ts`:
- Around line 23-29: The rollup output currently only customizes
output.entryFileNames which can leave shared chunks and assets dumped into the
dist root; add explicit output.chunkFileNames and output.assetFileNames patterns
alongside the existing entryFileNames to keep shared code and assets in
predictable subfolders (e.g., use a chunkFileNames pattern that routes
dynamic/shared chunks into a js/ or common/ folder and an assetFileNames pattern
that routes CSS/images into css/ and assets/ folders), and ensure these new
patterns work with the existing conditional logic for entryFileNames
(service_worker and content_* names) so generated chunks and assets maintain the
extension's folder structure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e6fd16eb-a505-46ab-a8cf-9805bcdf4b0e
📒 Files selected for processing (19)
background/adaptation.jsbackground/clawd_bridge.jsbackground/growth.jsbackground/memory.jsbackground/repair.jsbackground/service_worker.jscontent/action_executor.jscontent/ghostline_overlay.jscontent/page_reader.jsdocs/MIST_LIVING_SYSTEM.mdmanifest.jsonpackage.jsonsidebar/index.htmlsidebar/sidebar.jssubspace/index.htmlsubspace/subspace.csssubspace/subspace.jstools/registry.jsvite.ext.config.ts
| async logOutcome(action, outcome) { | ||
| const pattern = this._extractPattern(action); | ||
| const success = outcome.success !== false; | ||
|
|
||
| await this.memory.remember({ | ||
| type: 'outcome', | ||
| action, | ||
| outcome, | ||
| pattern, | ||
| timestamp: Date.now() | ||
| }); | ||
|
|
||
| await this.memory.updatePattern(pattern, success); |
There was a problem hiding this comment.
Persist the routing key you query on.
getRoutingRecommendation(intent) searches memory by intent, but logOutcome() never stores the originating intent/task type. In practice this path will usually return null unless that text happens to appear inside the outcome payload.
Also applies to: 30-33
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/adaptation.js` around lines 11 - 23, The logOutcome method
currently doesn't persist the routing key (intent/task type) that
getRoutingRecommendation(inten t) later queries; update logOutcome to extract
and store the originating routing key (e.g., intent or taskType) alongside the
outcome and pattern when calling this.memory.remember (and ensure the same key
is included in the object passed to this.memory.updatePattern if that API
expects it), so that getRoutingRecommendation can find entries by that routing
key rather than relying on the intent appearing inside the outcome payload;
reference the logOutcome function, this._extractPattern, this.memory.remember,
and this.memory.updatePattern when making the change.
| this.ws.onmessage = (event) => { | ||
| try { | ||
| const data = JSON.parse(event.data); | ||
| const resolver = this.pendingRequests.get(data.id); | ||
| if (resolver) { | ||
| resolver(data.result || data.error); | ||
| this.pendingRequests.delete(data.id); | ||
| } |
There was a problem hiding this comment.
Reject bridge errors instead of resolving them.
When clawd returns an error, Lines 35-38 still resolve the request with that payload. Callers like background/service_worker.js then treat failed analyses or tool executions as successful results.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/clawd_bridge.js` around lines 32 - 39, The onmessage handler
currently calls resolver(data.result || data.error) which incorrectly treats
errors as successful results; change the pendingRequests entries to store
{resolve, reject} when creating the request and update the ws.onmessage handler
(the function assigned to this.ws.onmessage and the lookup
this.pendingRequests.get(data.id)) to call entry.resolve(data.result) when a
result exists and entry.reject(data.error) when data.error exists, then delete
the pendingRequests entry in both branches; ensure callers that create the
promise use the new shape for pendingRequests so they receive rejections on
errors.
| return new Promise((resolve, reject) => { | ||
| const timeout = setTimeout(() => { | ||
| this.pendingRequests.delete(id); | ||
| reject(new Error(`Request timeout: ${procedure}`)); | ||
| }, 15000); | ||
|
|
||
| this.pendingRequests.set(id, (result) => { | ||
| clearTimeout(timeout); | ||
| resolve(result); | ||
| }); | ||
|
|
||
| if (this.connected && this.ws?.readyState === WebSocket.OPEN) { | ||
| this.ws.send(JSON.stringify(message)); | ||
| } else { | ||
| // Queue message — repair engine will reconnect and flush | ||
| this.messageQueue.push(message); | ||
| } |
There was a problem hiding this comment.
Timed-out queued requests can still execute later.
If _send() queues a message while disconnected, the 15s timeout rejects and removes only pendingRequests. The message stays in messageQueue, so a later reconnect can flush stale mist.executeTool/mist.build calls after the caller already gave up.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/clawd_bridge.js` around lines 65 - 81, The timeout only deletes
pendingRequests but leaves the message in messageQueue, allowing stale requests
to be sent later; modify _send() to associate the request id with the queued
entry (e.g., push an object {id, message} instead of message) or ensure the
timeout handler also removes the queued message by filtering messageQueue for
that id; also update the flush/flushQueued/send-queue logic to check
pendingRequests.has(id) before sending so messages whose pendingRequests were
deleted on timeout are skipped.
| registerTool(toolSpec) { | ||
| this.registry.set(toolSpec.name, { | ||
| name: toolSpec.name, | ||
| description: toolSpec.description, | ||
| execute: async (params) => this.bridge.executeTool(toolSpec.name, params) | ||
| }); |
There was a problem hiding this comment.
Don't let generated tools overwrite existing registrations.
registerTool() blindly set()s whatever name comes back from generateTool(). A bad or adversarial spec can replace built-ins like summarize or github_create, changing future behavior globally for the session.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/growth.js` around lines 81 - 86, registerTool currently overwrites
existing entries by calling this.registry.set(toolSpec.name, ...); change it to
first check whether this.registry.has(toolSpec.name) and refuse to register
duplicates (either throw an error or log/warn and return) so built-in tools like
"summarize" or "github_create" cannot be replaced; update the registerTool
implementation (the guard around toolSpec.name before calling this.registry.set)
and ensure any caller of registerTool handles the new duplicate-case behavior
appropriately.
| async _testTool(toolSpec) { | ||
| try { | ||
| // Dry run with test params | ||
| await this.bridge.executeTool(toolSpec.name, toolSpec.test_params || {}); | ||
| return { success: true }; |
There was a problem hiding this comment.
_testTool() is not actually a dry-run.
background/clawd_bridge.js forwards this to mist.executeTool with no dry-run flag, so discovery can trigger real side effects during testing (posting, repo creation, Drive writes, etc.).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/growth.js` around lines 89 - 93, _testTool currently calls
this.bridge.executeTool without a dry-run flag so calls propagate to
mist.executeTool and perform real side effects; change _testTool to pass a
dry-run indicator (e.g., merge toolSpec.test_params with {dryRun: true} or
{dry_run: true}) when calling this.bridge.executeTool, and ensure
background/clawd_bridge.js and mist.executeTool honor that flag to avoid
performing writes (convert side-effecting branches to no-ops when dryRun is
set).
| .sidebar-input { | ||
| flex: 1; | ||
| background: rgba(255, 255, 255, 0.04); | ||
| border: 1px solid var(--border); | ||
| border-radius: 8px; | ||
| color: var(--text); | ||
| padding: 10px 12px; | ||
| font-size: 13px; | ||
| outline: none; | ||
| font-family: inherit; | ||
| resize: none; | ||
| } | ||
| .sidebar-input:focus { border-color: rgba(139, 92, 246, 0.4); } | ||
| .send-btn { | ||
| background: var(--mist-purple); | ||
| border: none; | ||
| border-radius: 8px; | ||
| color: white; | ||
| padding: 10px 14px; | ||
| cursor: pointer; | ||
| font-size: 13px; | ||
| font-weight: 500; | ||
| transition: opacity 0.15s; | ||
| } | ||
| .send-btn:hover { opacity: 0.85; } |
There was a problem hiding this comment.
Add explicit accessible names and a visible keyboard focus state to the compose controls.
On Lines 129-131, the textarea only relies on placeholder text and the send button is exposed as just →. On Lines 76-100, the primary action also lacks a dedicated :focus-visible treatment. That makes the main chat flow hard to use with screen readers and keyboard navigation.
Suggested fix
- .sidebar-input:focus { border-color: rgba(139, 92, 246, 0.4); }
+ .sidebar-input:focus,
+ .sidebar-input:focus-visible { border-color: rgba(139, 92, 246, 0.4); }
+ .sidebar-input:focus-visible,
+ .send-btn:focus-visible {
+ outline: 2px solid var(--mist-purple);
+ outline-offset: 2px;
+ }
...
- <textarea class="sidebar-input" id="sidebar-input" placeholder="Talk to MIST..." rows="1"></textarea>
- <button class="send-btn" id="send-btn">→</button>
+ <textarea class="sidebar-input" id="sidebar-input" aria-label="Message MIST" placeholder="Talk to MIST..." rows="1"></textarea>
+ <button class="send-btn" id="send-btn" type="button" aria-label="Send message">→</button>Also applies to: 129-131
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidebar/index.html` around lines 76 - 100, The compose textarea (class
.sidebar-input / the textarea element currently using only placeholder) and the
send control (class .send-btn / the button showing "→") need explicit accessible
names and a visible keyboard focus state: add descriptive aria-label attributes
(e.g., aria-label="Message" on the textarea and aria-label="Send message" on the
send button) to ensure screen readers announce them, and add CSS focus-visible
rules (.sidebar-input:focus-visible and .send-btn:focus-visible) that apply a
clear high-contrast outline or box-shadow so keyboard users can see focus (keep
existing :focus styles but add a distinct :focus-visible rule for
accessibility).
| chrome.runtime.onMessage.addListener((message) => { | ||
| if (message.type === 'PAGE_CONTEXT_UPDATE') { | ||
| const { analysis } = message; | ||
| if (analysis?.summary) { | ||
| pageSummary.textContent = analysis.summary; | ||
| } | ||
| } |
There was a problem hiding this comment.
Ignore page-context updates from non-active tabs.
background/service_worker.js already includes tabId in PAGE_CONTEXT_UPDATE, but this listener applies every update unconditionally. A background tab navigation can overwrite the summary for the tab the user is actually viewing.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidebar/sidebar.js` around lines 13 - 19, The listener currently applies
PAGE_CONTEXT_UPDATE unconditionally; modify chrome.runtime.onMessage.addListener
to ignore updates from non-active tabs by checking message.tabId against the id
of the currently visible tab before applying the summary. If your sidebar
already stores the active tab id (e.g., currentTabId or activeTabId), return
early when message.tabId !== currentTabId; otherwise obtain the active tab id
via chrome.tabs.query({active:true, lastFocusedWindow:true}) and compare to
message.tabId, only updating pageSummary.textContent when they match.
| const response = await chrome.runtime.sendMessage({ | ||
| type: 'OMNI_INPUT', | ||
| text | ||
| }); |
There was a problem hiding this comment.
Send the active tabId with OMNI input.
The background router destructures { text, tabId } and uses tabId for page-scoped routes like Ghostline actions. From the sidebar this field is always omitted, so any route that needs the current tab cannot execute correctly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidebar/sidebar.js` around lines 35 - 38, The OMNI input message omits the
active tabId, so update the send flow around chrome.runtime.sendMessage({ type:
'OMNI_INPUT', text }) to include the current active tab id: query the active tab
(e.g. via chrome.tabs.query({ active: true, currentWindow: true })) and add
tabId to the payload ({ type: 'OMNI_INPUT', text, tabId }), falling back
gracefully if no tab is found; change the code that constructs the message in
sidebar.js where text is used so the background router receives both text and
tabId.
| // Dynamic import of React app (built separately via webpack/vite) | ||
| import('./SubspaceApp.js').then(({ default: init }) => init(document.getElementById('subspace-app'))); |
There was a problem hiding this comment.
Missing error handling for dynamic import—app silently fails to load if SubspaceApp.js is missing or has errors.
If the dynamic import fails (file missing, syntax error, export missing), the promise rejects silently and the new tab page remains blank with no indication of what went wrong.
🐛 Proposed fix with error handling
-// Dynamic import of React app (built separately via webpack/vite)
-import('./SubspaceApp.js').then(({ default: init }) => init(document.getElementById('subspace-app')));
+// Dynamic import of React app (built separately via webpack/vite)
+import('./SubspaceApp.js')
+ .then(({ default: init }) => {
+ const root = document.getElementById('subspace-app');
+ if (root && typeof init === 'function') {
+ init(root);
+ } else {
+ console.error('[Subspace] Failed to initialize: missing root element or init function');
+ }
+ })
+ .catch((err) => {
+ console.error('[Subspace] Failed to load SubspaceApp:', err);
+ const root = document.getElementById('subspace-app');
+ if (root) {
+ root.innerHTML = '<p style="color: `#fca5a5`; padding: 2rem;">MIST failed to initialize. Check console for details.</p>';
+ }
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Dynamic import of React app (built separately via webpack/vite) | |
| import('./SubspaceApp.js').then(({ default: init }) => init(document.getElementById('subspace-app'))); | |
| // Dynamic import of React app (built separately via webpack/vite) | |
| import('./SubspaceApp.js') | |
| .then(({ default: init }) => { | |
| const root = document.getElementById('subspace-app'); | |
| if (root && typeof init === 'function') { | |
| init(root); | |
| } else { | |
| console.error('[Subspace] Failed to initialize: missing root element or init function'); | |
| } | |
| }) | |
| .catch((err) => { | |
| console.error('[Subspace] Failed to load SubspaceApp:', err); | |
| const root = document.getElementById('subspace-app'); | |
| if (root) { | |
| root.innerHTML = '<p style="color: `#fca5a5`; padding: 2rem;">MIST failed to initialize. Check console for details.</p>'; | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subspace/subspace.js` around lines 21 - 22, Dynamic import of SubspaceApp.js
currently has no error handling so failures are silent; update the
import('./SubspaceApp.js').then(({ default: init }) =>
init(document.getElementById('subspace-app'))) flow to catch rejection, log the
error (e.g., console.error or use existing logger) and render a simple fallback
UI into the same mount node (document.getElementById('subspace-app')) indicating
the load failure; specifically add a .catch handler that receives the error,
logs it with context like "Failed to load SubspaceApp" and sets innerText/HTML
of the mount element to a user-visible message so the tab doesn't remain blank.
| rollupOptions: { | ||
| input: { | ||
| subspace: resolve(__dirname, 'subspace/index.html'), | ||
| sidebar: resolve(__dirname, 'sidebar/index.html'), | ||
| service_worker: resolve(__dirname, 'background/service_worker.js'), | ||
| content_page_reader: resolve(__dirname, 'content/page_reader.js'), | ||
| content_ghostline: resolve(__dirname, 'content/ghostline_overlay.js'), | ||
| content_action_executor: resolve(__dirname, 'content/action_executor.js') | ||
| }, | ||
| output: { | ||
| entryFileNames: (chunk) => { | ||
| if (chunk.name === 'service_worker') return 'background/service_worker.js'; | ||
| if (chunk.name.startsWith('content_')) return `content/${chunk.name.replace('content_', '')}.js`; | ||
| return '[name]/[name].js'; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Vite build service worker MV3 Chrome extension code splitting
💡 Result:
To get Vite to build an MV3 extension service worker with predictable code-splitting, you need to treat the service worker as a real build entry, and then decide whether you want one-file (recommended) or multi-chunk output.
1) Make the MV3 service worker an ES module
In manifest.json:
{
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
}
}Chrome’s MV3 docs explicitly call out "type": "module" for using import. [1]
2) Ensure Vite actually builds the service worker (multi-entry)
Vite/Rollup only emits what’s reachable from an entry. Add the SW as an input (along with your HTML pages, if any):
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
input: {
// extension pages
popup: 'popup.html',
options: 'options.html',
// service worker entry
background: 'src/background.ts',
},
},
},
})Vite documents multi-entry builds via build.rollupOptions.input. [2]
If you want a purpose-built setup, @crxjs/vite-plugin is a widely used Vite plugin for MV3 extensions. [3]
3) Decide your splitting strategy (the important part)
Option A (recommended): single-file service worker
MV3 service workers are short-lived/terminated; minimizing extra chunk fetches is usually more robust.
In Rollup, you can force dynamic imports to be inlined into one output for that bundle:
export default defineConfig({
build: {
rollupOptions: {
input: { background: 'src/background.ts' },
output: {
inlineDynamicImports: true,
},
},
},
})Option B: multi-chunk service worker
If you do want chunks, use normal ESM imports for splitting (and optionally manualChunks to control it):
export default defineConfig({
build: {
rollupOptions: {
input: { background: 'src/background.ts' },
output: {
manualChunks: {
vendor: ['lodash-es'],
},
},
},
},
})Vite points to Rollup’s manualChunks for chunk splitting control. [2]
About import() in extension service workers: ESM module workers generally support static+dynamic imports in modern Chromium, but extension-SW support for import() has historically been inconsistent (there are Chromium discussions/bugs around it). If you rely on import(), test in your target Chrome versions and consider preferring static imports + manualChunks (or Option A). [4]
Sources
[1] Chrome for Developers — Extension service worker basics (module SW via "type": "module") (developer.chrome.com)
[2] Vite — Building for Production (rollupOptions.input, manualChunks) (v3.vitejs.dev)
[3] CRXJS — chrome-extension-tools / @crxjs/vite-plugin (MV3-focused tooling) (github.com)
[4] Chromium Extensions Google Group thread noting limitations around dynamic import in module extension SW (historical) (groups.google.com)
Citations:
- 1: https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/basics?utm_source=openai
- 2: https://v3.vitejs.dev/guide/build.html?utm_source=openai
- 3: https://github.com/crxjs/chrome-extension-tools?utm_source=openai
- 4: https://groups.google.com/a/chromium.org/g/chromium-extensions/c/Xq4kEGjPG5A/m/Xxb0IhhBAwAJ?utm_source=openai
Ensure the MV3 service worker is built as a single file to avoid code splitting issues.
The service_worker entry point may be split into multiple chunks if it shares dependencies with other entries. MV3 service workers require special configuration to handle module imports reliably.
Add inlineDynamicImports: true to the output config for the service worker, or use manualChunks to explicitly control splitting. Additionally, ensure your manifest.json declares "type": "module" in the background field to enable ES module support in the service worker.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@vite.ext.config.ts` around lines 14 - 30, The service_worker entry in
rollupOptions.input can be split into chunks; update rollupOptions.output for
the service worker to prevent code splitting by setting inlineDynamicImports:
true (or add a manualChunks function to keep service_worker as a single chunk)
and ensure the manifest.json background entry declares "type": "module" so MV3
service workers load as ES modules; modify the output config (alongside
entryFileNames logic) to include inlineDynamicImports or explicit manualChunks
targeting the service_worker chunk and verify manifest background.type is set to
"module".
MIST Living System
This PR transforms AetherBrowser from a Tauri scaffold into a living Chrome Extension — MIST as the operating intelligence, not a chat widget.
What's in this commit
Chrome Extension Shell (MV3)
manifest.json— new-tab override (Subspace), side-panel, background service workerdist/for unpacked Chrome loadingMIST Brain (
background/)service_worker.js— Perceive → Reason → Act loop, always-onmemory.js— IndexedDB (local) + Chroma cloud sync via clawd. MIST never forgets.adaptation.js— Behavioral reinforcement. Tracks what works, improves routing over time.growth.js— Capability acquisition. Discovers new APIs while browsing, auto-registers tools.repair.js— 30s watchdog. Self-heals broken bridges silently. Only surfaces to Mars when stuck.clawd_bridge.js— tRPC WebSocket to clawd (port 18789). Cloud (Gemini) + local (Ollama) dual-path.Subspace Agent OS (
subspace/)Sidebar (
sidebar/)Content Scripts (
content/)page_reader.js— extracts page text/meta, sends to MIST on every loadghostline_overlay.js— passive tracker detection, reports to ContextDockaction_executor.js— MIST can click, fill, extract, scroll on any pageTool Registry (
tools/registry.js)Omni-Input Agentic Routing
/→ command palette,@→ agent,!→ ghostline,~→ broadcastTo load locally
npm install npm run watch:ext # Chrome → chrome://extensions → Developer mode → Load unpacked → select dist/Requires: clawd running on port 18789
"Not software that runs. A system that lives."
Summary by CodeRabbit
Release Notes
New Features
Documentation
Chores