Skip to content

Add Terminal component with ANSI parser support#1

Open
Oluwasetemi wants to merge 29 commits intomainfrom
feature/terminal
Open

Add Terminal component with ANSI parser support#1
Oluwasetemi wants to merge 29 commits intomainfrom
feature/terminal

Conversation

@Oluwasetemi
Copy link
Copy Markdown
Owner

@Oluwasetemi Oluwasetemi commented Jan 21, 2026

Adds Terminal component to Playground with ANSI escape sequence parsing capabilities.

Key changes:

  • Implement Terminal component in Playground Context
  • Add ANSI parser for handling terminal output formatting
  • Add try-catch callback mechanism
  • Update PlaygroundPanel and tsconfig configuration

Co-Authored-By: Warp agent@warp.dev

Summary by CodeRabbit

  • New Features

    • Added a live Terminal panel to view process output and server logs.
    • Option to use an alternative Monaco editor backend.
    • File-hiding support to exclude files from the file tree.
    • Code formatter now supports HTML, CSS and Markdown.
  • Bug Fixes

    • Console.clear is forwarded to the UI.
    • More robust storage access handling for sandboxed environments.
  • Chores

    • Updated package metadata and TypeScript config.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
playground-demo Ready Ready Preview, Comment Jan 23, 2026 4:57am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds a pluggable editor abstraction (CodeMirror + lazy-loadable Monaco), a new xterm.js Terminal UI with streaming process output, console-forwarder injection into mounted HTML, hidden-files support for templates, and related exports/configuration and styling updates.

Changes

Cohort / File(s) Summary
Editor Abstraction & Implementations
packages/core/src/editor/EditorAdapter.ts, packages/core/src/editor/CodeMirrorEditor.ts, packages/core/src/editor/MonacoEditor.ts, packages/core/src/editor/editorFactory.ts, packages/core/src/editor/EditorController.ts, packages/core/src/monaco.ts
Add EditorAdapter interface and EditorOptions; introduce CodeMirrorEditor and MonacoEditor implementations; provide createEditor/createEditorAsync/isMonacoAvailable factory and make EditorController delegate to adapter; add separate monaco entry.
Core Build & Exports
packages/core/vite.config.ts, packages/core/src/index.ts, packages/core/package.json, packages/core/src/vite-env.d.ts, packages/core/tsconfig.json
Add monaco lib entry and mark monaco-editor external; export new editor APIs and types; add worker ambient declarations; add ts types for vitest/node/vite client; update package.json exports, deps, and peer metadata.
Monaco-related Packaging
packages/core/package.json, packages/core/vite.config.ts
New "./monaco" export, monaco devDependency, peerDependency, and rollup externalization to allow optional Monaco packaging.
Terminal UI & Styling
packages/react/src/components/Terminal.tsx, packages/react/src/styles/playground.css, apps/demo/src/playground.css
New Terminal React component using @xterm/xterm and FitAddon; responsive fit and incremental rendering; extensive Monaco/CSS styling and terminal panel CSS adjustments.
UI Integration & Context
packages/react/src/components/PlaygroundPanel.tsx, packages/react/src/context/PlaygroundContext.tsx, packages/react/src/index.ts, packages/react/src/components/PlaygroundHeader.test.tsx
Add Terminal tab to panel, expose Terminal and TerminalProps from react package, extend PlaygroundContextValue with terminalMessages/clearTerminal/hiddenFiles, and update tests to mock new context properties.
Hook & State Management
packages/react/src/usePlayground.ts
Subscribe to engine process:output to append terminalMessages; expose terminalMessages and clearTerminal in hook return; clear on template change.
Preview & Webcontainer Forwarding
packages/core/src/preview/PreviewServer.ts, packages/core/src/webcontainer/WebContainerManager.ts, packages/core/src/webcontainer/FileSystemManager.ts
Pipe dev server stdout into WritableStream emitting process:output with metadata; add console forwarder injection into HTML files during mount to forward console calls (including clear); inject script placement logic.
File Tree Hidden Files
packages/react/src/components/PlaygroundFileTree.tsx, packages/templates/src/types.ts, packages/templates/src/react.ts, apps/demo/src/react.ts
Add hiddenFiles optional property to Template type; filter file tree rendering by hiddenFiles; add React demo template and mark specific files hidden.
Resizable Panel Robustness
packages/react/src/components/ResizablePanel.tsx
Wrap localStorage get/set in try/catch and use Number.parseFloat/Number.isNaN to tolerate sandboxed environments.
Tests, Minor Docs & Formatting
packages/core/src/engine/EventEmitter.test.ts, packages/core/src/template/TemplateCache.test.ts, packages/core/src/terminal/TerminalController.ts, apps/demo/src/App.tsx
Test imports/describe tweaks, minor comment added, small JSX/formatting changes and import ordering.
React Package Dependencies
packages/react/package.json
Add @xterm/xterm and @xterm/addon-fit dependencies for terminal functionality.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Dev Server
    participant Preview as PreviewServer
    participant Engine as PlaygroundEngine
    participant Hook as usePlayground
    participant Context as PlaygroundContext
    participant Terminal as Terminal Component

    Dev->>Preview: stdout/stderr output
    Preview->>Preview: pipe to WritableStream
    Preview->>Engine: emit "process:output" (processId, command, type, data, timestamp)

    Engine->>Hook: notify process:output listener
    Hook->>Hook: append TerminalMessage (type, text, timestamp)
    Hook->>Context: update terminalMessages state

    Context->>Terminal: provide terminalMessages
    Terminal->>Terminal: append new messages to xterm
    Terminal->>Terminal: FitAddon auto-fit on update/resize
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through code to stitch a glowing wheel,
Terminal whispers streaming line by line—what a feel!
Hidden files tucked snug beneath the tree,
Editors now switchable, light and free,
A playground blooms; I nibble the merge, and squeal!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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 PR title 'Add Terminal component with ANSI parser support' accurately describes the primary changes in the changeset. The PR introduces a new Terminal component with ANSI handling capabilities across multiple files, new xterm dependencies, terminal styling, and related context/hook updates.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react/src/usePlayground.ts (1)

50-57: Handle console.clear by clearing state instead of appending.
PreviewServer now forwards clear, but this handler always appends. The console UI will never clear when console.clear() is called inside the iframe.

✅ Suggested fix
-      const unsubscribeConsole = engine.on('console:message', (message: { type: string, args: any[] }) => {
-        setConsoleMessages(prev => [...prev, {
-          type: message.type as 'log' | 'error' | 'warn' | 'info',
-          text: message.args.map(arg =>
-            typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg),
-          ).join(' '),
-          timestamp: Date.now(),
-        }])
-      })
+      const unsubscribeConsole = engine.on('console:message', (message: { type: string, args: any[] }) => {
+        if (message.type === 'clear') {
+          setConsoleMessages([])
+          return
+        }
+        setConsoleMessages(prev => [...prev, {
+          type: message.type as 'log' | 'error' | 'warn' | 'info',
+          text: message.args.map(arg =>
+            typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg),
+          ).join(' '),
+          timestamp: Date.now(),
+        }])
+      })
🤖 Fix all issues with AI agents
In `@packages/core/src/webcontainer/FileSystemManager.ts`:
- Around line 17-23: The originalConsole object in FileSystemManager.ts
mistakenly binds clear to console.info (copy/paste bug); update the
originalConsole definition so clear is correctly bound to console.clear (e.g.,
set clear: console.clear.bind(console)) so calls to originalConsole.clear()
perform a real console clear instead of logging info; locate the originalConsole
constant in FileSystemManager.ts to make this one-line change.
🧹 Nitpick comments (3)
packages/core/src/terminal/TerminalController.ts (3)

71-71: Address the TODO comment for ANSI handling.

This comment indicates incomplete work. The PR objectives mention "ANSI parser for handling terminal output formatting" and there's an ansiParser.ts utility in the React package, but this core TerminalController doesn't use it. The current render() method escapes HTML but strips ANSI sequences entirely.

Consider either:

  1. Integrating ANSI parsing here for the core controller
  2. Removing this TODO if ANSI handling is intentionally delegated to the React layer only

Would you like me to help integrate the ANSI parser into this controller, or open an issue to track this task?


46-64: Duplicate logic for maxLines handling.

Both addOutput and addConsoleMessage contain identical logic for pushing to the array and enforcing maxLines. Consider extracting this into a shared helper.

♻️ Suggested refactor
+ private appendLine(line: ProcessOutput | ConsoleMessage): void {
+   this.outputLines.push(line)
+   if (this.outputLines.length > this.maxLines) {
+     this.outputLines.shift()
+   }
+   this.render()
+ }
+
  private addOutput(output: ProcessOutput): void {
-   this.outputLines.push(output)
-
-   if (this.outputLines.length > this.maxLines) {
-     this.outputLines.shift()
-   }
-
-   this.render()
+   this.appendLine(output)
  }

  private addConsoleMessage(message: ConsoleMessage): void {
-   this.outputLines.push(message)
-
-   if (this.outputLines.length > this.maxLines) {
-     this.outputLines.shift()
-   }
-
-   this.render()
+   this.appendLine(message)
  }

107-111: DOM-based HTML escaping creates elements on every call.

Creating a DOM element for each escape operation works correctly but may be inefficient when rendering many lines. For a terminal with up to 1000 lines, this could create performance overhead.

♻️ Alternative using string replacement
  private escapeHtml(text: string): string {
-   const div = document.createElement('div')
-   div.textContent = text
-   return div.innerHTML
+   return text
+     .replace(/&/g, '&')
+     .replace(/</g, '&lt;')
+     .replace(/>/g, '&gt;')
+     .replace(/"/g, '&quot;')
+     .replace(/'/g, '&#039;')
  }

Comment thread packages/core/src/webcontainer/FileSystemManager.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/templates/src/react.ts (1)

1-1: Fix incorrect import path in apps/demo/src/react.ts.

The import statement import type { Template } from '../../packages/react/src/types' is incorrect—packages/react/src/types.ts does not exist. The Template type is defined in packages/templates/src/types.ts. Change the import to:

import type { Template } from '../../packages/templates/src/types'

This will resolve the CI pipeline error Cannot find module '../../packages/react/src/types'.

packages/react/src/usePlayground.ts (1)

50-58: Handle console.clear instead of appending.
Now that console messages include 'clear', Line 50 should reset state when that event arrives, otherwise clear won’t work.

🛠️ Proposed fix
-      const unsubscribeConsole = engine.on('console:message', (message: { type: string, args: any[] }) => {
-        setConsoleMessages(prev => [...prev, {
-          type: message.type as 'log' | 'error' | 'warn' | 'info',
-          text: message.args.map(arg =>
-            typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg),
-          ).join(' '),
-          timestamp: Date.now(),
-        }])
-      })
+      const unsubscribeConsole = engine.on('console:message', (message: { type: string, args: any[] }) => {
+        if (message.type === 'clear') {
+          setConsoleMessages([])
+          return
+        }
+        setConsoleMessages(prev => [...prev, {
+          type: message.type as 'log' | 'error' | 'warn' | 'info',
+          text: message.args.map(arg =>
+            typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg),
+          ).join(' '),
+          timestamp: Date.now(),
+        }])
+      })
🤖 Fix all issues with AI agents
In `@apps/demo/src/react.ts`:
- Line 1: The import for the Template type is pointing to a non-existent module;
update the import in apps/demo/src/react.ts to import the Template type from the
correct package (change the import source used for the Template symbol to
'../../packages/templates') so the line importing Template references the
existing module instead of '../../packages/react/src/types'.

In `@packages/core/src/webcontainer/FileSystemManager.ts`:
- Around line 45-67: The injected console/error sender currently uses
window.parent.postMessage(..., '*') which allows any parent to receive or spoof
messages; change the injected script to accept an explicit expectedOrigin
variable and use window.parent.postMessage(..., expectedOrigin) wherever
postMessage is called (references: the console wrapper that calls
window.parent.postMessage, and the window.addEventListener('error') and
'unhandledrejection' handlers). Then update the receiver in PreviewServer.ts
(the event listener that handles messages based on event.data.source) to
validate event.origin strictly equals the same expected origin before trusting
or processing event.data. Ensure the expected origin value is passed securely
from the host when injecting the script and used in both sender and receiver
checks.

In `@packages/react/package.json`:
- Around line 47-49: Update the dependency for "@xterm/xterm" in package.json
from "^6.0.0" to "^5.5.0" (leave "@xterm/addon-fit" as-is); locate the
dependency entry for "@xterm/xterm" in packages/react/package.json and change
its version specifier to "^5.5.0" so the project uses the existing published
5.5.0 release.

In `@packages/react/src/components/Terminal.tsx`:
- Around line 134-146: Detect when messages length shrinks (messages.length <
lastMessageIndexRef.current) inside the useEffect in Terminal.tsx and call
terminalRef.current.clear() and reset lastMessageIndexRef.current (e.g., to 0)
before writing new messages; keep using terminalRef.current.write for
newMessages as before so clearing happens whenever clearTerminal or any reset
reduces the messages array.
🧹 Nitpick comments (6)
packages/core/src/webcontainer/FileSystemManager.ts (1)

123-137: Avoid double‑injecting the console forwarder.

If mount is called with already‑injected files (e.g., template switch or re‑mount from saved files), this will keep growing HTML contents. Add a sentinel check to skip reinjection.

♻️ Suggested guard
-        // Inject the script right after <head> or at the start of the file
-        let modifiedContent: string
-        if (content.includes('<head>')) {
+        // Avoid re-injecting if the forwarder is already present
+        let modifiedContent: string
+        if (content.includes('__playgroundConsoleInjected')) {
+          modifiedContent = content
+        }
+        else if (content.includes('<head>')) {
           modifiedContent = content.replace('<head>', `<head>${CONSOLE_FORWARDER_SCRIPT}`)
         }
packages/react/src/components/PlaygroundFileTree.tsx (2)

26-27: Add newline after if statement per linting rules.

The static analysis flagged a formatting issue.

🔧 Suggested fix
 function filterHiddenFiles(nodes: FileNode[], hiddenFiles: string[]): FileNode[] {
-  if (!hiddenFiles.length) return nodes
+  if (!hiddenFiles.length) {
+    return nodes
+  }

   return nodes

44-67: Consider removing unused hiddenFiles prop from FileTreeNode.

The hiddenFiles prop is passed to FileTreeNode but is never used within the component. The filtering is already performed by filterHiddenFiles() before rendering, so child nodes receive pre-filtered children. The prop can be removed to simplify the component.

♻️ Proposed refactor
 interface FileTreeNodeProps {
   node: FileNode
   onFileClick: (path: string) => void
-  hiddenFiles: string[]
 }

-function FileTreeNode({ node, onFileClick, hiddenFiles }: FileTreeNodeProps) {
+function FileTreeNode({ node, onFileClick }: FileTreeNodeProps) {
   if (node.type === 'file') {
     // ... unchanged
   }

   return (
     <details className="directory-node" open>
       {/* ... */}
       <div className="directory-children">
         {node.children?.map((child: FileNode) => (
           <FileTreeNode
             key={child.path}
             node={child}
             onFileClick={onFileClick}
-            hiddenFiles={hiddenFiles}
           />
         ))}
       </div>
     </details>
   )
 }

And update the usage in PlaygroundFileTree:

         {visibleFiles.map(node => (
           <FileTreeNode
             key={node.path}
             node={node}
             onFileClick={openFile}
-            hiddenFiles={hiddenFiles}
           />
         ))}
apps/demo/src/App.tsx (2)

1-27: Remove commented-out code or restore template selection.

There's significant commented-out code that should either be removed or the template selection feature should be restored. The current state with a disabled dropdown and dead code is confusing for maintenance.

Additionally, the alias newTemp is not descriptive. Consider using reactTemplate directly or a clearer name if aliasing is necessary.

♻️ Suggested cleanup

If template selection is deferred, simplify by removing dead code:

-// import type { Template } from '@setemiojo/playground-templates'
 import {
   Playground,
   // ...
 } from '@setemiojo/playground-react'
 import { useState } from 'react'
-import { reactTemplate as newTemp } from './react.ts'
-// import {
-//   nodeTemplate,
-//   reactTemplate,
-//   vanillaTemplate,
-//   vueTemplate,
-// } from '@setemiojo/playground-templates'
+import { reactTemplate } from './react.ts'
 import './playground.css'
 import './App.css'

-// console.log(newTemp)
-// const templates: Record<string, Template> = {
-//   vanilla: vanillaTemplate,
-//   react: reactTemplate,
-//   vue: vueTemplate,
-//   node: nodeTemplate,
-// }

29-32: Remove unused state and logic if template selection is disabled.

selectedTemplate state is used only for the disabled dropdown and getTitle(). If template selection is intentionally disabled, consider removing the unused state and simplifying getTitle() to return a constant.

packages/react/src/index.ts (1)

9-16: Fix export ordering per linting rules.

Static analysis indicates exports should be alphabetically sorted. The linter flagged multiple ordering issues.

🔧 Suggested reordering
 export { PlaygroundEditor } from './components/PlaygroundEditor'
 export { PlaygroundFileTree } from './components/PlaygroundFileTree'
 export { PlaygroundHeader } from './components/PlaygroundHeader'
 export type { PlaygroundHeaderProps } from './components/PlaygroundHeader'
 export { PlaygroundPanel } from './components/PlaygroundPanel'
 export type { PlaygroundPanelProps, PanelTab } from './components/PlaygroundPanel'
 export { PlaygroundPreview } from './components/PlaygroundPreview'
 export { PlaygroundTerminal } from './components/PlaygroundTerminal'
-export { Terminal } from './components/Terminal'
-export type { TerminalProps } from './components/Terminal'
 export { PlaygroundToolbar } from './components/PlaygroundToolbar'
 export { ResizablePanel } from './components/ResizablePanel'
 export type { ResizablePanelProps } from './components/ResizablePanel'
-export { useMediaQuery, useIsMobile } from './hooks/useMediaQuery'
-export { usePlaygroundContext } from './context/PlaygroundContext'
-export type { PlaygroundContextValue, ConsoleMessage, TerminalMessage } from './context/PlaygroundContext'
+export { Terminal } from './components/Terminal'
+export type { TerminalProps } from './components/Terminal'
+export { usePlaygroundContext } from './context/PlaygroundContext'
+export type { ConsoleMessage, PlaygroundContextValue, TerminalMessage } from './context/PlaygroundContext'
+export { useIsMobile, useMediaQuery } from './hooks/useMediaQuery'
 export { Playground } from './Playground'

Comment thread apps/demo/src/react.ts Outdated
@@ -0,0 +1,161 @@
import type { Template } from '../../packages/react/src/types'
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

🏁 Script executed:

# First, check if the demo file exists and examine the import
find . -name "react.ts" -type f | grep -E "(demo|apps)" | head -20

Repository: Oluwasetemi/playground

Length of output: 90


🏁 Script executed:

# Look for packages/react directory and check what's exported
find . -path "*/packages/react/*" -name "*.ts" -o -name "*.tsx" | grep -E "(index|types)" | head -20

Repository: Oluwasetemi/playground

Length of output: 95


🏁 Script executed:

# Check if Template type exists and where it's exported from
rg "export.*Template" --type ts --type tsx | head -30

Repository: Oluwasetemi/playground

Length of output: 93


🏁 Script executed:

# Check tsconfig configuration for path aliases
find . -name "tsconfig*.json" -type f | head -10

Repository: Oluwasetemi/playground

Length of output: 376


🏁 Script executed:

# Check the directory structure of packages/react/src
ls -la packages/react/src/

Repository: Oluwasetemi/playground

Length of output: 593


🏁 Script executed:

# Try searching for Template export with correct rg syntax
rg "export.*Template" --type ts | head -30

Repository: Oluwasetemi/playground

Length of output: 1524


🏁 Script executed:

# Check the root tsconfig.json for path aliases
cat tsconfig.json

Repository: Oluwasetemi/playground

Length of output: 706


🏁 Script executed:

# Check packages/react tsconfig
cat packages/react/tsconfig.json

Repository: Oluwasetemi/playground

Length of output: 355


🏁 Script executed:

# Verify the current state of apps/demo/src/react.ts
cat -n apps/demo/src/react.ts | head -20

Repository: Oluwasetemi/playground

Length of output: 738


Fix broken Template import (CI is failing).
../../packages/react/src/types does not exist. Import from ../../packages/templates instead:

import type { Template } from '../../packages/templates'
🧰 Tools
🪛 GitHub Actions: CI

[error] 1-1: Cannot find module '../../packages/react/src/types' or its corresponding type declarations.

🤖 Prompt for AI Agents
In `@apps/demo/src/react.ts` at line 1, The import for the Template type is
pointing to a non-existent module; update the import in apps/demo/src/react.ts
to import the Template type from the correct package (change the import source
used for the Template symbol to '../../packages/templates') so the line
importing Template references the existing module instead of
'../../packages/react/src/types'.

Comment on lines +45 to +67
window.parent.postMessage({
source: 'playground-console',
type: method,
args: args.map(serialize)
}, '*');
} catch (e) {}
};
});

window.addEventListener('error', function(e) {
window.parent.postMessage({
source: 'playground-console',
type: 'error',
args: [e.message + ' at ' + (e.filename || 'unknown') + ':' + (e.lineno || 0)]
}, '*');
});

window.addEventListener('unhandledrejection', function(e) {
window.parent.postMessage({
source: 'playground-console',
type: 'error',
args: ['Unhandled promise rejection: ' + (e.reason ? (e.reason.message || e.reason) : 'unknown')]
}, '*');
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

🏁 Script executed:

rg -nP -C3 '\baddEventListener\(\s*["\']message["\']' --type ts --type js

Repository: Oluwasetemi/playground

Length of output: 134


🏁 Script executed:

rg -nP -C3 '\bonmessage\b' --type ts --type js

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Also check for message event handling in window context
rg -nP 'window\.on' --type ts --type js | head -20

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

rg "addEventListener.*message" --type ts --type js -A 5 -B 2

Repository: Oluwasetemi/playground

Length of output: 766


🏁 Script executed:

rg "onmessage" --type ts --type js -A 5 -B 2

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Check the file structure to understand the project layout
git ls-files | grep -E "\.(ts|js)$" | head -20

Repository: Oluwasetemi/playground

Length of output: 821


🏁 Script executed:

# Look for the parent window message listener specifically
rg "source.*playground-console" --type ts --type js -B 5 -A 5

Repository: Oluwasetemi/playground

Length of output: 5667


🏁 Script executed:

cat -n packages/core/src/preview/PreviewServer.ts | sed -n '1,150p'

Repository: Oluwasetemi/playground

Length of output: 5590


🏁 Script executed:

# Get the full setupMessageListener implementation
rg -A 50 "setupMessageListener" packages/core/src/preview/PreviewServer.ts

Repository: Oluwasetemi/playground

Length of output: 3171


🏁 Script executed:

# Check if there's any origin validation anywhere in the PreviewServer
rg "event\.origin" packages/core/src/preview/PreviewServer.ts

Repository: Oluwasetemi/playground

Length of output: 48


Lock down the postMessage target origin.

Using "*" can leak console output to any embedding parent. The receiver in PreviewServer.ts (lines 87-99) only validates event.data.source but does not check event.origin, allowing any malicious parent to inject fake console messages. Pass an explicit, expected origin into the injected script and update both sender and receiver to validate the origin.

🔐 Suggested hardening
 const CONSOLE_FORWARDER_SCRIPT = `
 <script>
 (function() {
   if (window.__playgroundConsoleInjected) return;
   window.__playgroundConsoleInjected = true;

+  const targetOrigin = (() => {
+    try {
+      return document.referrer ? new URL(document.referrer).origin : window.location.origin;
+    } catch (e) {
+      return window.location.origin;
+    }
+  })();
 ...
-        }, '*');
+        }, targetOrigin);
 ...
-    }, '*');
+    }, targetOrigin);
 ...
-    }, '*');
+    }, targetOrigin);
 })();
 </script>
 `

And update the receiver in PreviewServer.ts to validate origin:

 window.addEventListener('message', (event) => {
+  if (!event.origin || !event.origin.includes(window.location.origin)) return;
   if (event.data && event.data.source === 'playground-console') {
🤖 Prompt for AI Agents
In `@packages/core/src/webcontainer/FileSystemManager.ts` around lines 45 - 67,
The injected console/error sender currently uses window.parent.postMessage(...,
'*') which allows any parent to receive or spoof messages; change the injected
script to accept an explicit expectedOrigin variable and use
window.parent.postMessage(..., expectedOrigin) wherever postMessage is called
(references: the console wrapper that calls window.parent.postMessage, and the
window.addEventListener('error') and 'unhandledrejection' handlers). Then update
the receiver in PreviewServer.ts (the event listener that handles messages based
on event.data.source) to validate event.origin strictly equals the same expected
origin before trusting or processing event.data. Ensure the expected origin
value is passed securely from the host when injecting the script and used in
both sender and receiver checks.

Comment on lines +47 to +49
"@setemiojo/playground-core": "workspace:*",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0"
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

🧩 Analysis chain

🌐 Web query:

@xterm/xterm latest version npm package

💡 Result:

As of January 21, 2026 the latest published stable version of @xterm/xterm is 5.5.0. [1][2]

Sources:

  • npm org/package list (shows @xterm packages and recent publishes). [1]
  • xterm.js GitHub releases (5.x release history / migration to @xterm scope). [2]

🌐 Web query:

@xterm/addon-fit latest version npm package 2024 2025

💡 Result:

As of now (Jan 21, 2026):

  • Latest stable @xterm/addon-fit: 0.11.0. [1][2]
  • There are newer beta releases in the 0.12.0-beta.* line published in late‑2025 / early‑2026. [2][3]

Sources: Snyk package info (shows 0.11.0 as latest) [1], Yarn/npm registry listing with beta tags and recent beta publishes [2][3].


@xterm/xterm version does not exist—downgrade to 5.5.0.

The @xterm/xterm package should be ^5.5.0, not ^6.0.0. As of January 2026, the latest stable version of @xterm/xterm is 5.5.0; version 6.0.0 has not been released. The @xterm/addon-fit ^0.11.0 specification is correct.

🤖 Prompt for AI Agents
In `@packages/react/package.json` around lines 47 - 49, Update the dependency for
"@xterm/xterm" in package.json from "^6.0.0" to "^5.5.0" (leave
"@xterm/addon-fit" as-is); locate the dependency entry for "@xterm/xterm" in
packages/react/package.json and change its version specifier to "^5.5.0" so the
project uses the existing published 5.5.0 release.

Comment on lines +134 to +146
// Write new messages to terminal
useEffect(() => {
if (!terminalRef.current) return

// Only write new messages
const newMessages = messages.slice(lastMessageIndexRef.current)

for (const msg of newMessages) {
terminalRef.current.write(msg.text)
}

lastMessageIndexRef.current = messages.length
}, [messages])
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

🏁 Script executed:

cd packages/react/src/components && ls -la Terminal.tsx 2>/dev/null || echo "File not found in expected location"

Repository: Oluwasetemi/playground

Length of output: 127


🏁 Script executed:

find . -name "Terminal.tsx" -type f 2>/dev/null | head -5

Repository: Oluwasetemi/playground

Length of output: 110


🏁 Script executed:

fd "Terminal.tsx" --type f

Repository: Oluwasetemi/playground

Length of output: 161


🏁 Script executed:

cat -n packages/react/src/components/Terminal.tsx

Repository: Oluwasetemi/playground

Length of output: 5092


🏁 Script executed:

cd packages/react && cat package.json | grep -A 5 '"@xterm'

Repository: Oluwasetemi/playground

Length of output: 255


🏁 Script executed:

web_search
xterm.js Terminal class clear() reset() API methods

Repository: Oluwasetemi/playground

Length of output: 253


🌐 Web query:

xterm.js Terminal API clear reset methods documentation

💡 Result:

Short summary:

  • Terminal.clear(): clears the entire buffer so the current prompt line becomes the new first line. Call: term.clear(). [1]

  • Terminal.reset(): performs a full terminal reset (RIS, equivalent to control sequence '\x1bc'). Call: term.reset(). Use when you need to fully reinitialize terminal state. [1][2]

References:
[1] xterm.js API — Terminal methods (clear, reset).
[2] xterm.js VT features — RIS / soft terminal reset explanation.


Terminal buffer not cleared when messages are reset.

When the messages array is cleared (e.g., clearTerminal), the terminal displays stale content because the buffer is never reset. The code only writes new messages from the last index onwards, so an empty array produces no output—but the xterm instance retains all previous writes. Detect when the array shrinks and call terminal.clear() before continuing.

Proposed fix
  useEffect(() => {
    if (!terminalRef.current) return

+   const terminal = terminalRef.current
+
+   // If messages were cleared/reset, clear terminal buffer too
+   if (messages.length < lastMessageIndexRef.current) {
+     terminal.clear()
+     lastMessageIndexRef.current = 0
+   }
+
    // Only write new messages
    const newMessages = messages.slice(lastMessageIndexRef.current)

    for (const msg of newMessages) {
-     terminalRef.current.write(msg.text)
+     terminal.write(msg.text)
    }

    lastMessageIndexRef.current = messages.length
  }, [messages])
🧰 Tools
🪛 ESLint

[error] 136-136: Expect newline after if

(setemiojo/if-newline)

🤖 Prompt for AI Agents
In `@packages/react/src/components/Terminal.tsx` around lines 134 - 146, Detect
when messages length shrinks (messages.length < lastMessageIndexRef.current)
inside the useEffect in Terminal.tsx and call terminalRef.current.clear() and
reset lastMessageIndexRef.current (e.g., to 0) before writing new messages; keep
using terminalRef.current.write for newMessages as before so clearing happens
whenever clearTerminal or any reset reduces the messages array.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react/src/usePlayground.ts (1)

160-165: Missing terminalMessages reset in resetCode.

The resetCode function clears consoleMessages but not terminalMessages. For consistency with the template switch behavior (lines 109-111), consider clearing terminal messages here as well.

Proposed fix
 const resetCode = useCallback(async () => {
   if (engineRef.current && templateRef.current) {
     await engineRef.current.resetToTemplate(templateRef.current)
     setConsoleMessages([])
+    setTerminalMessages([])
   }
 }, [])
🤖 Fix all issues with AI agents
In `@apps/demo/src/playground.css`:
- Around line 329-331: The CSS rule for `.playground-console-message.info` with
`color: `#75beff`;` at lines 329-331 is a duplicate of the same rule that already
exists at lines 286-289. Remove the duplicate rule block entirely from the
playground.css file to eliminate the redundancy.

In `@apps/demo/src/react.ts`:
- Line 1: The import path for the Template type is incorrect and causes module
resolution to fail; update the import statement that currently references
Template (import type { Template } from '../../packages/react/src/types') to
import Template from the package that actually exports it (the
`@setemiojo/playground-templates` package) so the code imports Template from
'@setemiojo/playground-templates' instead of the local packages path.

In `@packages/core/src/webcontainer/FileSystemManager.ts`:
- Around line 10-68: The CONSOLE_FORWARDER_SCRIPT currently calls
window.parent.postMessage(..., '*'); change these postMessage calls to use an
explicit targetOrigin (e.g. window.location.origin or a provided allowedOrigin
variable) instead of '*' so messages are only delivered to the expected parent
origin; update all occurrences inside CONSOLE_FORWARDER_SCRIPT (the
console[method] wrapper, window.addEventListener('error') and
'unhandledrejection' handlers) and apply the same change to the analogous
forwarder usages in PreviewServer.ts to ensure event.origin can be validated by
the receiver.
- Around line 90-94: The current flow injects the console-forwarding script into
files before calling this.webcontainer.mount (via injectConsoleForwarder), which
mutates stored HTML and leaks into the editor and snapshots; instead, remove the
pre-mount mutation and move injection to runtime preview/serve path (mirror
PreviewServer.injectConsoleForwarder) so files passed to this.webcontainer.mount
remain unmodified and the script is injected only into served/iframe responses;
update FileSystemManager to stop calling injectConsoleForwarder before mount and
ensure preview-serving code calls the non-mutating injection routine.
🧹 Nitpick comments (4)
packages/core/src/webcontainer/FileSystemManager.ts (1)

103-150: Make HTML detection case‑insensitive and less brittle.

Current checks are case‑sensitive (.html, <head>, <html>). Consider a small refactor using case‑insensitive regexes so .HTML and uppercase tags are handled reliably.

♻️ Suggested refactor
-      else if ('file' in node && name.endsWith('.html') && 'contents' in node.file) {
+      else if ('file' in node && name.toLowerCase().endsWith('.html') && 'contents' in node.file) {
@@
-        if (content.includes('<head>')) {
-          modifiedContent = content.replace('<head>', `<head>${CONSOLE_FORWARDER_SCRIPT}`)
-        }
-        else if (content.includes('<head ')) {
-          modifiedContent = content.replace(/<head\s[^>]*>/, `\$&${CONSOLE_FORWARDER_SCRIPT}`)
-        }
-        else if (content.includes('<html>') || content.includes('<html ')) {
-          modifiedContent = content.replace(/<html[^>]*>/, `\$&${CONSOLE_FORWARDER_SCRIPT}`)
+        const headTag = /<head\b[^>]*>/i
+        const htmlTag = /<html\b[^>]*>/i
+        if (headTag.test(content)) {
+          modifiedContent = content.replace(headTag, `$&${CONSOLE_FORWARDER_SCRIPT}`)
+        }
+        else if (htmlTag.test(content)) {
+          modifiedContent = content.replace(htmlTag, `$&${CONSOLE_FORWARDER_SCRIPT}`)
         }
packages/react/src/components/Terminal.tsx (1)

69-106: Terminal recreates and loses content when style props change.

The initialization effect depends on [theme, fontFamily, fontSize, lineHeight]. When any of these props change, the terminal is disposed and recreated, resetting lastMessageIndexRef to 0. However, the message-writing effect (lines 134-146) will then re-render all messages from index 0, which is correct behavior but causes a visual flash.

If dynamic theme/font changes are expected, consider using xterm's setOption() API to update options without recreating the terminal.

Alternative approach using setOption
+ // Update terminal options without recreating
+ useEffect(() => {
+   if (!terminalRef.current) return
+   terminalRef.current.options.theme = { ...DEFAULT_THEME, ...theme }
+   terminalRef.current.options.fontFamily = fontFamily
+   terminalRef.current.options.fontSize = fontSize
+   terminalRef.current.options.lineHeight = lineHeight
+   handleResize() // Re-fit after font changes
+ }, [theme, fontFamily, fontSize, lineHeight, handleResize])

  // Initialize terminal - remove style deps from this effect
  useEffect(() => {
    if (!containerRef.current) return

    const terminal = new XTerm({
      theme: { ...DEFAULT_THEME, ...theme },
      fontFamily,
      fontSize,
      lineHeight,
      // ... rest of options
    })
    // ...
-  }, [theme, fontFamily, fontSize, lineHeight])
+  }, []) // Only run once on mount
apps/demo/src/App.tsx (1)

29-45: Dead code: selectedTemplate state and getTitle() are unused.

The selectedTemplate state (line 30) and getTitle() function (lines 34-45) are no longer functional since the template selector is disabled and a hardcoded template is used. Consider removing this dead code or adding a TODO comment if dynamic template switching will be restored later.

apps/demo/src/react.ts (1)

145-154: Dependency duplication between files['package.json'] and top-level dependencies.

The dependencies are defined both in the stringified package.json content (lines 18-27) and in the top-level dependencies/devDependencies fields (lines 145-154). Ensure these are intentionally kept in sync, or consider generating one from the other to avoid drift.

Comment on lines 329 to 331
.playground-console-message.info {
color: #75beff;
}
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 | 🟡 Minor

Duplicate CSS rule for .playground-console-message.info.

This rule (lines 329-331) duplicates the one at lines 286-289. This appears to be an artifact from inserting the terminal styles. Consider removing the duplicate.

Proposed fix
 .playground-panel-terminal .xterm-screen {
   width: 100% !important;
 }
-
-.playground-console-message.info {
-  color: `#75beff`;
-}

 /* Legacy Preview (keep for compatibility) */
📝 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
.playground-console-message.info {
color: #75beff;
}
🤖 Prompt for AI Agents
In `@apps/demo/src/playground.css` around lines 329 - 331, The CSS rule for
`.playground-console-message.info` with `color: `#75beff`;` at lines 329-331 is a
duplicate of the same rule that already exists at lines 286-289. Remove the
duplicate rule block entirely from the playground.css file to eliminate the
redundancy.

Comment thread apps/demo/src/react.ts Outdated
@@ -0,0 +1,161 @@
import type { Template } from '../../packages/react/src/types'
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where Template type is exported from
echo "=== Checking Template type exports ==="
rg -n "export.*Template" --type=ts -g '!node_modules' -g '!*.test.*' | head -30

echo ""
echo "=== Checking packages/react/src/index.ts exports ==="
if [ -f packages/react/src/index.ts ]; then
  cat packages/react/src/index.ts
else
  echo "File not found"
fi

echo ""
echo "=== Checking packages/templates/src/index.ts exports ==="
if [ -f packages/templates/src/index.ts ]; then
  cat packages/templates/src/index.ts
else
  echo "File not found"
fi

echo ""
echo "=== Directory structure of packages ==="
ls -la packages/ 2>/dev/null | head -20

Repository: Oluwasetemi/playground

Length of output: 3483


🏁 Script executed:

#!/bin/bash
# Check package.json files for package names
echo "=== packages/templates/package.json ==="
cat packages/templates/package.json 2>/dev/null | grep -E '"name"' || echo "Not found"

echo ""
echo "=== packages/react/package.json ==="
cat packages/react/package.json 2>/dev/null | grep -E '"name"' || echo "Not found"

echo ""
echo "=== apps/demo/package.json ==="
cat apps/demo/package.json 2>/dev/null | grep -E '"name"' || echo "Not found"

echo ""
echo "=== Root package.json workspaces ==="
cat package.json 2>/dev/null | grep -A 10 '"workspaces"' || echo "Not found"

Repository: Oluwasetemi/playground

Length of output: 326


Fix the broken import path causing the pipeline failure.

The pipeline reports: Cannot find module '../../packages/react/src/types'. The Template type is exported from the @setemiojo/playground-templates package, not from packages/react/src/types.

Proposed fix
-import type { Template } from '../../packages/react/src/types'
+import type { Template } from '@setemiojo/playground-templates'
📝 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
import type { Template } from '../../packages/react/src/types'
import type { Template } from '@setemiojo/playground-templates'
🧰 Tools
🪛 GitHub Actions: CI

[error] 1-1: Cannot find module '../../packages/react/src/types' or its corresponding type declarations. (TS2307)

🤖 Prompt for AI Agents
In `@apps/demo/src/react.ts` at line 1, The import path for the Template type is
incorrect and causes module resolution to fail; update the import statement that
currently references Template (import type { Template } from
'../../packages/react/src/types') to import Template from the package that
actually exports it (the `@setemiojo/playground-templates` package) so the code
imports Template from '@setemiojo/playground-templates' instead of the local
packages path.

Comment on lines +10 to +68
// Console forwarder script to inject into HTML files
const CONSOLE_FORWARDER_SCRIPT = `
<script>
(function() {
if (window.__playgroundConsoleInjected) return;
window.__playgroundConsoleInjected = true;

const originalConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
info: console.info.bind(console),
clear: console.clear.bind(console)
};

function serialize(arg) {
try {
if (arg === undefined) return 'undefined';
if (arg === null) return 'null';
if (typeof arg === 'function') return arg.toString();
if (typeof arg === 'object') {
if (arg instanceof Error) return arg.message + (arg.stack ? '\\n' + arg.stack : '');
return JSON.stringify(arg, null, 2);
}
return String(arg);
} catch (e) {
return String(arg);
}
}

['log', 'warn', 'error', 'info', 'clear'].forEach(function(method) {
console[method] = function() {
var args = Array.prototype.slice.call(arguments);
originalConsole[method].apply(console, args);
try {
window.parent.postMessage({
source: 'playground-console',
type: method,
args: args.map(serialize)
}, '*');
} catch (e) {}
};
});

window.addEventListener('error', function(e) {
window.parent.postMessage({
source: 'playground-console',
type: 'error',
args: [e.message + ' at ' + (e.filename || 'unknown') + ':' + (e.lineno || 0)]
}, '*');
});

window.addEventListener('unhandledrejection', function(e) {
window.parent.postMessage({
source: 'playground-console',
type: 'error',
args: ['Unhandled promise rejection: ' + (e.reason ? (e.reason.message || e.reason) : 'unknown')]
}, '*');
});
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

🏁 Script executed:

# First, let's verify the file exists and examine the exact code
cat -n packages/core/src/webcontainer/FileSystemManager.ts | head -100

Repository: Oluwasetemi/playground

Length of output: 3999


🏁 Script executed:

# Search for where these postMessages are received to understand the full context
rg "playground-console" --type ts --type tsx -B 2 -A 2

Repository: Oluwasetemi/playground

Length of output: 93


🏁 Script executed:

# Check if there's any origin validation or security context in the codebase
rg "event\.origin|targetOrigin|__PLAYGROUND_PARENT_ORIGIN__" --type ts --type tsx

Repository: Oluwasetemi/playground

Length of output: 93


🏁 Script executed:

# Search for postMessage usage patterns in the codebase to understand security practices
rg "postMessage" --type ts --type tsx -B 2 -A 2 | head -80

Repository: Oluwasetemi/playground

Length of output: 93


🏁 Script executed:

# Search for message event listeners and playground-console handlers
rg "message.*playground-console|playground-console.*message" --type ts

Repository: Oluwasetemi/playground

Length of output: 200


🏁 Script executed:

# Search for addEventListener message and postMessage handling
rg "addEventListener.*message|onmessage" --type ts -A 5 -B 2 | head -100

Repository: Oluwasetemi/playground

Length of output: 766


🏁 Script executed:

# Look for any security-related comments or origin validation
rg "origin|cors|CORS|security|postMessage" --type ts -B 1 -A 1 | head -150

Repository: Oluwasetemi/playground

Length of output: 5528


🏁 Script executed:

# Check the full FileSystemManager to see context of injection and usage
cat -n packages/core/src/webcontainer/FileSystemManager.ts | tail -100

Repository: Oluwasetemi/playground

Length of output: 3233


🏁 Script executed:

# Get the full PreviewServer.ts to see the complete message listener implementation
cat -n packages/core/src/preview/PreviewServer.ts | grep -A 20 "setupMessageListener"

Repository: Oluwasetemi/playground

Length of output: 1671


🏁 Script executed:

# Search for where setupMessageListener is called and any origin validation
cat -n packages/core/src/preview/PreviewServer.ts | head -150

Repository: Oluwasetemi/playground

Length of output: 5590


🏁 Script executed:

# Check if there's any window.location.origin or __PLAYGROUND_PARENT_ORIGIN__ configuration
rg "window.location.origin|__PLAYGROUND_PARENT_ORIGIN__|targetOrigin" --type ts

Repository: Oluwasetemi/playground

Length of output: 48


Use explicit targetOrigin instead of wildcard "*" in postMessage calls.

The console forwarder script uses window.parent.postMessage(..., '*') which allows any parent window to receive console output. The receiving code in PreviewServer.ts (lines 87-99) checks only event.data.source but never validates event.origin, creating an information disclosure risk. Replace wildcard with an explicit origin or use window.location.origin as default.

🔒 Proposed fix
(function() {
  if (window.__playgroundConsoleInjected) return;
  window.__playgroundConsoleInjected = true;
+
+  const targetOrigin = window.location.origin;

  const originalConsole = {
    log: console.log.bind(console),
    warn: console.warn.bind(console),
    error: console.error.bind(console),
    info: console.info.bind(console),
    clear: console.clear.bind(console)
  };

  ['log', 'warn', 'error', 'info', 'clear'].forEach(function(method) {
    console[method] = function() {
      var args = Array.prototype.slice.call(arguments);
      originalConsole[method].apply(console, args);
      try {
        window.parent.postMessage({
          source: 'playground-console',
          type: method,
          args: args.map(serialize)
-       }, '*');
+       }, targetOrigin);
      } catch (e) {}
    };
  });

  window.addEventListener('error', function(e) {
    window.parent.postMessage({
      source: 'playground-console',
      type: 'error',
      args: [e.message + ' at ' + (e.filename || 'unknown') + ':' + (e.lineno || 0)]
-   }, '*');
+   }, targetOrigin);
  });

  window.addEventListener('unhandledrejection', function(e) {
    window.parent.postMessage({
      source: 'playground-console',
      type: 'error',
      args: ['Unhandled promise rejection: ' + (e.reason ? (e.reason.message || e.reason) : 'unknown')]
-   }, '*');
+   }, targetOrigin);
  });
})();

Note: Same fix applies to the similar forwarder in PreviewServer.ts (lines 137 and 146).

🤖 Prompt for AI Agents
In `@packages/core/src/webcontainer/FileSystemManager.ts` around lines 10 - 68,
The CONSOLE_FORWARDER_SCRIPT currently calls window.parent.postMessage(...,
'*'); change these postMessage calls to use an explicit targetOrigin (e.g.
window.location.origin or a provided allowedOrigin variable) instead of '*' so
messages are only delivered to the expected parent origin; update all
occurrences inside CONSOLE_FORWARDER_SCRIPT (the console[method] wrapper,
window.addEventListener('error') and 'unhandledrejection' handlers) and apply
the same change to the analogous forwarder usages in PreviewServer.ts to ensure
event.origin can be validated by the receiver.

Comment on lines +90 to 94
// Inject console forwarder into HTML files
const modifiedFiles = this.injectConsoleForwarder(files)

await this.webcontainer.mount(modifiedFiles)

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

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the FileSystemManager.ts file structure
fd FileSystemManager.ts -t f

Repository: Oluwasetemi/playground

Length of output: 117


🏁 Script executed:

# Get the full context around lines 90-94
cat -n packages/core/src/webcontainer/FileSystemManager.ts | head -150 | tail -70

Repository: Oluwasetemi/playground

Length of output: 2920


🏁 Script executed:

# Search for injectConsoleForwarder implementation
rg "injectConsoleForwarder" -A 15 -B 2

Repository: Oluwasetemi/playground

Length of output: 5980


🏁 Script executed:

# Search for mount implementation and understand its behavior
rg "mount\(" -A 5 -B 2 packages/core/src/webcontainer/

Repository: Oluwasetemi/playground

Length of output: 2031


🏁 Script executed:

# Find buildFileTree implementation
rg "buildFileTree" -A 10 -B 2 packages/core/src/webcontainer/FileSystemManager.ts

Repository: Oluwasetemi/playground

Length of output: 1298


🏁 Script executed:

# Search for where mount() is called to understand the flow
rg "\.mount\(" -B 3 -A 3 packages/core/src/

Repository: Oluwasetemi/playground

Length of output: 2822


🏁 Script executed:

# Check if there's an editor component and how it accesses files
rg -l "editor\|Editor" packages/core/src/ | head -10

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Search for editor file access patterns
rg "readFile\|getFile\|file.*content" packages/core/src/ -A 2 -B 2 | head -60

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Check if there's a template comparison or diff mechanism
rg "template\|diff" packages/core/src/ --ignore-case -l | head -10

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Look at the WebContainer's readFile API usage
rg "fs\.read" packages/core/src/webcontainer/ -A 3 -B 1

Repository: Oluwasetemi/playground

Length of output: 884


🏁 Script executed:

# Check the full context of buildFileTree to see what data it exposes
cat -n packages/core/src/webcontainer/FileSystemManager.ts | sed -n '186,250p'

Repository: Oluwasetemi/playground

Length of output: 2368


🏁 Script executed:

# Get more context on PreviewServer's injectConsoleForwarder
rg -A 20 "private injectConsoleForwarder" packages/core/src/preview/PreviewServer.ts

Repository: Oluwasetemi/playground

Length of output: 677


🏁 Script executed:

# Check what CONSOLE_FORWARDER_SCRIPT contains
rg "CONSOLE_FORWARDER_SCRIPT" -B 5 -A 15

Repository: Oluwasetemi/playground

Length of output: 4329


🏁 Script executed:

# Search for how the editor or UI accesses file content to see if it exposes the injected script
rg "readFile\|getFile" packages/core/src/ -B 2 -A 5 | head -100

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Look for readFile usage patterns and see if it's called on HTML
rg "readFile\(" packages/core/src/ -B 3 -A 3

Repository: Oluwasetemi/playground

Length of output: 4076


🏁 Script executed:

# Check if there's an editor component that might display file contents
fd -e ts -e tsx packages/core/src/ | xargs grep -l "editor\|Editor" | head -5

Repository: Oluwasetemi/playground

Length of output: 443


🏁 Script executed:

# Look at the full buildFileTree to understand what it returns
cat -n packages/core/src/webcontainer/FileSystemManager.ts | sed -n '234,280p'

Repository: Oluwasetemi/playground

Length of output: 1730


🏁 Script executed:

# Find the editor implementation to see if it filters or strips injected content
rg "openFile" packages/core/src/ -B 2 -A 5

Repository: Oluwasetemi/playground

Length of output: 4308


🏁 Script executed:

# Check if there's any filtering of the injected script before displaying in editor
rg "CONSOLE_FORWARDER\|strip\|remove.*script\|filter" packages/core/src/ -i -B 2 -A 2

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Look for any removal of injected content or special handling
rg "openFile\|setContent\|setFile" packages/core/src/editor -A 5 2>/dev/null || echo "editor dir not found, searching in UI"

Repository: Oluwasetemi/playground

Length of output: 103


🏁 Script executed:

# Check if PreviewServer's injection duplicates FileSystemManager's injection
rg "injectConsoleForwarder\|CONSOLE_FORWARDER" packages/core/src/ -l

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Get the full openFile implementation in EditorController
cat -n packages/core/src/editor/EditorController.ts | grep -A 20 "async openFile"

Repository: Oluwasetemi/playground

Length of output: 658


🏁 Script executed:

# Check if there's any snapshot storage or comparison that might be affected by the injected script
rg "saveSnapshot\|getSnapshot" packages/core/src/ -A 10 | head -80

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Look at the full snapshot mechanism to understand the flow
cat -n packages/core/src/engine/PlaygroundEngine.ts | sed -n '1,50p'

Repository: Oluwasetemi/playground

Length of output: 2552


🏁 Script executed:

# Check if there's any post-processing or filtering of file content before display
rg "snapshot\|save.*file\|persist" packages/core/src/engine/PlaygroundEngine.ts -B 2 -A 5 | head -100

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Verify that readFile returns the full content including injected script
cat -n packages/core/src/webcontainer/FileSystemManager.ts | sed -n '159,175p'

Repository: Oluwasetemi/playground

Length of output: 604


HTML injection at mount-time will leak into editor and snapshots.

injectConsoleForwarder() runs before mount(), so HTML files in the webcontainer filesystem are permanently modified with the injected script. When users open HTML files in the editor, they will see the injected <script> block; similarly, snapshots will persist the modified content. Consider moving the injection to preview/serve time instead, matching the approach in PreviewServer.injectConsoleForwarder(), which injects only in the iframe without modifying stored files.

🤖 Prompt for AI Agents
In `@packages/core/src/webcontainer/FileSystemManager.ts` around lines 90 - 94,
The current flow injects the console-forwarding script into files before calling
this.webcontainer.mount (via injectConsoleForwarder), which mutates stored HTML
and leaks into the editor and snapshots; instead, remove the pre-mount mutation
and move injection to runtime preview/serve path (mirror
PreviewServer.injectConsoleForwarder) so files passed to this.webcontainer.mount
remain unmodified and the script is injected only into served/iframe responses;
update FileSystemManager to stop calling injectConsoleForwarder before mount and
ensure preview-serving code calls the non-mutating injection routine.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/demo/src/App.tsx (1)

1-19: Fix import ordering and remove disallowed semicolons.

ESLint flags both perfectionist/sort-imports and style/semi in the import section. Reorder the template imports ahead of React and drop the trailing semicolons.

🛠️ Proposed fix
-import type { Template } from '@setemiojo/playground-templates'
+import type { Template } from "@setemiojo/playground-templates"
 import {
-  Playground,
-  PlaygroundEditor,
-  PlaygroundFileTree,
-  PlaygroundHeader,
-  PlaygroundPanel,
-  ResizablePanel,
-} from "@setemiojo/playground-react";
-import { useState } from "react";
+  nodeTemplate,
+  reactTemplate,
+  vanillaTemplate,
+  vueTemplate,
+} from "@setemiojo/playground-templates"
+import {
+  Playground,
+  PlaygroundEditor,
+  PlaygroundFileTree,
+  PlaygroundHeader,
+  PlaygroundPanel,
+  ResizablePanel,
+} from "@setemiojo/playground-react"
+import { useState } from "react"
 // import { reactTemplate as newTemp } from "./react.ts";
-import {
-  nodeTemplate,
-  reactTemplate,
-  vanillaTemplate,
-  vueTemplate,
-} from '@setemiojo/playground-templates'
-import "./playground.css";
-import "./App.css";
+import "./playground.css"
+import "./App.css"
🤖 Fix all issues with AI agents
In `@apps/demo/src/App.tsx`:
- Line 52: Remove unnecessary parens from single-arg arrow functions and wrap
multiline JSX in parentheses: change handlers like the onChange arrow that calls
setSelectedTemplate from "(e) => setSelectedTemplate(e.target.value)" to use the
single-arg form "e => setSelectedTemplate(e.target.value)", and ensure any
multiline JSX blocks (the JSX expression around the component/fragment
referenced in the render returned JSX, specifically the block covering lines
82-91) are wrapped in parentheses so the JSX-wrap-multilines rule is satisfied.
- Around line 29-44: Remove the stray semicolons in the component body: delete
the unnecessary semicolons after the useState declarations
(selectedTemplate/setSelectedTemplate and showSidebar/setShowSidebar), remove
the extra semicolons after each return in the getTitle switch cases and the
trailing semicolon after the getTitle function (so getTitle ends with } not };),
and remove any extra semicolon following the component return. Target the
identifiers selectedTemplate, setSelectedTemplate, showSidebar, setShowSidebar,
template, and getTitle when making these edits.

In `@packages/core/src/editor/CodeMirrorEditor.ts`:
- Around line 201-209: In handleContentChange, capture the current active file
and its content immediately before scheduling the debounce so the timeout
callback emits the change for the correct file; specifically, read
this.activeFile into a local variable (e.g., currentFile) and call
this.getContent() into another local (e.g., currentContent) prior to
window.setTimeout, then use those locals inside the timeout instead of accessing
this.activeFile/getContent at fire time; keep using this.updateTimeout for
clearing and this.events.emit('file:change', ...) but pass the captured locals
to avoid mis-attribution when switching files quickly.

In `@packages/core/src/editor/editorFactory.ts`:
- Around line 108-113: getLineNumbers currently returns
monacoEditor?.getLineNumbers() || true which will coerce a false value to true;
change it so it only defaults to true when the editor is missing (e.g., use
nullish check). In practice, update getLineNumbers to return
monacoEditor?.getLineNumbers() ?? true (or equivalent conditional) so false from
monacoEditor.getLineNumbers() is preserved; keep setLineNumbers and monacoEditor
references intact.

In `@packages/core/src/editor/MonacoEditor.ts`:
- Around line 429-437: handleContentChange currently reads this.activeFile when
the timeout fires, which can mis-attribute edits after quick file switches;
capture the current activeFile synchronously before scheduling the timeout
(e.g., const fileAtSchedule = this.activeFile) and use that captured variable
inside the setTimeout callback when calling this.events.emit('file:change',
fileAtSchedule, content), leaving the rest of the debounce logic
(this.updateTimeout, clearTimeout, this.getContent()) intact.
- Around line 154-190: In configureMonacoEnvironment replace all uses of the
browser global self with globalThis (e.g. check
globalThis.MonacoEnvironment?.getWorker and assign globalThis.MonacoEnvironment)
and inside the getWorker async function destructure the imported modules to
obtain the default export as a constructor (e.g. const { default: JsonWorker } =
await import(...)) and then instantiate via new JsonWorker() for JsonWorker,
CssWorker, HtmlWorker, TsWorker and EditorWorker; leave the catch fallback to
this.createCdnWorker(label) unchanged so ESLint no-restricted-globals and
new-cap errors are resolved while retaining getWorker/createCdnWorker behavior.
- Around line 130-143: The if-block in initializeServices checks
StandaloneServices.get and then returns, which is a no-op since the function
ends with only comments and triggers ESLint no-useless-return; remove the early
return (or replace it with a meaningful branch) so the function simply checks
the condition without returning—locate the initializeServices method and the
StandaloneServices.get check and delete the redundant "return" to satisfy the
linter.

In `@packages/core/src/index.ts`:
- Around line 20-27: The export block in packages/core/src/index.ts is failing
perfectionist/sort-exports and sort-named-exports; reorder the exports so named
exports are alphabetized and grouped consistently (types vs. values) to satisfy
the linters: ensure type exports like FileDiff and WebContainerAuthConfig are in
sorted order and the editor-related exports (EditorController, CodeMirrorEditor,
createEditor, createEditorAsync, isMonacoAvailable) and the type list
(EditorAdapter, EditorFactory, EditorOptions, EditorType) are each sorted
alphabetically within their groups; update the export order for those symbols
accordingly.

In `@packages/react/src/styles/playground.css`:
- Around line 78-84: The current CSS rules for ".editor-container .monaco-editor
a" and ".editor-container .monaco-editor a:hover" remove any link affordance
making links invisible; instead, keep links styled plainly by removing underline
on the base selector but add a clear hover affordance on the hover selector (for
example enable text-decoration: underline or a subtle border-bottom/highlight
using the theme link color) so links remain discoverable in the Monaco editor;
update those two selectors accordingly to use text-decoration on hover rather
than unconditionally removing the visual cue.
🧹 Nitpick comments (1)
packages/core/src/vite-env.d.ts (1)

4-42: Extract shared type to reduce duplication and fix type structure.

All five worker module declarations use an identical type structure that can be simplified. Extract a shared type alias for maintainability.

Additionally, the current type { new (): Worker; default: { new (): Worker } } conflates the constructor with the module namespace. The workerConstructor itself should simply be { new (): Worker }; the default property comes from the module system when using dynamic import().

♻️ Suggested refactor
 /// <reference types="vite/client" />
 
 // Worker module declarations for Monaco editor workers
+type ViteWorkerConstructor = {
+  new (): Worker
+}
+
 declare module 'monaco-editor/esm/vs/language/json/json.worker?worker' {
-  const workerConstructor: {
-    new (): Worker
-    default: { new (): Worker }
-  }
+  const workerConstructor: ViteWorkerConstructor
   export default workerConstructor
 }
 
 declare module 'monaco-editor/esm/vs/language/css/css.worker?worker' {
-  const workerConstructor: {
-    new (): Worker
-    default: { new (): Worker }
-  }
+  const workerConstructor: ViteWorkerConstructor
   export default workerConstructor
 }
 
 declare module 'monaco-editor/esm/vs/language/html/html.worker?worker' {
-  const workerConstructor: {
-    new (): Worker
-    default: { new (): Worker }
-  }
+  const workerConstructor: ViteWorkerConstructor
   export default workerConstructor
 }
 
 declare module 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' {
-  const workerConstructor: {
-    new (): Worker
-    default: { new (): Worker }
-  }
+  const workerConstructor: ViteWorkerConstructor
   export default workerConstructor
 }
 
 declare module 'monaco-editor/esm/vs/editor/editor.worker?worker' {
-  const workerConstructor: {
-    new (): Worker
-    default: { new (): Worker }
-  }
+  const workerConstructor: ViteWorkerConstructor
   export default workerConstructor
 }

Comment thread apps/demo/src/App.tsx
Comment on lines +29 to +44
const [selectedTemplate, setSelectedTemplate] = useState<string>("react");
const [showSidebar, setShowSidebar] = useState(false);
const template = templates[selectedTemplate]

const getTitle = () => {
switch (selectedTemplate) {
case 'react':
return 'React Playground'
case 'vue':
return 'Vue Playground'
case 'node':
return 'Node.js Playground'
case "react":
return "React Playground";
case "vue":
return "Vue Playground";
case "node":
return "Node.js Playground";
default:
return 'JavaScript Playground'
return "JavaScript Playground";
}
}
};
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 | 🟡 Minor

Remove extra semicolons in component body.

style/semi is erroring on the state declarations, switch returns, and component return.

🛠️ Proposed fix
-  const [selectedTemplate, setSelectedTemplate] = useState<string>("react");
-  const [showSidebar, setShowSidebar] = useState(false);
+  const [selectedTemplate, setSelectedTemplate] = useState<string>("react")
+  const [showSidebar, setShowSidebar] = useState(false)
@@
-      case "react":
-        return "React Playground";
-      case "vue":
-        return "Vue Playground";
-      case "node":
-        return "Node.js Playground";
+      case "react":
+        return "React Playground"
+      case "vue":
+        return "Vue Playground"
+      case "node":
+        return "Node.js Playground"
@@
-      default:
-        return "JavaScript Playground";
+      default:
+        return "JavaScript Playground"
     }
-  };
+  }
@@
-  );
+  )

Also applies to: 107-107

🧰 Tools
🪛 ESLint

[error] 29-29: Extra semicolon.

(style/semi)


[error] 30-30: Extra semicolon.

(style/semi)


[error] 36-36: Extra semicolon.

(style/semi)


[error] 38-38: Extra semicolon.

(style/semi)


[error] 40-40: Extra semicolon.

(style/semi)


[error] 42-42: Extra semicolon.

(style/semi)


[error] 44-44: Extra semicolon.

(style/semi)

🤖 Prompt for AI Agents
In `@apps/demo/src/App.tsx` around lines 29 - 44, Remove the stray semicolons in
the component body: delete the unnecessary semicolons after the useState
declarations (selectedTemplate/setSelectedTemplate and
showSidebar/setShowSidebar), remove the extra semicolons after each return in
the getTitle switch cases and the trailing semicolon after the getTitle function
(so getTitle ends with } not };), and remove any extra semicolon following the
component return. Target the identifiers selectedTemplate, setSelectedTemplate,
showSidebar, setShowSidebar, template, and getTitle when making these edits.

Comment thread apps/demo/src/App.tsx
disabled
value={selectedTemplate}
onChange={e => setSelectedTemplate(e.target.value)}
onChange={(e) => setSelectedTemplate(e.target.value)}
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 | 🟡 Minor

Satisfy arrow-parens and jsx-wrap-multilines rules.

Lint requires removing unnecessary parens for single-arg arrows and wrapping multiline JSX expressions.

🛠️ Proposed fix
-          onChange={(e) => setSelectedTemplate(e.target.value)}
+          onChange={e => setSelectedTemplate(e.target.value)}
@@
-              <ResizablePanel
-                firstPanel={
-                  <div className="playground-editor-section">
-                    <PlaygroundEditor />
-                  </div>
-                }
-                secondPanel={
-                  <div className="playground-preview-section">
-                    <PlaygroundPanel />
-                  </div>
-                }
+              <ResizablePanel
+                firstPanel={(
+                  <div className="playground-editor-section">
+                    <PlaygroundEditor />
+                  </div>
+                )}
+                secondPanel={(
+                  <div className="playground-preview-section">
+                    <PlaygroundPanel />
+                  </div>
+                )}

Also applies to: 82-91

🧰 Tools
🪛 ESLint

[error] 52-52: Unexpected parentheses around single function argument having a body with no curly braces.

(style/arrow-parens)

🤖 Prompt for AI Agents
In `@apps/demo/src/App.tsx` at line 52, Remove unnecessary parens from single-arg
arrow functions and wrap multiline JSX in parentheses: change handlers like the
onChange arrow that calls setSelectedTemplate from "(e) =>
setSelectedTemplate(e.target.value)" to use the single-arg form "e =>
setSelectedTemplate(e.target.value)", and ensure any multiline JSX blocks (the
JSX expression around the component/fragment referenced in the render returned
JSX, specifically the block covering lines 82-91) are wrapped in parentheses so
the JSX-wrap-multilines rule is satisfied.

Comment on lines +201 to +209
private handleContentChange(): void {
if (this.updateTimeout) {
clearTimeout(this.updateTimeout)
}

this.updateTimeout = window.setTimeout(() => {
const content = this.getContent()
this.events.emit('file:change', this.activeFile, content)
}, 300)
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

Debounced change can mis-attribute edits when switching files quickly.

The timeout callback reads this.activeFile/getContent() at fire time; if the user switches tabs within 300 ms, the change can be emitted for the wrong file (or dropped for the previous file).

✅ Proposed fix
   private handleContentChange(): void {
     if (this.updateTimeout) {
       clearTimeout(this.updateTimeout)
     }
 
+    const path = this.activeFile
+    const content = this.getContent()
+
     this.updateTimeout = window.setTimeout(() => {
-      const content = this.getContent()
-      this.events.emit('file:change', this.activeFile, content)
+      this.events.emit('file:change', path, content)
     }, 300)
   }
📝 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
private handleContentChange(): void {
if (this.updateTimeout) {
clearTimeout(this.updateTimeout)
}
this.updateTimeout = window.setTimeout(() => {
const content = this.getContent()
this.events.emit('file:change', this.activeFile, content)
}, 300)
private handleContentChange(): void {
if (this.updateTimeout) {
clearTimeout(this.updateTimeout)
}
const path = this.activeFile
const content = this.getContent()
this.updateTimeout = window.setTimeout(() => {
this.events.emit('file:change', path, content)
}, 300)
🤖 Prompt for AI Agents
In `@packages/core/src/editor/CodeMirrorEditor.ts` around lines 201 - 209, In
handleContentChange, capture the current active file and its content immediately
before scheduling the debounce so the timeout callback emits the change for the
correct file; specifically, read this.activeFile into a local variable (e.g.,
currentFile) and call this.getContent() into another local (e.g.,
currentContent) prior to window.setTimeout, then use those locals inside the
timeout instead of accessing this.activeFile/getContent at fire time; keep using
this.updateTimeout for clearing and this.events.emit('file:change', ...) but
pass the captured locals to avoid mis-attribution when switching files quickly.

Comment on lines +108 to +113
setLineNumbers(show) {
monacoEditor?.setLineNumbers(show)
},
getLineNumbers() {
return monacoEditor?.getLineNumbers() || 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 | 🟠 Major

Fix getLineNumbers to preserve false.
Line 112 uses || true, which forces true when line numbers are disabled—callers can never observe false. Default only when the editor isn’t loaded.

✅ Proposed fix
-    getLineNumbers() {
-      return monacoEditor?.getLineNumbers() || true
-    },
+    getLineNumbers() {
+      return monacoEditor?.getLineNumbers() ?? true
+    },
📝 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
setLineNumbers(show) {
monacoEditor?.setLineNumbers(show)
},
getLineNumbers() {
return monacoEditor?.getLineNumbers() || true
},
setLineNumbers(show) {
monacoEditor?.setLineNumbers(show)
},
getLineNumbers() {
return monacoEditor?.getLineNumbers() ?? true
},
🤖 Prompt for AI Agents
In `@packages/core/src/editor/editorFactory.ts` around lines 108 - 113,
getLineNumbers currently returns monacoEditor?.getLineNumbers() || true which
will coerce a false value to true; change it so it only defaults to true when
the editor is missing (e.g., use nullish check). In practice, update
getLineNumbers to return monacoEditor?.getLineNumbers() ?? true (or equivalent
conditional) so false from monacoEditor.getLineNumbers() is preserved; keep
setLineNumbers and monacoEditor references intact.

Comment on lines +130 to +143
private async initializeServices(_monaco: Monaco): Promise<void> {
try {
// Import StandaloneServices for proper initialization
// @ts-expect-error - Monaco internal API
const { StandaloneServices } = await import('monaco-editor/esm/vs/editor/standalone/browser/standaloneServices')

// Check if services are already initialized
if (!StandaloneServices.get) {
return
}

// Services are automatically initialized on first editor creation
// We just need to ensure Monaco is ready
}
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

🏁 Script executed:

fd "MonacoEditor.ts" --type f

Repository: Oluwasetemi/playground

Length of output: 106


🏁 Script executed:

cat -n packages/core/src/editor/MonacoEditor.ts | sed -n '120,150p'

Repository: Oluwasetemi/playground

Length of output: 1250


🏁 Script executed:

# Check for ESLint configuration and linting issues
find . -name ".eslintrc*" -o -name "eslint.config.*" | head -5

Repository: Oluwasetemi/playground

Length of output: 461


🏁 Script executed:

cat -n eslint.config.mjs | head -60

Repository: Oluwasetemi/playground

Length of output: 361


🏁 Script executed:

# Check if there's a way to see the actual linting rules in the parent config
# and verify the pattern of the proposed fix
rg "no-useless-return" . --type js --type ts | head -10

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Let's check what the parent eslint config (`@setemiojo/eslint-config`) includes
# by looking at whether no-useless-return is part of standard ESLint rules
# and verify the logic of the proposed fix

# First, let's understand the context better - check the full initializeServices method
cat -n packages/core/src/editor/MonacoEditor.ts | sed -n '126,148p'

Repository: Oluwasetemi/playground

Length of output: 1026


🏁 Script executed:

# Let's check the complete context and see if there's any code after line 143
# that might execute, which would make the return not useless
wc -l packages/core/src/editor/MonacoEditor.ts

Repository: Oluwasetemi/playground

Length of output: 110


🏁 Script executed:

# Let me verify the ESLint no-useless-return rule behavior
# by checking if there's any actual code after the if block that would execute
cat -n packages/core/src/editor/MonacoEditor.ts | sed -n '130,148p' | cat -A

Repository: Oluwasetemi/playground

Length of output: 922


🏁 Script executed:

# Let's verify the proposed fix makes semantic sense
# by understanding what the import is trying to achieve
rg "StandaloneServices" packages/core/src/editor/MonacoEditor.ts -A 2 -B 2

Repository: Oluwasetemi/playground

Length of output: 890


Remove the no‑op return to satisfy no-useless-return.

The return statement at line 138 is flagged by ESLint because it doesn't change the control flow—the function ends regardless of whether the condition is met. The code after the if block contains only comments, so the early return provides no functional benefit.

🧹 Proposed fix
-      // Check if services are already initialized
-      if (!StandaloneServices.get) {
-        return
-      }
-
-      // Services are automatically initialized on first editor creation
-      // We just need to ensure Monaco is ready
+      // Services are automatically initialized on first editor creation.
+      // Touch the API so the import isn't tree-shaken.
+      void StandaloneServices.get
📝 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
private async initializeServices(_monaco: Monaco): Promise<void> {
try {
// Import StandaloneServices for proper initialization
// @ts-expect-error - Monaco internal API
const { StandaloneServices } = await import('monaco-editor/esm/vs/editor/standalone/browser/standaloneServices')
// Check if services are already initialized
if (!StandaloneServices.get) {
return
}
// Services are automatically initialized on first editor creation
// We just need to ensure Monaco is ready
}
private async initializeServices(_monaco: Monaco): Promise<void> {
try {
// Import StandaloneServices for proper initialization
// `@ts-expect-error` - Monaco internal API
const { StandaloneServices } = await import('monaco-editor/esm/vs/editor/standalone/browser/standaloneServices')
// Services are automatically initialized on first editor creation.
// Touch the API so the import isn't tree-shaken.
void StandaloneServices.get
}
🧰 Tools
🪛 ESLint

[error] 138-138: Unnecessary return statement.

(no-useless-return)

🤖 Prompt for AI Agents
In `@packages/core/src/editor/MonacoEditor.ts` around lines 130 - 143, The
if-block in initializeServices checks StandaloneServices.get and then returns,
which is a no-op since the function ends with only comments and triggers ESLint
no-useless-return; remove the early return (or replace it with a meaningful
branch) so the function simply checks the condition without returning—locate the
initializeServices method and the StandaloneServices.get check and delete the
redundant "return" to satisfy the linter.

Comment on lines +154 to +190
private configureMonacoEnvironment(): void {
// Skip if already configured
if (self.MonacoEnvironment?.getWorker) {
return
}

// Configure Monaco to use workers
// We'll try Vite worker imports first, fallback to CDN
self.MonacoEnvironment = {
getWorker: async (_workerId: string, label: string) => {
// Try to use Vite worker imports (recommended)
try {
if (label === 'json') {
const JsonWorker = await import('monaco-editor/esm/vs/language/json/json.worker?worker')
return new JsonWorker.default()
}
if (label === 'css' || label === 'scss' || label === 'less') {
const CssWorker = await import('monaco-editor/esm/vs/language/css/css.worker?worker')
return new CssWorker.default()
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
const HtmlWorker = await import('monaco-editor/esm/vs/language/html/html.worker?worker')
return new HtmlWorker.default()
}
if (label === 'typescript' || label === 'javascript') {
const TsWorker = await import('monaco-editor/esm/vs/language/typescript/ts.worker?worker')
return new TsWorker.default()
}
const EditorWorker = await import('monaco-editor/esm/vs/editor/editor.worker?worker')
return new EditorWorker.default()
}
catch {
// Fallback to CDN workers if Vite imports fail
return this.createCdnWorker(label)
}
},
}
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

🏁 Script executed:

# Find ESLint configuration
find . -maxdepth 3 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) 2>/dev/null | head -20

Repository: Oluwasetemi/playground

Length of output: 188


🏁 Script executed:

# Check if the MonacoEditor.ts file exists and view the relevant code
git ls-files | grep -i "monacoeditor"

Repository: Oluwasetemi/playground

Length of output: 106


🏁 Script executed:

# List package structure to understand the layout
ls -la packages/core/src/editor/ 2>/dev/null | head -20

Repository: Oluwasetemi/playground

Length of output: 526


🏁 Script executed:

# View the MonacoEditor.ts file, focusing on lines 154-190
sed -n '154,190p' packages/core/src/editor/MonacoEditor.ts

Repository: Oluwasetemi/playground

Length of output: 1646


🏁 Script executed:

# Check for ESLint configuration files in the project root and relevant directories
find . -maxdepth 2 -type f -name ".eslintrc*" -o -name "eslint.config.*" -o -name "package.json" | grep -E "eslint|package" | head -20

Repository: Oluwasetemi/playground

Length of output: 124


🏁 Script executed:

# Look for ESLint configuration in package.json or dedicated config files
cat package.json 2>/dev/null | head -100

Repository: Oluwasetemi/playground

Length of output: 1216


🏁 Script executed:

# Check the ESLint configuration file
cat ./eslint.config.mjs

Repository: Oluwasetemi/playground

Length of output: 284


🏁 Script executed:

# Check if there are any ESLint disable comments in the MonacoEditor.ts file
grep -n "eslint" packages/core/src/editor/MonacoEditor.ts

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Check the `@setemiojo/eslint-config` to understand what rules are enabled
npm list `@setemiojo/eslint-config` 2>/dev/null || echo "Not installed"

Repository: Oluwasetemi/playground

Length of output: 553


🏁 Script executed:

# Search for any references to ESLint rule overrides in the codebase
rg "new-cap|no-restricted-globals" .

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Check if there's a .eslintignore file or any other ESLint config
find . -maxdepth 2 -name ".eslintignore" -o -name "eslint*" -type f

Repository: Oluwasetemi/playground

Length of output: 85


🏁 Script executed:

# Look at the entire MonacoEditor.ts file to see if there are any ESLint comments or context we're missing
wc -l packages/core/src/editor/MonacoEditor.ts

Repository: Oluwasetemi/playground

Length of output: 110


🏁 Script executed:

# Let's look at the actual `@setemiojo/eslint-config` to understand the rules
find ./node_modules/@setemiojo/eslint-config -type f -name "*.js" -o -name "*.mjs" -o -name "*.json" | head -20

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Check the package.json of `@setemiojo/eslint-config`
cat ./node_modules/.pnpm/@setemiojo+eslint-config@6.2.0_*/node_modules/@setemiojo/eslint-config/package.json 2>/dev/null | head -50

Repository: Oluwasetemi/playground

Length of output: 1560


🏁 Script executed:

# Search for where this config is defined
find ./node_modules -path "*@setemiojo/eslint-config*" -type f -name "index.*" | head -5

Repository: Oluwasetemi/playground

Length of output: 648


🏁 Script executed:

# Look at the actual imports and entire context in MonacoEditor to understand the full picture
head -30 packages/core/src/editor/MonacoEditor.ts

Repository: Oluwasetemi/playground

Length of output: 1301


🏁 Script executed:

# Check the actual ESLint config rules from the dist/index.js
head -200 ./node_modules/.pnpm/@setemiojo+eslint-config@6.2.0_@vue+compiler-sfc@3.5.25_eslint@9.39.2_jiti@2.6.1__types_e754690a75991020b17cb20945e0d261/node_modules/@setemiojo/eslint-config/dist/index.js

Repository: Oluwasetemi/playground

Length of output: 5986


🏁 Script executed:

# Search for 'self' global references in ESLint configs and TypeScript lib configs
rg "resticted.*global|no-restricted-globals|self" . --type-list | head -20

Repository: Oluwasetemi/playground

Length of output: 455


🏁 Script executed:

# Check if there are TypeScript lib or compiler options affecting 'self'
find . -maxdepth 3 -name "tsconfig.json" -exec cat {} \;

Repository: Oluwasetemi/playground

Length of output: 1707


🏁 Script executed:

# Continue reading the ESLint config to find the rules section
sed -n '200,500p' ./node_modules/.pnpm/@setemiojo+eslint-config@6.2.0_@vue+compiler-sfc@3.5.25_eslint@9.39.2_jiti@2.6.1__types_e754690a75991020b17cb20945e0d261/node_modules/@setemiojo/eslint-config/dist/index.js | head -300

Repository: Oluwasetemi/playground

Length of output: 8802


🏁 Script executed:

# Search for 'new-cap' or 'no-restricted-globals' in the ESLint config
grep -i "new-cap\|no-restricted-globals\|self" ./node_modules/.pnpm/@setemiojo+eslint-config@6.2.0_@vue+compiler-sfc@3.5.25_eslint@9.39.2_jiti@2.6.1__types_e754690a75991020b17cb20945e0d261/node_modules/@setemiojo/eslint-config/dist/index.js

Repository: Oluwasetemi/playground

Length of output: 287


🏁 Script executed:

# Check if there are any ESLint errors/warnings when running lint on this file
cd packages/core && npx eslint src/editor/MonacoEditor.ts 2>&1 || true

Repository: Oluwasetemi/playground

Length of output: 1052


Replace self with globalThis and destructure worker constructors to fix ESLint errors.

The code violates two ESLint rules: no-restricted-globals (which restricts self) and new-cap (which requires constructor names to start with uppercase). The current code at lines 156, 162, and 168–183 triggers 9 linting errors that must be resolved.

Proposed fix
   private configureMonacoEnvironment(): void {
+    const globalScope = globalThis as typeof self
     // Skip if already configured
-    if (self.MonacoEnvironment?.getWorker) {
+    if (globalScope.MonacoEnvironment?.getWorker) {
       return
     }
 
     // Configure Monaco to use workers
     // We'll try Vite worker imports first, fallback to CDN
-    self.MonacoEnvironment = {
+    globalScope.MonacoEnvironment = {
       getWorker: async (_workerId: string, label: string) => {
         // Try to use Vite worker imports (recommended)
         try {
           if (label === 'json') {
-            const JsonWorker = await import('monaco-editor/esm/vs/language/json/json.worker?worker')
-            return new JsonWorker.default()
+            const { default: JsonWorker } = await import('monaco-editor/esm/vs/language/json/json.worker?worker')
+            return new JsonWorker()
           }
           if (label === 'css' || label === 'scss' || label === 'less') {
-            const CssWorker = await import('monaco-editor/esm/vs/language/css/css.worker?worker')
-            return new CssWorker.default()
+            const { default: CssWorker } = await import('monaco-editor/esm/vs/language/css/css.worker?worker')
+            return new CssWorker()
           }
           if (label === 'html' || label === 'handlebars' || label === 'razor') {
-            const HtmlWorker = await import('monaco-editor/esm/vs/language/html/html.worker?worker')
-            return new HtmlWorker.default()
+            const { default: HtmlWorker } = await import('monaco-editor/esm/vs/language/html/html.worker?worker')
+            return new HtmlWorker()
           }
           if (label === 'typescript' || label === 'javascript') {
-            const TsWorker = await import('monaco-editor/esm/vs/language/typescript/ts.worker?worker')
-            return new TsWorker.default()
+            const { default: TsWorker } = await import('monaco-editor/esm/vs/language/typescript/ts.worker?worker')
+            return new TsWorker()
           }
-          const EditorWorker = await import('monaco-editor/esm/vs/editor/editor.worker?worker')
-          return new EditorWorker.default()
+          const { default: EditorWorker } = await import('monaco-editor/esm/vs/editor/editor.worker?worker')
+          return new EditorWorker()
         }
         catch {
           // Fallback to CDN workers if Vite imports fail
           return this.createCdnWorker(label)
         }
       },
     }
   }
📝 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
private configureMonacoEnvironment(): void {
// Skip if already configured
if (self.MonacoEnvironment?.getWorker) {
return
}
// Configure Monaco to use workers
// We'll try Vite worker imports first, fallback to CDN
self.MonacoEnvironment = {
getWorker: async (_workerId: string, label: string) => {
// Try to use Vite worker imports (recommended)
try {
if (label === 'json') {
const JsonWorker = await import('monaco-editor/esm/vs/language/json/json.worker?worker')
return new JsonWorker.default()
}
if (label === 'css' || label === 'scss' || label === 'less') {
const CssWorker = await import('monaco-editor/esm/vs/language/css/css.worker?worker')
return new CssWorker.default()
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
const HtmlWorker = await import('monaco-editor/esm/vs/language/html/html.worker?worker')
return new HtmlWorker.default()
}
if (label === 'typescript' || label === 'javascript') {
const TsWorker = await import('monaco-editor/esm/vs/language/typescript/ts.worker?worker')
return new TsWorker.default()
}
const EditorWorker = await import('monaco-editor/esm/vs/editor/editor.worker?worker')
return new EditorWorker.default()
}
catch {
// Fallback to CDN workers if Vite imports fail
return this.createCdnWorker(label)
}
},
}
private configureMonacoEnvironment(): void {
const globalScope = globalThis as typeof self
// Skip if already configured
if (globalScope.MonacoEnvironment?.getWorker) {
return
}
// Configure Monaco to use workers
// We'll try Vite worker imports first, fallback to CDN
globalScope.MonacoEnvironment = {
getWorker: async (_workerId: string, label: string) => {
// Try to use Vite worker imports (recommended)
try {
if (label === 'json') {
const { default: JsonWorker } = await import('monaco-editor/esm/vs/language/json/json.worker?worker')
return new JsonWorker()
}
if (label === 'css' || label === 'scss' || label === 'less') {
const { default: CssWorker } = await import('monaco-editor/esm/vs/language/css/css.worker?worker')
return new CssWorker()
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
const { default: HtmlWorker } = await import('monaco-editor/esm/vs/language/html/html.worker?worker')
return new HtmlWorker()
}
if (label === 'typescript' || label === 'javascript') {
const { default: TsWorker } = await import('monaco-editor/esm/vs/language/typescript/ts.worker?worker')
return new TsWorker()
}
const { default: EditorWorker } = await import('monaco-editor/esm/vs/editor/editor.worker?worker')
return new EditorWorker()
}
catch {
// Fallback to CDN workers if Vite imports fail
return this.createCdnWorker(label)
}
},
}
}
🧰 Tools
🪛 ESLint

[error] 156-156: Unexpected use of 'self'. Use globalThis instead.

(no-restricted-globals)


[error] 162-162: Unexpected use of 'self'. Use globalThis instead.

(no-restricted-globals)


[error] 168-168: A constructor name should not start with a lowercase letter.

(new-cap)


[error] 172-172: A constructor name should not start with a lowercase letter.

(new-cap)


[error] 176-176: A constructor name should not start with a lowercase letter.

(new-cap)


[error] 180-180: A constructor name should not start with a lowercase letter.

(new-cap)


[error] 183-183: A constructor name should not start with a lowercase letter.

(new-cap)

🤖 Prompt for AI Agents
In `@packages/core/src/editor/MonacoEditor.ts` around lines 154 - 190, In
configureMonacoEnvironment replace all uses of the browser global self with
globalThis (e.g. check globalThis.MonacoEnvironment?.getWorker and assign
globalThis.MonacoEnvironment) and inside the getWorker async function
destructure the imported modules to obtain the default export as a constructor
(e.g. const { default: JsonWorker } = await import(...)) and then instantiate
via new JsonWorker() for JsonWorker, CssWorker, HtmlWorker, TsWorker and
EditorWorker; leave the catch fallback to this.createCdnWorker(label) unchanged
so ESLint no-restricted-globals and new-cap errors are resolved while retaining
getWorker/createCdnWorker behavior.

Comment on lines +429 to +437
private handleContentChange(): void {
if (this.updateTimeout) {
clearTimeout(this.updateTimeout)
}

this.updateTimeout = window.setTimeout(() => {
const content = this.getContent()
this.events.emit('file:change', this.activeFile, content)
}, 300)
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

Debounced change can mis-attribute edits when switching files quickly.

The timeout callback reads the current activeFile at fire time; quick tab switches can emit changes for the wrong file.

✅ Proposed fix
   private handleContentChange(): void {
     if (this.updateTimeout) {
       clearTimeout(this.updateTimeout)
     }
 
+    const path = this.activeFile
+    const content = this.getContent()
+
     this.updateTimeout = window.setTimeout(() => {
-      const content = this.getContent()
-      this.events.emit('file:change', this.activeFile, content)
+      this.events.emit('file:change', path, content)
     }, 300)
   }
🤖 Prompt for AI Agents
In `@packages/core/src/editor/MonacoEditor.ts` around lines 429 - 437,
handleContentChange currently reads this.activeFile when the timeout fires,
which can mis-attribute edits after quick file switches; capture the current
activeFile synchronously before scheduling the timeout (e.g., const
fileAtSchedule = this.activeFile) and use that captured variable inside the
setTimeout callback when calling this.events.emit('file:change', fileAtSchedule,
content), leaving the rest of the debounce logic (this.updateTimeout,
clearTimeout, this.getContent()) intact.

Comment on lines 20 to +27
export type { FileDiff } from './template/TemplateManager'
export type { WebContainerAuthConfig } from './webcontainer/WebContainerManager'

// Editor exports
export { EditorController } from './editor/EditorController'
export { CodeMirrorEditor } from './editor/CodeMirrorEditor'
export { createEditor, createEditorAsync, isMonacoAvailable } from './editor/editorFactory'
export type { EditorAdapter, EditorOptions, EditorType, EditorFactory } from './editor/EditorAdapter'
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 | 🟡 Minor

Resolve export ordering lint violations.
ESLint perfectionist/sort-exports and sort-named-exports flag this block; reordering should clear the errors.

🔧 Suggested reordering
-export type { FileDiff } from './template/TemplateManager'
-export type { WebContainerAuthConfig } from './webcontainer/WebContainerManager'
-
-// Editor exports
-export { EditorController } from './editor/EditorController'
-export { CodeMirrorEditor } from './editor/CodeMirrorEditor'
-export { createEditor, createEditorAsync, isMonacoAvailable } from './editor/editorFactory'
-export type { EditorAdapter, EditorOptions, EditorType, EditorFactory } from './editor/EditorAdapter'
+export type { FileDiff } from './template/TemplateManager'
+
+// Editor exports
+export { CodeMirrorEditor } from './editor/CodeMirrorEditor'
+export { EditorController } from './editor/EditorController'
+export type { EditorAdapter, EditorFactory, EditorOptions, EditorType } from './editor/EditorAdapter'
+export { createEditor, createEditorAsync, isMonacoAvailable } from './editor/editorFactory'
+
+export type { WebContainerAuthConfig } from './webcontainer/WebContainerManager'
🧰 Tools
🪛 ESLint

[error] 24-24: Expected "./editor/EditorController" to come before "./webcontainer/WebContainerManager".

(perfectionist/sort-exports)


[error] 25-25: Expected "./editor/CodeMirrorEditor" to come before "./editor/EditorController".

(perfectionist/sort-exports)


[error] 27-27: Expected "./editor/EditorAdapter" to come before "./editor/editorFactory".

(perfectionist/sort-exports)


[error] 27-27: Expected "EditorFactory" to come before "EditorType".

(perfectionist/sort-named-exports)

🤖 Prompt for AI Agents
In `@packages/core/src/index.ts` around lines 20 - 27, The export block in
packages/core/src/index.ts is failing perfectionist/sort-exports and
sort-named-exports; reorder the exports so named exports are alphabetized and
grouped consistently (types vs. values) to satisfy the linters: ensure type
exports like FileDiff and WebContainerAuthConfig are in sorted order and the
editor-related exports (EditorController, CodeMirrorEditor, createEditor,
createEditorAsync, isMonacoAvailable) and the type list (EditorAdapter,
EditorFactory, EditorOptions, EditorType) are each sorted alphabetically within
their groups; update the export order for those symbols accordingly.

Comment on lines +78 to +84
.editor-container .monaco-editor a {
border-bottom: none;
}

.editor-container .monaco-editor a:hover {
border-bottom: none;
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's confirm the file exists and read the relevant section
cat -n packages/react/src/styles/playground.css | sed -n '70,90p'

Repository: Oluwasetemi/playground

Length of output: 645


🏁 Script executed:

# Let's also check the full context around those lines
wc -l packages/react/src/styles/playground.css

Repository: Oluwasetemi/playground

Length of output: 110


🏁 Script executed:

# Search for other link-related styles in the playground.css file
rg -n "a\s*{|a:" packages/react/src/styles/playground.css -A 3

Repository: Oluwasetemi/playground

Length of output: 161


🏁 Script executed:

# Check if there are other CSS files that might define link styles
fd "\.css$" packages/react/src/styles/ | head -20

Repository: Oluwasetemi/playground

Length of output: 106


🏁 Script executed:

# Correct the regex and search for other anchor element styles
rg -n "a\s*\{" packages/react/src/styles/playground.css -A 3

Repository: Oluwasetemi/playground

Length of output: 256


🏁 Script executed:

# Also search for text-decoration patterns in the CSS file
rg -n "text-decoration" packages/react/src/styles/playground.css

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Check if there's any documentation or comments about link styling
rg -n "link\|hover\|underline" packages/react/src/styles/playground.css -i

Repository: Oluwasetemi/playground

Length of output: 48


🏁 Script executed:

# Look for any other CSS files that might have related styles
find packages/react/src/styles -name "*.css" -exec wc -l {} \;

Repository: Oluwasetemi/playground

Length of output: 110


🏁 Script executed:

# Check if there's a reset or normalize CSS section at the beginning
head -50 packages/react/src/styles/playground.css

Repository: Oluwasetemi/playground

Length of output: 1210


🏁 Script executed:

# Search across the entire packages/react for any other anchor/link styling
rg -n "border-bottom|text-decoration" packages/react --include="*.css" --include="*.scss"

Repository: Oluwasetemi/playground

Length of output: 507


🏁 Script executed:

# Check if there are any imports or references to other CSS files in playground.css
head -20 packages/react/src/styles/playground.css

Repository: Oluwasetemi/playground

Length of output: 522


🏁 Script executed:

# Look for any JavaScript that might be adding styles to links
rg -n "monaco.*editor.*a\|anchor.*style\|link.*style" packages/react/src --include="*.ts" --include="*.tsx" --include="*.js" -i

Repository: Oluwasetemi/playground

Length of output: 507


🏁 Script executed:

# Check if there are any other references to border-bottom in the project
rg "border-bottom" packages/react --include="*.css" --include="*.scss" --include="*.ts" --include="*.tsx"

Repository: Oluwasetemi/playground

Length of output: 507


🏁 Script executed:

# Find all CSS files in packages/react
fd "\.css$" packages/react

Repository: Oluwasetemi/playground

Length of output: 106


🏁 Script executed:

# Search for border-bottom across the entire packages/react directory
rg "border-bottom" packages/react

Repository: Oluwasetemi/playground

Length of output: 475


🏁 Script executed:

# Look for any global styles or resets that might affect links
rg "a\s*\{" packages/react -A 2

Repository: Oluwasetemi/playground

Length of output: 475


🏁 Script executed:

# Check if there's any Monaco Editor configuration that addresses link styling
rg "monaco" packages/react --type-list

Repository: Oluwasetemi/playground

Length of output: 455


🏁 Script executed:

# Check if there's any git history or comments explaining this decision
git log --oneline packages/react/src/styles/playground.css | head -10

Repository: Oluwasetemi/playground

Length of output: 97


🏁 Script executed:

# Look for any README or documentation about styling
fd "README" packages/react | head -5

Repository: Oluwasetemi/playground

Length of output: 48


Consider retaining a hover affordance for links in the Monaco editor.

Removing the underline/border makes links invisible and hard to discover. While a clean base style is reasonable, links need visual feedback. Consider replacing the border-bottom reset with a text-decoration-based approach to maintain hover affordance.

💡 Suggested approach
.editor-container .monaco-editor a {
-  border-bottom: none;
+  text-decoration: none;
}
.editor-container .monaco-editor a:hover {
-  border-bottom: none;
+  text-decoration: underline;
}
📝 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
.editor-container .monaco-editor a {
border-bottom: none;
}
.editor-container .monaco-editor a:hover {
border-bottom: none;
}
.editor-container .monaco-editor a {
text-decoration: none;
}
.editor-container .monaco-editor a:hover {
text-decoration: underline;
}
🤖 Prompt for AI Agents
In `@packages/react/src/styles/playground.css` around lines 78 - 84, The current
CSS rules for ".editor-container .monaco-editor a" and ".editor-container
.monaco-editor a:hover" remove any link affordance making links invisible;
instead, keep links styled plainly by removing underline on the base selector
but add a clear hover affordance on the hover selector (for example enable
text-decoration: underline or a subtle border-bottom/highlight using the theme
link color) so links remain discoverable in the Monaco editor; update those two
selectors accordingly to use text-decoration on hover rather than
unconditionally removing the visual cue.

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