+ Drag parts from the components rail β or double-click one to drop it here.
+
+
+ )}
+
+
+
+ {/* footer */}
+
+
+ Parts: {diagram.parts.length} Β· Connections: {diagram.connections.length} Β· Nets:{' '}
+ {netCount}
+
+ {isAI && (
+
+ Auto-wired by Studio AI
+
+ )}
+ {!editable ? (
+
+ View-only Β· hover a wire to light its net Β· click edit to change
+
+ ) : armed ? (
+
+ Click a pin or wire to connect Β· click for a bend Β· hold Shift for
+ straight Β· Esc to cancel
+
+ ) : selWire != null ? (
+
+ Drag a handle to reshape Β· double-click to add/remove a bend Β· pick a color Β· Del to
+ remove
+
+ ) : (
+
+ Scroll to zoom Β· middle/Alt-drag to pan Β· click a pin to
+ wire
+
+ )}
+
+
+ {openFiles.map((file) => (
+
+ {editorMode === 'blocks' ? (
+
+ ) : (
+ handleContentChange(content, file.id)}
+ onSaveFile={(content) => handleSaveFile(content, file.id)}
+ />
+ )}
+
+ ))}
+
+ )
+}
+
+// ββ Circuit view: full-window interactive diagram.json ββββββββββββββββββββββ
+
+function CircuitView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+ const file = useProjectFile('diagram.json', () => DEFAULT_DIAGRAM)
+
+ if (!workspace) return
+ if (!file) return
+
+ return (
+ dispatch(updateFileContent({ id: file.id, content }))}
+ onOpenCode={() => {
+ dispatch(revealFile(file.id))
+ dispatch(setEditorView('code'))
+ }}
+ onEditChange={(editing) => {
+ // entering edit mode: free horizontal space by closing the docs panel
+ if (editing) dispatch(setPanelOpen({ panel: 'docs', isOpen: false }))
+ }}
+ />
+ )
+}
+
+// ββ Visual view: full-window p5 sketch with an Edit-code button βββββββββββββ
+
+function VisualView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+
+ // Which .js sketch is shown in the Visual view. Defaults to visual.js, the
+ // sketch auto-created for every project; other .js files are selectable too.
+ const [activeSketch, setActiveSketch] = useState('visual.js')
+ const [showNew, setShowNew] = useState(false)
+ // Serial is owned app-wide by SerialProvider and feeds the running sketch via
+ // the shared bus β the Visual view no longer opens the port itself, so
+ // switching to/from this view doesn't reset the board.
+ const [publishing, setPublishing] = useState(false)
+
+ const jsFiles = useMemo(
+ () => (workspace ? collectFiles(workspace.root, (i) => /\.js$/i.test(i.name!)) : []),
+ [workspace]
+ )
+
+ // If the active sketch isn't in this project (after a project switch or a
+ // delete), fall back to visual.js or the first sketch available.
+ useEffect(() => {
+ if (jsFiles.length && !jsFiles.some((f) => f.name === activeSketch)) {
+ setActiveSketch(jsFiles.find((f) => f.name === 'visual.js')?.name ?? jsFiles[0].name!)
+ }
+ }, [jsFiles, activeSketch])
+
+ // Only visual.js gets seeded from a default; other sketches must already exist.
+ const file = useProjectFile(
+ activeSketch,
+ activeSketch === 'visual.js' ? () => DEFAULT_VISUAL : undefined
+ )
+
+ if (!workspace) return
+
+ const ws = workspace
+
+ const createSketch = async (fileName: string): Promise => {
+ const path = `${ws.path}/${fileName}`
+ try {
+ await fileSystem.writeFile(path, sketchTemplate(fileName.replace(/\.js$/i, '')))
+ await new RefreshWorkspaceCommand(ws).execute()
+ setActiveSketch(fileName)
+ setShowNew(false)
+ } catch (e) {
+ toast.error('Could not create sketch', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Build the standalone page, titled after the .ino project (not the folder).
+ const buildHtml = (): string => {
+ const ino = findInTree(ws.root, (i) => /\.ino$/i.test(i.name!))
+ const projectName = ino?.name?.replace(/\.ino$/i, '') || ws.name || 'tinyStudio sketch'
+ return buildVisualExportHtml(projectName, file?.content ?? '')
+ }
+
+ // Preview: write index.html into the project root and open it in the browser.
+ const preview = async (): Promise => {
+ const path = `${ws.path}/index.html`
+ try {
+ await window.api.fs.writeFile(path, buildHtml())
+ await new RefreshWorkspaceCommand(ws).execute()
+ const err = await window.api.fs.openPath(path)
+ if (err) toast.error('Could not open preview', { description: err })
+ else toast.success('Preview opened in your browser', { description: path })
+ } catch (e) {
+ toast.error('Preview failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Publish: push index.html to the linked repo and turn on GitHub Pages.
+ const publish = async (): Promise => {
+ const account = loadAccount()
+ if (!account) {
+ toast.info('Connect GitHub first', {
+ description: 'Open the GitHub tab in the sidebar to sign in.'
+ })
+ return
+ }
+ const link = loadLink(ws.path)
+ if (!link) {
+ toast.info('Link this project to a repo first', {
+ description: 'Use the GitHub tab to link or publish a repository.'
+ })
+ return
+ }
+ setPublishing(true)
+ try {
+ const html = buildHtml()
+ await window.api.fs.writeFile(`${ws.path}/index.html`, html)
+ await new RefreshWorkspaceCommand(ws).execute()
+ await pushFile(
+ link.remote,
+ link.branch,
+ 'index.html',
+ html,
+ account.token,
+ 'Publish web export via tinyStudio'
+ )
+ const url = await enablePages(link.remote, link.branch, account.token)
+ toast.success('Published to GitHub Pages', {
+ description: `${url} β the first build can take a minute.`
+ })
+ window.api.fs.openExternal(url)
+ } catch (e) {
+ toast.error('Publish failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ } finally {
+ setPublishing(false)
+ }
+ }
+
+ return (
+
+ {/* Sketch picker + view actions. Tabs let you keep several p5.js renders
+ (asteroids.js, pong.js, β¦) in one project and switch between them. */}
+
+ )
+}
+
+// ββ Code view: the normal tabbed IDE ββββββββββββββββββββββββββββββββββββββββ
+
+function CodeView(): React.JSX.Element {
+ const allOpenFiles = useAppSelector(selectOpenFiles)
+ // Background buffers (diagram.json / visual.js loaded for the Circuit/Visual
+ // views) are hidden from the tab bar until revealed via their code button.
+ const openFiles = allOpenFiles.filter((f) => !f.hidden)
+ const viewingFileId = useAppSelector(selectViewingFileId)
+ const editorMode = useAppSelector((state) => state.editor.editorMode)
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+ const monacoEditorRef = useRef(null)
+
+ const handleFileClose = useCallback(
+ (fileId: string): void => {
+ dispatch(closeFile(fileId))
+ },
+ [dispatch]
+ )
+
+ const handleFileSelect = useCallback(
+ (fileId: string): void => {
+ dispatch(setViewingFile(fileId))
+ setTimeout(() => {
+ if (monacoEditorRef.current && editorMode !== 'blocks') monacoEditorRef.current.focus()
+ }, 50)
+ },
+ [dispatch, editorMode]
+ )
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent): void => {
+ if ((event.metaKey || event.ctrlKey) && event.key === 'w') {
+ event.preventDefault()
+ if (viewingFileId) handleFileClose(viewingFileId)
+ }
+ if (event.ctrlKey && event.key === 'Tab') {
+ event.preventDefault()
+ if (openFiles.length > 1) {
+ const i = openFiles.findIndex((f) => f.id === viewingFileId)
+ const next = event.shiftKey
+ ? i <= 0
+ ? openFiles.length - 1
+ : i - 1
+ : (i + 1) % openFiles.length
+ if (openFiles[next]) handleFileSelect(openFiles[next].id)
+ }
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [viewingFileId, handleFileClose, openFiles, handleFileSelect])
+
+ const handleContentChange = useCallback(
+ (content: string, fileId: string) => {
+ const file = openFiles.find((f) => f.id === fileId)
+ if (file) {
+ dispatch(updateFileContent({ id: file.id, content }))
+ if (file.name === 'README.md') dispatch(updateReadmeContent(content))
+ }
+ },
+ [openFiles, dispatch]
+ )
+
+ const handleSaveFile = async (content: string, fileId: string): Promise => {
+ const file = openFiles.find((f) => f.id === fileId)
+ if (file && file.path) {
+ try {
+ await fileSystem.writeFile(file.path, content)
+ dispatch(saveFileWithContent({ id: file.id, content }))
+ } catch (error) {
+ console.error('Failed to save file:', error)
+ }
+ }
+ }
+
+ if (openFiles.length === 0) {
+ // No file open β land on the Examples browser so there is something to do
+ // right away (replaces the old logo / first-run welcome dialog).
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ {openFiles.map((file) => (
+
+ ))}
+
+
+ {openFiles.map((file) => (
+
+ {editorMode === 'blocks' ? (
+
+ ) : (
+ handleContentChange(content, file.id)}
+ onSaveFile={(content) => handleSaveFile(content, file.id)}
+ />
+ )}
+
+ ))}
+
+ )
+}
+
+// ββ Circuit view: full-window interactive diagram.json ββββββββββββββββββββββ
+
+function CircuitView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+ const file = useProjectFile('diagram.json', () => DEFAULT_DIAGRAM)
+
+ if (!workspace) return
+ if (!file) return
+
+ return (
+ dispatch(updateFileContent({ id: file.id, content }))}
+ onOpenCode={() => {
+ dispatch(revealFile(file.id))
+ dispatch(setEditorView('code'))
+ }}
+ onEditChange={(editing) => {
+ // entering edit mode: free horizontal space by closing the docs panel
+ if (editing) dispatch(setPanelOpen({ panel: 'docs', isOpen: false }))
+ }}
+ />
+ )
+}
+
+// ββ Visual view: full-window p5 sketch with an Edit-code button βββββββββββββ
+
+function VisualView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+
+ // Which .js sketch is shown in the Visual view. Defaults to visual.js, the
+ // sketch auto-created for every project; other .js files are selectable too.
+ const [activeSketch, setActiveSketch] = useState('visual.js')
+ const [showNew, setShowNew] = useState(false)
+ // Serial is owned app-wide by SerialProvider and feeds the running sketch via
+ // the shared bus β the Visual view no longer opens the port itself, so
+ // switching to/from this view doesn't reset the board.
+ const [publishing, setPublishing] = useState(false)
+
+ const jsFiles = useMemo(
+ () => (workspace ? collectFiles(workspace.root, (i) => /\.js$/i.test(i.name!)) : []),
+ [workspace]
+ )
+
+ // If the active sketch isn't in this project (after a project switch or a
+ // delete), fall back to visual.js or the first sketch available.
+ useEffect(() => {
+ if (jsFiles.length && !jsFiles.some((f) => f.name === activeSketch)) {
+ setActiveSketch(jsFiles.find((f) => f.name === 'visual.js')?.name ?? jsFiles[0].name!)
+ }
+ }, [jsFiles, activeSketch])
+
+ // Only visual.js gets seeded from a default; other sketches must already exist.
+ const file = useProjectFile(
+ activeSketch,
+ activeSketch === 'visual.js' ? () => DEFAULT_VISUAL : undefined
+ )
+
+ if (!workspace) return
+
+ const ws = workspace
+
+ const createSketch = async (fileName: string): Promise => {
+ const path = `${ws.path}/${fileName}`
+ try {
+ await fileSystem.writeFile(path, sketchTemplate(fileName.replace(/\.js$/i, '')))
+ await new RefreshWorkspaceCommand(ws).execute()
+ setActiveSketch(fileName)
+ setShowNew(false)
+ } catch (e) {
+ toast.error('Could not create sketch', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Build the standalone page, titled after the .ino project (not the folder).
+ const buildHtml = (): string => {
+ const ino = findInTree(ws.root, (i) => /\.ino$/i.test(i.name!))
+ const projectName = ino?.name?.replace(/\.ino$/i, '') || ws.name || 'tinyStudio sketch'
+ return buildVisualExportHtml(projectName, file?.content ?? '')
+ }
+
+ // Preview: write index.html into the project root and open it in the browser.
+ const preview = async (): Promise => {
+ const path = `${ws.path}/index.html`
+ try {
+ await window.api.fs.writeFile(path, buildHtml())
+ await new RefreshWorkspaceCommand(ws).execute()
+ const err = await window.api.fs.openPath(path)
+ if (err) toast.error('Could not open preview', { description: err })
+ else toast.success('Preview opened in your browser', { description: path })
+ } catch (e) {
+ toast.error('Preview failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Publish: push index.html to the linked repo and turn on GitHub Pages.
+ const publish = async (): Promise => {
+ const account = loadAccount()
+ if (!account) {
+ toast.info('Connect GitHub first', {
+ description: 'Open the GitHub tab in the sidebar to sign in.'
+ })
+ return
+ }
+ const link = loadLink(ws.path)
+ if (!link) {
+ toast.info('Link this project to a repo first', {
+ description: 'Use the GitHub tab to link or publish a repository.'
+ })
+ return
+ }
+ setPublishing(true)
+ try {
+ const html = buildHtml()
+ await window.api.fs.writeFile(`${ws.path}/index.html`, html)
+ await new RefreshWorkspaceCommand(ws).execute()
+ await pushFile(
+ link.remote,
+ link.branch,
+ 'index.html',
+ html,
+ account.token,
+ 'Publish web export via tinyStudio'
+ )
+ const url = await enablePages(link.remote, link.branch, account.token)
+ toast.success('Published to GitHub Pages', {
+ description: `${url} β the first build can take a minute.`
+ })
+ window.api.fs.openExternal(url)
+ } catch (e) {
+ toast.error('Publish failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ } finally {
+ setPublishing(false)
+ }
+ }
+
+ return (
+
+ {/* Sketch picker + view actions. Tabs let you keep several p5.js renders
+ (asteroids.js, pong.js, β¦) in one project and switch between them. */}
+
+ Drag parts from the components rail β or double-click one to drop it here.
+
+
+ )}
+
+
+
+ {/* footer */}
+
+
+ Parts: {diagram.parts.length} Β· Connections: {diagram.connections.length} Β· Nets:{' '}
+ {netCount}
+
+ {isAI && (
+
+ Auto-wired by Studio AI
+
+ )}
+ {!editable ? (
+
+ View-only Β· hover a wire to light its net Β· click edit to change
+
+ ) : armed ? (
+
+ Click a pin or wire to connect Β· click for a bend Β· hold Shift for
+ straight Β· Esc to cancel
+
+ ) : selWire != null ? (
+
+ Drag a handle to reshape Β· double-click to add/remove a bend Β· pick a color Β· Del to
+ remove
+
+ ) : (
+
+ Scroll to zoom Β· middle/Alt-drag to pan Β· click a pin to
+ wire
+
+ )}
+
+
+ {openFiles.map((file) => (
+
+ {editorMode === 'blocks' ? (
+
+ ) : (
+ handleContentChange(content, file.id)}
+ onSaveFile={(content) => handleSaveFile(content, file.id)}
+ />
+ )}
+
+ ))}
+
+ )
+}
+
+// ββ Circuit view: full-window interactive diagram.json ββββββββββββββββββββββ
+
+function CircuitView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+ const file = useProjectFile('diagram.json', () => DEFAULT_DIAGRAM)
+
+ if (!workspace) return
+ if (!file) return
+
+ return (
+ dispatch(updateFileContent({ id: file.id, content }))}
+ onOpenCode={() => {
+ dispatch(revealFile(file.id))
+ dispatch(setEditorView('code'))
+ }}
+ onEditChange={(editing) => {
+ // entering edit mode: free horizontal space by closing the docs panel
+ if (editing) dispatch(setPanelOpen({ panel: 'docs', isOpen: false }))
+ }}
+ />
+ )
+}
+
+// ββ Visual view: full-window p5 sketch with an Edit-code button βββββββββββββ
+
+function VisualView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+
+ // Which .js sketch is shown in the Visual view. Defaults to visual.js, the
+ // sketch auto-created for every project; other .js files are selectable too.
+ const [activeSketch, setActiveSketch] = useState('visual.js')
+ const [showNew, setShowNew] = useState(false)
+ // Serial is owned app-wide by SerialProvider and feeds the running sketch via
+ // the shared bus β the Visual view no longer opens the port itself, so
+ // switching to/from this view doesn't reset the board.
+ const [publishing, setPublishing] = useState(false)
+
+ const jsFiles = useMemo(
+ () => (workspace ? collectFiles(workspace.root, (i) => /\.js$/i.test(i.name!)) : []),
+ [workspace]
+ )
+
+ // If the active sketch isn't in this project (after a project switch or a
+ // delete), fall back to visual.js or the first sketch available.
+ useEffect(() => {
+ if (jsFiles.length && !jsFiles.some((f) => f.name === activeSketch)) {
+ setActiveSketch(jsFiles.find((f) => f.name === 'visual.js')?.name ?? jsFiles[0].name!)
+ }
+ }, [jsFiles, activeSketch])
+
+ // Only visual.js gets seeded from a default; other sketches must already exist.
+ const file = useProjectFile(
+ activeSketch,
+ activeSketch === 'visual.js' ? () => DEFAULT_VISUAL : undefined
+ )
+
+ if (!workspace) return
+
+ const ws = workspace
+
+ const createSketch = async (fileName: string): Promise => {
+ const path = `${ws.path}/${fileName}`
+ try {
+ await fileSystem.writeFile(path, sketchTemplate(fileName.replace(/\.js$/i, '')))
+ await new RefreshWorkspaceCommand(ws).execute()
+ setActiveSketch(fileName)
+ setShowNew(false)
+ } catch (e) {
+ toast.error('Could not create sketch', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Build the standalone page, titled after the .ino project (not the folder).
+ const buildHtml = (): string => {
+ const ino = findInTree(ws.root, (i) => /\.ino$/i.test(i.name!))
+ const projectName = ino?.name?.replace(/\.ino$/i, '') || ws.name || 'tinyStudio sketch'
+ return buildVisualExportHtml(projectName, file?.content ?? '')
+ }
+
+ // Preview: write index.html into the project root and open it in the browser.
+ const preview = async (): Promise => {
+ const path = `${ws.path}/index.html`
+ try {
+ await window.api.fs.writeFile(path, buildHtml())
+ await new RefreshWorkspaceCommand(ws).execute()
+ const err = await window.api.fs.openPath(path)
+ if (err) toast.error('Could not open preview', { description: err })
+ else toast.success('Preview opened in your browser', { description: path })
+ } catch (e) {
+ toast.error('Preview failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Publish: push index.html to the linked repo and turn on GitHub Pages.
+ const publish = async (): Promise => {
+ const account = loadAccount()
+ if (!account) {
+ toast.info('Connect GitHub first', {
+ description: 'Open the GitHub tab in the sidebar to sign in.'
+ })
+ return
+ }
+ const link = loadLink(ws.path)
+ if (!link) {
+ toast.info('Link this project to a repo first', {
+ description: 'Use the GitHub tab to link or publish a repository.'
+ })
+ return
+ }
+ setPublishing(true)
+ try {
+ const html = buildHtml()
+ await window.api.fs.writeFile(`${ws.path}/index.html`, html)
+ await new RefreshWorkspaceCommand(ws).execute()
+ await pushFile(
+ link.remote,
+ link.branch,
+ 'index.html',
+ html,
+ account.token,
+ 'Publish web export via tinyStudio'
+ )
+ const url = await enablePages(link.remote, link.branch, account.token)
+ toast.success('Published to GitHub Pages', {
+ description: `${url} β the first build can take a minute.`
+ })
+ window.api.fs.openExternal(url)
+ } catch (e) {
+ toast.error('Publish failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ } finally {
+ setPublishing(false)
+ }
+ }
+
+ return (
+
+ {/* Sketch picker + view actions. Tabs let you keep several p5.js renders
+ (asteroids.js, pong.js, β¦) in one project and switch between them. */}
+
+ Drag parts from the components rail β or double-click one to drop it here.
+
+
+ )}
+
+
+
+ {/* footer */}
+
+
+ Parts: {diagram.parts.length} Β· Connections: {diagram.connections.length} Β· Nets:{' '}
+ {netCount}
+
+ {isAI && (
+
+ Auto-wired by Studio AI
+
+ )}
+ {!editable ? (
+
+ View-only Β· hover a wire to light its net Β· click edit to change
+
+ ) : armed ? (
+
+ Click a pin or wire to connect Β· click for a bend Β· hold Shift for
+ straight Β· Esc to cancel
+
+ ) : selWire != null ? (
+
+ Drag a handle to reshape Β· double-click to add/remove a bend Β· pick a color Β· Del to
+ remove
+
+ ) : (
+
+ Scroll to zoom Β· middle/Alt-drag to pan Β· click a pin to
+ wire
+
+ )}
+
+
+ {openFiles.map((file) => (
+
+ {editorMode === 'blocks' ? (
+
+ ) : (
+ handleContentChange(content, file.id)}
+ onSaveFile={(content) => handleSaveFile(content, file.id)}
+ />
+ )}
+
+ ))}
+
+ )
+}
+
+// ββ Circuit view: full-window interactive diagram.json ββββββββββββββββββββββ
+
+function CircuitView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+ const file = useProjectFile('diagram.json', () => DEFAULT_DIAGRAM)
+
+ if (!workspace) return
+ if (!file) return
+
+ return (
+ dispatch(updateFileContent({ id: file.id, content }))}
+ onOpenCode={() => {
+ dispatch(revealFile(file.id))
+ dispatch(setEditorView('code'))
+ }}
+ onEditChange={(editing) => {
+ // entering edit mode: free horizontal space by closing the docs panel
+ if (editing) dispatch(setPanelOpen({ panel: 'docs', isOpen: false }))
+ }}
+ />
+ )
+}
+
+// ββ Visual view: full-window p5 sketch with an Edit-code button βββββββββββββ
+
+function VisualView(): React.JSX.Element {
+ const workspace = useAppSelector((s) => s.file.workspace)
+ const dispatch = useAppDispatch()
+
+ // Which .js sketch is shown in the Visual view. Defaults to visual.js, the
+ // sketch auto-created for every project; other .js files are selectable too.
+ const [activeSketch, setActiveSketch] = useState('visual.js')
+ const [showNew, setShowNew] = useState(false)
+ // Serial is owned app-wide by SerialProvider and feeds the running sketch via
+ // the shared bus β the Visual view no longer opens the port itself, so
+ // switching to/from this view doesn't reset the board.
+ const [publishing, setPublishing] = useState(false)
+
+ const jsFiles = useMemo(
+ () => (workspace ? collectFiles(workspace.root, (i) => /\.js$/i.test(i.name!)) : []),
+ [workspace]
+ )
+
+ // If the active sketch isn't in this project (after a project switch or a
+ // delete), fall back to visual.js or the first sketch available.
+ useEffect(() => {
+ if (jsFiles.length && !jsFiles.some((f) => f.name === activeSketch)) {
+ setActiveSketch(jsFiles.find((f) => f.name === 'visual.js')?.name ?? jsFiles[0].name!)
+ }
+ }, [jsFiles, activeSketch])
+
+ // Only visual.js gets seeded from a default; other sketches must already exist.
+ const file = useProjectFile(
+ activeSketch,
+ activeSketch === 'visual.js' ? () => DEFAULT_VISUAL : undefined
+ )
+
+ if (!workspace) return
+
+ const ws = workspace
+
+ const createSketch = async (fileName: string): Promise => {
+ const path = `${ws.path}/${fileName}`
+ try {
+ await fileSystem.writeFile(path, sketchTemplate(fileName.replace(/\.js$/i, '')))
+ await new RefreshWorkspaceCommand(ws).execute()
+ setActiveSketch(fileName)
+ setShowNew(false)
+ } catch (e) {
+ toast.error('Could not create sketch', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Build the standalone page, titled after the .ino project (not the folder).
+ const buildHtml = (): string => {
+ const ino = findInTree(ws.root, (i) => /\.ino$/i.test(i.name!))
+ const projectName = ino?.name?.replace(/\.ino$/i, '') || ws.name || 'tinyStudio sketch'
+ return buildVisualExportHtml(projectName, file?.content ?? '')
+ }
+
+ // Preview: write index.html into the project root and open it in the browser.
+ const preview = async (): Promise => {
+ const path = `${ws.path}/index.html`
+ try {
+ await window.api.fs.writeFile(path, buildHtml())
+ await new RefreshWorkspaceCommand(ws).execute()
+ const err = await window.api.fs.openPath(path)
+ if (err) toast.error('Could not open preview', { description: err })
+ else toast.success('Preview opened in your browser', { description: path })
+ } catch (e) {
+ toast.error('Preview failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ }
+ }
+
+ // Publish: push index.html to the linked repo and turn on GitHub Pages.
+ const publish = async (): Promise => {
+ const account = loadAccount()
+ if (!account) {
+ toast.info('Connect GitHub first', {
+ description: 'Open the GitHub tab in the sidebar to sign in.'
+ })
+ return
+ }
+ const link = loadLink(ws.path)
+ if (!link) {
+ toast.info('Link this project to a repo first', {
+ description: 'Use the GitHub tab to link or publish a repository.'
+ })
+ return
+ }
+ setPublishing(true)
+ try {
+ const html = buildHtml()
+ await window.api.fs.writeFile(`${ws.path}/index.html`, html)
+ await new RefreshWorkspaceCommand(ws).execute()
+ await pushFile(
+ link.remote,
+ link.branch,
+ 'index.html',
+ html,
+ account.token,
+ 'Publish web export via tinyStudio'
+ )
+ const url = await enablePages(link.remote, link.branch, account.token)
+ toast.success('Published to GitHub Pages', {
+ description: `${url} β the first build can take a minute.`
+ })
+ window.api.fs.openExternal(url)
+ } catch (e) {
+ toast.error('Publish failed', {
+ description: e instanceof Error ? e.message : 'Unknown error'
+ })
+ } finally {
+ setPublishing(false)
+ }
+ }
+
+ return (
+
+ {/* Sketch picker + view actions. Tabs let you keep several p5.js renders
+ (asteroids.js, pong.js, β¦) in one project and switch between them. */}
+
+ )
+}
diff --git a/src/renderer/src/components/AIAssistant.tsx b/src/renderer/src/components/AIAssistant.tsx
index 81f55e8..463f314 100644
--- a/src/renderer/src/components/AIAssistant.tsx
+++ b/src/renderer/src/components/AIAssistant.tsx
@@ -366,7 +366,7 @@ function PermissionDialog({
Studio AI wants to change a file in your workspace.
-