Skip to content

feat: MIST Living System — Chrome Extension Agentic Core - #1

Open
Mellowambience wants to merge 1 commit into
mainfrom
feature/mist-living-system
Open

feat: MIST Living System — Chrome Extension Agentic Core#1
Mellowambience wants to merge 1 commit into
mainfrom
feature/mist-living-system

Conversation

@Mellowambience

@Mellowambience Mellowambience commented Mar 11, 2026

Copy link
Copy Markdown
Owner

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 worker
  • Vite build config → outputs to dist/ for unpacked Chrome loading

MIST Brain (background/)

  • service_worker.js — Perceive → Reason → Act loop, always-on
  • memory.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/)

  • New tab page wired to live MIST backend
  • Receives proactive page analysis without user prompt

Sidebar (sidebar/)

  • Persistent MIST panel across all tabs
  • Page context surfaced in header automatically

Content Scripts (content/)

  • page_reader.js — extracts page text/meta, sends to MIST on every load
  • ghostline_overlay.js — passive tracker detection, reports to ContextDock
  • action_executor.js — MIST can click, fill, extract, scroll on any page

Tool Registry (tools/registry.js)

  • Dynamic, self-expanding capability manifest
  • Built-in: summarize, post_twitter, post_linkedin, save_to_drive, github_create, scrape, block_trackers, translate
  • Growth engine adds new tools autonomously

Omni-Input Agentic Routing

  • No prefix → MIST decides routing autonomously
  • / → command palette, @ → agent, ! → ghostline, ~ → broadcast

To 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

    • Introduced MIST Chrome Extension with four core capabilities: persistent memory via IndexedDB, behavioral adaptation through action outcome learning, autonomous tool discovery and registration, and self-healing repair via watchdog monitoring.
    • Added sidebar chat interface for user interactions with the extension.
    • Added content scripts for page reading, security monitoring (tracker detection), and page action execution.
    • Implemented tool registry with dynamic capability acquisition.
    • Added Subspace new tab page interface.
  • Documentation

    • Added MIST Living System documentation detailing architecture and development setup.
  • Chores

    • Updated project dependencies and build configuration for Chrome extension development.

- 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.
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

A 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

Cohort / File(s) Summary
Background Service Worker Core
background/adaptation.js, background/clawd_bridge.js, background/growth.js, background/memory.js, background/repair.js, background/service_worker.js
Introduces five core MIST engine classes: MISTAdaptation (pattern learning via memory), MISTBridge (WebSocket RPC to clawd service with queue/timeout handling), MISTGrowth (tool discovery/registration with dry-run testing), MISTMemory (IndexedDB three-tier persistence with semantic indexing), and MISTRepair (watchdog with self-healing retry logic). Service worker orchestrates initialization, message routing, page analysis, and component coordination.
Content Scripts
content/page_reader.js, content/ghostline_overlay.js, content/action_executor.js
Three content scripts injected on all pages: page_reader collects URL/title/text/metadata/scripts; ghostline_overlay detects trackers and fingerprinting; action_executor listens for MIST_PAGE_ACTION messages and executes click/fill/extract/scroll/get_text operations with structured responses.
Sidebar UI
sidebar/index.html, sidebar/sidebar.js
Implements side panel UI with glassmorphic styling, persistent messaging to background service worker, real-time page context updates, chat interface with thinking indicator, textarea auto-resize, and Enter-to-send input handling.
Subspace New Tab
subspace/index.html, subspace/subspace.css, subspace/subspace.js
New tab override with glassmorphism CSS theme (pulsing status dot, bridge alert badge), real-time message listener for PAGE_CONTEXT_UPDATE and BRIDGE_DOWN events, and React app initialization.
Tool Registry & Configuration
tools/registry.js, manifest.json, vite.ext.config.ts
Tool registry with BUILTIN_TOOLS list and routeIntent keyword matching; MV3 manifest defining service worker, content script injection, permissions, and new tab override; Vite config with multi-entry bundling and custom output paths for Chrome Extension.
Build & Dependencies
package.json
Updated from Tauri desktop app to Chrome extension: removed @tauri-apps/\* dependencies, added ws and @trpc/client, updated React to 18.3.0, added typecheck/build:ext/watch:ext/load:chrome scripts, updated DevDependencies to Chrome types and latest Vite.
Documentation
docs/MIST_LIVING_SYSTEM.md
Comprehensive system documentation covering MIST concepts (memory, adaptation, growth, repair), file structure, local development setup (clawd on port 18789), Omni-Input routing semantics, and command prefix mapping.

Sequence Diagrams

sequenceDiagram
    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}
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A rabbit weaves through digital threads,
Memory blooms where browsers tread,
Tools grow wild from pages unknown,
Self-healing bridges, trust full-grown,
MIST dances alive—the web's new heart! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing the MIST Living System as a Chrome Extension Agentic Core, which matches the substantial additions across background services, UI components, and configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/mist-living-system

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread vite.ext.config.ts
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`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread subspace/subspace.js
});

// Dynamic import of React app (built separately via webpack/vite)
import('./SubspaceApp.js').then(({ default: init }) => init(document.getElementById('subspace-app')));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Tracker blocking is not implemented—only a flag is set.

The GHOSTLINE_ACTION handler sets window._mistTrackerBlocked = true but 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 MutationObserver to 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 MutationObserver to 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 | 🟡 Minor

Duplicate 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 in dist/
  • subspace.css — correct relative path after Vite builds

The 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 | 🟡 Minor

Bridge alert is shown but never hidden on recovery.

When BRIDGE_DOWN is 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_UP handler 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 | 🟡 Minor

Inconsistent 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 expect tool or action to 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 | 🟡 Minor

Remove 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 tracker src and inline analytics code.

A script element matching a tracker domain (lines 23-27) could also contain _ga/fbq/gtag in its textContent, resulting in duplicate entries in the trackers array.

♻️ 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_TOOLS affects routing. For example, if a user types "save to github repo", and save_to_drive (with keyword "save this") appears before github_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 text or plaintext.

📝 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: Missing chunkFileNames and assetFileNames may 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0db84ca and 6eedac4.

📒 Files selected for processing (19)
  • background/adaptation.js
  • background/clawd_bridge.js
  • background/growth.js
  • background/memory.js
  • background/repair.js
  • background/service_worker.js
  • content/action_executor.js
  • content/ghostline_overlay.js
  • content/page_reader.js
  • docs/MIST_LIVING_SYSTEM.md
  • manifest.json
  • package.json
  • sidebar/index.html
  • sidebar/sidebar.js
  • subspace/index.html
  • subspace/subspace.css
  • subspace/subspace.js
  • tools/registry.js
  • vite.ext.config.ts

Comment thread background/adaptation.js
Comment on lines +11 to +23
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +32 to +39
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +65 to +81
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment thread background/growth.js
Comment on lines +81 to +86
registerTool(toolSpec) {
this.registry.set(toolSpec.name, {
name: toolSpec.name,
description: toolSpec.description,
execute: async (params) => this.bridge.executeTool(toolSpec.name, params)
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread background/growth.js
Comment on lines +89 to +93
async _testTool(toolSpec) {
try {
// Dry run with test params
await this.bridge.executeTool(toolSpec.name, toolSpec.test_params || {});
return { success: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

_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).

Comment thread sidebar/index.html
Comment on lines +76 to +100
.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; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment thread sidebar/sidebar.js
Comment on lines +13 to +19
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'PAGE_CONTEXT_UPDATE') {
const { analysis } = message;
if (analysis?.summary) {
pageSummary.textContent = analysis.summary;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread sidebar/sidebar.js
Comment on lines +35 to +38
const response = await chrome.runtime.sendMessage({
type: 'OMNI_INPUT',
text
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread subspace/subspace.js
Comment on lines +21 to +22
// Dynamic import of React app (built separately via webpack/vite)
import('./SubspaceApp.js').then(({ default: init }) => init(document.getElementById('subspace-app')));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Comment thread vite.ext.config.ts
Comment on lines +14 to +30
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';
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant