Add Terminal component with ANSI parser support#1
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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: Handleconsole.clearby clearing state instead of appending.
PreviewServernow forwardsclear, but this handler always appends. The console UI will never clear whenconsole.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.tsutility in the React package, but this coreTerminalControllerdoesn't use it. The currentrender()method escapes HTML but strips ANSI sequences entirely.Consider either:
- Integrating ANSI parsing here for the core controller
- 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 formaxLineshandling.Both
addOutputandaddConsoleMessagecontain identical logic for pushing to the array and enforcingmaxLines. 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, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') }
There was a problem hiding this comment.
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 inapps/demo/src/react.ts.The import statement
import type { Template } from '../../packages/react/src/types'is incorrect—packages/react/src/types.tsdoes not exist. TheTemplatetype is defined inpackages/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: Handleconsole.clearinstead 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
mountis 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 afterifstatement 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 unusedhiddenFilesprop fromFileTreeNode.The
hiddenFilesprop is passed toFileTreeNodebut is never used within the component. The filtering is already performed byfilterHiddenFiles()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
newTempis not descriptive. Consider usingreactTemplatedirectly 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.
selectedTemplatestate is used only for the disabled dropdown andgetTitle(). If template selection is intentionally disabled, consider removing the unused state and simplifyinggetTitle()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'
| @@ -0,0 +1,161 @@ | |||
| import type { Template } from '../../packages/react/src/types' | |||
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -20Repository: 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 -30Repository: Oluwasetemi/playground
Length of output: 93
🏁 Script executed:
# Check tsconfig configuration for path aliases
find . -name "tsconfig*.json" -type f | head -10Repository: 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 -30Repository: Oluwasetemi/playground
Length of output: 1524
🏁 Script executed:
# Check the root tsconfig.json for path aliases
cat tsconfig.jsonRepository: Oluwasetemi/playground
Length of output: 706
🏁 Script executed:
# Check packages/react tsconfig
cat packages/react/tsconfig.jsonRepository: 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 -20Repository: 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'.
| 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')] | ||
| }, '*'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -nP -C3 '\baddEventListener\(\s*["\']message["\']' --type ts --type jsRepository: Oluwasetemi/playground
Length of output: 134
🏁 Script executed:
rg -nP -C3 '\bonmessage\b' --type ts --type jsRepository: 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 -20Repository: Oluwasetemi/playground
Length of output: 48
🏁 Script executed:
rg "addEventListener.*message" --type ts --type js -A 5 -B 2Repository: Oluwasetemi/playground
Length of output: 766
🏁 Script executed:
rg "onmessage" --type ts --type js -A 5 -B 2Repository: 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 -20Repository: 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 5Repository: 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.tsRepository: 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.tsRepository: 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.
| "@setemiojo/playground-core": "workspace:*", | ||
| "@xterm/addon-fit": "^0.11.0", | ||
| "@xterm/xterm": "^6.0.0" |
There was a problem hiding this comment.
🧩 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
@xtermpackages and recent publishes). [1] - xterm.js GitHub releases (5.x release history / migration to
@xtermscope). [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.
| // 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]) |
There was a problem hiding this comment.
🧩 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 -5Repository: Oluwasetemi/playground
Length of output: 110
🏁 Script executed:
fd "Terminal.tsx" --type fRepository: Oluwasetemi/playground
Length of output: 161
🏁 Script executed:
cat -n packages/react/src/components/Terminal.tsxRepository: 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 methodsRepository: 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.
There was a problem hiding this comment.
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: MissingterminalMessagesreset inresetCode.The
resetCodefunction clearsconsoleMessagesbut notterminalMessages. 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.HTMLand 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, resettinglastMessageIndexRefto 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 mountapps/demo/src/App.tsx (1)
29-45: Dead code:selectedTemplatestate andgetTitle()are unused.The
selectedTemplatestate (line 30) andgetTitle()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 betweenfiles['package.json']and top-leveldependencies.The dependencies are defined both in the stringified
package.jsoncontent (lines 18-27) and in the top-leveldependencies/devDependenciesfields (lines 145-154). Ensure these are intentionally kept in sync, or consider generating one from the other to avoid drift.
| .playground-console-message.info { | ||
| color: #75beff; | ||
| } |
There was a problem hiding this comment.
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.
| .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.
| @@ -0,0 +1,161 @@ | |||
| import type { Template } from '../../packages/react/src/types' | |||
There was a problem hiding this comment.
🧩 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 -20Repository: 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.
| 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.
| // 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')] | ||
| }, '*'); | ||
| }); |
There was a problem hiding this comment.
🧩 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 -100Repository: 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 2Repository: 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 tsxRepository: 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 -80Repository: 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 tsRepository: 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 -100Repository: 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 -150Repository: 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 -100Repository: 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 -150Repository: 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 tsRepository: 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.
| // Inject console forwarder into HTML files | ||
| const modifiedFiles = this.injectConsoleForwarder(files) | ||
|
|
||
| await this.webcontainer.mount(modifiedFiles) | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's explore the FileSystemManager.ts file structure
fd FileSystemManager.ts -t fRepository: 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 -70Repository: Oluwasetemi/playground
Length of output: 2920
🏁 Script executed:
# Search for injectConsoleForwarder implementation
rg "injectConsoleForwarder" -A 15 -B 2Repository: 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.tsRepository: 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 -10Repository: 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 -60Repository: 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 -10Repository: 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 1Repository: 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.tsRepository: Oluwasetemi/playground
Length of output: 677
🏁 Script executed:
# Check what CONSOLE_FORWARDER_SCRIPT contains
rg "CONSOLE_FORWARDER_SCRIPT" -B 5 -A 15Repository: 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 -100Repository: 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 3Repository: 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 -5Repository: 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 5Repository: 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 2Repository: 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/ -lRepository: 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 -80Repository: 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 -100Repository: 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.
There was a problem hiding this comment.
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-importsandstyle/semiin 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. TheworkerConstructoritself should simply be{ new (): Worker }; thedefaultproperty comes from the module system when using dynamicimport().♻️ 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 }
| 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"; | ||
| } | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| disabled | ||
| value={selectedTemplate} | ||
| onChange={e => setSelectedTemplate(e.target.value)} | ||
| onChange={(e) => setSelectedTemplate(e.target.value)} |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| 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.
| setLineNumbers(show) { | ||
| monacoEditor?.setLineNumbers(show) | ||
| }, | ||
| getLineNumbers() { | ||
| return monacoEditor?.getLineNumbers() || true | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "MonacoEditor.ts" --type fRepository: 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 -5Repository: Oluwasetemi/playground
Length of output: 461
🏁 Script executed:
cat -n eslint.config.mjs | head -60Repository: 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 -10Repository: 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.tsRepository: 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 -ARepository: 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 2Repository: 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.
| 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.
| 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) | ||
| } | ||
| }, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find ESLint configuration
find . -maxdepth 3 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) 2>/dev/null | head -20Repository: 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 -20Repository: 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.tsRepository: 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 -20Repository: 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 -100Repository: Oluwasetemi/playground
Length of output: 1216
🏁 Script executed:
# Check the ESLint configuration file
cat ./eslint.config.mjsRepository: 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.tsRepository: 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 fRepository: 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.tsRepository: 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 -20Repository: 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 -50Repository: 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 -5Repository: 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.tsRepository: 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.jsRepository: 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 -20Repository: 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 -300Repository: 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.jsRepository: 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 || trueRepository: 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.
| 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.
| 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) |
There was a problem hiding this comment.
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.
| 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' |
There was a problem hiding this comment.
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.
| .editor-container .monaco-editor a { | ||
| border-bottom: none; | ||
| } | ||
|
|
||
| .editor-container .monaco-editor a:hover { | ||
| border-bottom: none; | ||
| } |
There was a problem hiding this comment.
🧩 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.cssRepository: 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 3Repository: 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 -20Repository: 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 3Repository: 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.cssRepository: 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 -iRepository: 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.cssRepository: 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.cssRepository: 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" -iRepository: 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/reactRepository: Oluwasetemi/playground
Length of output: 106
🏁 Script executed:
# Search for border-bottom across the entire packages/react directory
rg "border-bottom" packages/reactRepository: 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 2Repository: 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-listRepository: 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 -10Repository: Oluwasetemi/playground
Length of output: 97
🏁 Script executed:
# Look for any README or documentation about styling
fd "README" packages/react | head -5Repository: 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.
| .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.
Adds Terminal component to Playground with ANSI escape sequence parsing capabilities.
Key changes:
Co-Authored-By: Warp agent@warp.dev
Summary by CodeRabbit
New Features
Bug Fixes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.