fix: keep raw-app editor selection consistent across sidebar and tabs (#10885)

* fix: route raw-app editor selection through one switch function

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: stop announcing folders as selected from the file tree

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: carry the selection through a folder rename

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: keep the generated wmill.ts tab out of stale-tab cleanup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* refactor: test document existence through one predicate

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* refactor: route the history replay through the same predicate

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: clear the selection in the same tick a runnable is deleted

Deleting the selected runnable dropped it from `runnables` and left the
editor to notice via the stale-tab effect, one frame later. In that window
the pane rendered "No runnable at id <key>".

The sidebar list now reports the delete instead of mutating `runnables`
itself; the editor deletes and closes the tab together, so the selection
moves through `select` synchronously. The stale-tab effect stays as the
backstop for deletes that come from elsewhere.

Also retitle the two sidebar create buttons and rename the FileExplorer
exports behind them: both have always anchored on the selected file's
parent folder, never the root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* refactor: require the runnable delete callback

Optional, the row's Delete button renders and does nothing. There is one
caller and it always supplies it, so the compiler can hold that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-08-31 14:13:32 +00:00
committed by GitHub
co-authored by Claude Opus 5
parent 66123f3a9b
commit b57e231c2b
6 changed files with 204 additions and 162 deletions
+29 -44
View File
@@ -8,9 +8,11 @@
interface Props {
/** File path → content map. Keys use / prefix (e.g. /index.html). */
files: Record<string, string>
/** Currently selected path (/-prefixed). Read-only; changes via onSelectPath callback. */
/** Currently selected file (/-prefixed). Read-only; changes via onSelectPath callback. */
selectedPath?: string | undefined
/** Called when user clicks a path (file or folder). */
/** Called when the user clicks a file. Folders aren't selectable — clicking
* one only expands it — so the only non-file paths this reports are the root
* row under `showRoot`, and '' when the last file is deleted. */
onSelectPath?: (path: string) => void
/** Extra tree nodes appended after the main tree (e.g. read-only wmill.ts). */
extraNodes?: TreeNode[]
@@ -80,6 +82,12 @@
onSelectPath?.(path)
}
function parentFolderOfSelection(): string {
if (!selectedPath || selectedPath === '/') return '/'
const pathParts = selectedPath.split('/').filter(Boolean)
return pathParts.length > 1 ? '/' + pathParts.slice(0, -1).join('/') + '/' : '/'
}
function handleAddFile(folderPath: string) {
const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/'
const basePath = normalizedFolder + 'newfile.txt'
@@ -88,20 +96,10 @@
pathToEdit = newPath
}
export function handleAddRootFile() {
let basePath: string
if (selectedPath && selectedPath !== '/') {
if (selectedPath.endsWith('/')) {
basePath = selectedPath + 'newfile.txt'
} else {
const pathParts = selectedPath.split('/').filter(Boolean)
const parentPath =
pathParts.length > 1 ? '/' + pathParts.slice(0, -1).join('/') + '/' : '/'
basePath = parentPath + 'newfile.txt'
}
} else {
basePath = '/newfile.txt'
}
// New entries land beside the selected file; with nothing selected, at the
// root. To create inside another folder, use that folder row's own menu.
export function handleAddFileBesideSelection() {
const basePath = parentFolderOfSelection() + 'newfile.txt'
const newPath = getUniquePath(basePath)
pendingNewFilePath = newPath
pathToEdit = newPath
@@ -115,20 +113,8 @@
pathToEdit = newPath
}
export function handleAddRootFolder() {
let basePath: string
if (selectedPath && selectedPath !== '/') {
if (selectedPath.endsWith('/')) {
basePath = selectedPath + 'newfolder/'
} else {
const pathParts = selectedPath.split('/').filter(Boolean)
const parentPath =
pathParts.length > 1 ? '/' + pathParts.slice(0, -1).join('/') + '/' : '/'
basePath = parentPath + 'newfolder/'
}
} else {
basePath = '/newfolder/'
}
export function handleAddFolderBesideSelection() {
const basePath = parentFolderOfSelection() + 'newfolder/'
const newPath = getUniquePath(basePath)
pendingNewFilePath = newPath
pathToEdit = newPath
@@ -168,9 +154,7 @@
}
// Also rename in emptyFolders
emptyFolders = emptyFolders.map((f) =>
f === oldPath || f.startsWith(oldPath)
? newPath + f.substring(oldPath.length)
: f
f === oldPath || f.startsWith(oldPath) ? newPath + f.substring(oldPath.length) : f
)
}
} else {
@@ -187,7 +171,13 @@
files = nfiles
pathToEdit = undefined
onSelectPath?.(newPath)
if (!isFolder) {
onSelectPath?.(newPath)
} else if (selectedPath?.startsWith(oldPath)) {
// A folder isn't selectable, but the selected file moved with it — follow
// it to its new path, or the caller keeps editing a key that's now gone.
onSelectPath?.(newPath + selectedPath.slice(oldPath.length))
}
}
function handleDelete(path: string) {
@@ -208,12 +198,8 @@
files = nfiles
if (selectedPath === path || (isFolder && selectedPath?.startsWith(path))) {
const remaining = Object.keys(nfiles)
if (remaining.length > 0) {
onSelectPath?.(remaining[0])
} else {
onSelectPath?.(showRoot ? '/' : '')
}
const remainingFile = Object.keys(nfiles).find((key) => !key.endsWith('/'))
onSelectPath?.(remainingFile ?? (showRoot ? '/' : ''))
}
}
</script>
@@ -223,7 +209,7 @@
<span class="text-xs font-semibold text-emphasis">Files</span>
<div class="flex gap-1">
<Button
onClick={handleAddRootFile}
onClick={handleAddFileBesideSelection}
title="Add file"
unifiedSize="xs"
variant="subtle"
@@ -233,7 +219,7 @@
<File size={12} />
</Button>
<Button
onClick={handleAddRootFolder}
onClick={handleAddFolderBesideSelection}
title="Add folder"
unifiedSize="xs"
variant="subtle"
@@ -255,8 +241,7 @@
: ''}"
>
<FolderOpen size={12} class="flex-shrink-0 text-secondary" />
<span
class="truncate text-primary font-normal {selectedPath === '/' ? 'text-accent' : ''}"
<span class="truncate text-primary font-normal {selectedPath === '/' ? 'text-accent' : ''}"
>/</span
>
</button>
@@ -1,5 +1,15 @@
<script lang="ts">
import { ChevronRight, ChevronDown, Folder, Pencil, Trash2, Lock, Ellipsis } from 'lucide-svelte'
import {
ChevronRight,
ChevronDown,
Folder,
FilePlus,
FolderPlus,
Pencil,
Trash2,
Lock,
Ellipsis
} from 'lucide-svelte'
import Self from './FileTreeNode.svelte'
import { twMerge } from 'tailwind-merge'
import DropdownV2 from '../DropdownV2.svelte'
@@ -69,13 +79,13 @@
}
function handleClick() {
// Always notify about selection
onFileClick?.(node.path)
// Toggle expansion for folders
// A folder can't be opened in an editor, so clicking one only expands it —
// selecting it would highlight a row nothing on screen corresponds to.
if (node.isFolder) {
toggleExpanded()
return
}
onFileClick?.(node.path)
}
function handleEdit(e: MouseEvent) {
@@ -211,6 +221,20 @@
{#if !noEdit}
<DropdownV2
items={[
...(node.isFolder
? [
{
displayName: 'New file',
icon: FilePlus,
action: () => onAddFile?.(node.path)
},
{
displayName: 'New folder',
icon: FolderPlus,
action: () => onAddFolder?.(node.path)
}
]
: []),
{
displayName: 'Rename',
icon: Pencil,
@@ -18,6 +18,7 @@
import { setRawAppOperatingWorkspace } from './rawAppWorkspace'
import { useLocalStorageValue } from '$lib/svelte5Utils.svelte'
import {
WMILL_TS_PATH,
genWmillTs,
normalizeRawAppRuntimeLogs,
type Runnable,
@@ -518,27 +519,66 @@
)
}
// What the editor area shows. Every selection is something it can display, so
// each one has a tab.
type EditorSelection =
| { kind: 'file'; path: string }
| { kind: 'runnable'; key: string }
| { kind: 'preview' }
function selectionOfTab(id: string): EditorSelection {
if (id.startsWith(FILE_PREFIX)) return { kind: 'file', path: id.slice(FILE_PREFIX.length) }
if (id.startsWith(RUNNABLE_PREFIX))
return { kind: 'runnable', key: id.slice(RUNNABLE_PREFIX.length) }
return { kind: 'preview' }
}
// The only writer of `activeTabId`, `selectedRunnable` and `selectedDocument`.
// Everything switches through here, so the three can't disagree: one left
// stale marks two sidebar rows selected, or leaves a tab over an empty pane.
function select(next: EditorSelection, opts?: { force?: boolean; notifyIframe?: boolean }): void {
if (next.kind === 'preview') {
// In split mode Preview is always shown on the right, so selecting it is
// a no-op (would collapse the left pane). `force` lets closeTab fall back
// to it.
if (!opts?.force && splitWithPreview) return
activeTabId = PREVIEW_TAB_ID
selectedRunnable = undefined
selectedDocument = undefined
return
}
if (next.kind === 'file') {
activeTabId = ensureFileTab(next.path)
selectedRunnable = undefined
selectedDocument = next.path
if (opts?.notifyIframe !== false) openInIframe(next.path)
return
}
activeTabId = ensureRunnableTab(next.key)
selectedDocument = undefined
selectedRunnable = next.key
}
function activateTab(id: string, opts?: { force?: boolean }) {
const tab = tabs.find((t) => t.id === id)
if (!tab) return
// In split mode Preview is always shown on the right, so clicking it is a
// no-op (would collapse the left pane). `force` lets closeTab fall back to it.
if (!opts?.force && splitWithPreview && id === PREVIEW_TAB_ID) return
activeTabId = id
if (tab.id === PREVIEW_TAB_ID) {
selectedRunnable = undefined
} else if (tab.id.startsWith(FILE_PREFIX)) {
const filePath = tab.id.slice(FILE_PREFIX.length)
selectedRunnable = undefined
// `populateFiles` reads this on iframe load, so set it even if the
// iframe isn't ready yet (the postMessage below is then skipped).
selectedDocument = filePath
if (iframeLoaded) {
iframe?.contentWindow?.postMessage({ type: 'selectFile', path: filePath }, '*')
}
} else if (tab.id.startsWith(RUNNABLE_PREFIX)) {
const key = tab.id.slice(RUNNABLE_PREFIX.length)
if (selectedRunnable !== key) selectedRunnable = key
if (!tabs.some((t) => t.id === id)) return
select(selectionOfTab(id), opts)
}
// Closing the tab moves the selection off the runnable in the same tick. The
// stale-tab effect would get there too, but a frame later — long enough for
// the pane to render "No runnable at id …".
function deleteRunnable(key: string) {
delete runnables[key]
closeTab(runnableTabId(key))
}
// Ask the UI Builder iframe to open a document. `populateFiles` replays
// `iframeDocument` on iframe load, so record it even when the iframe isn't
// ready yet (the postMessage is then skipped).
function openInIframe(path: string) {
iframeDocument = path
if (iframeLoaded) {
iframe?.contentWindow?.postMessage({ type: 'selectFile', path }, '*')
}
}
@@ -584,10 +624,6 @@
const tab = tabs[idx]
if (!tab || tab.closable === false) return
const wasActive = activeTabId === id
// Clear selection before removal so the cleanup $effect doesn't recreate it.
if (wasActive && tab.id.startsWith(RUNNABLE_PREFIX)) {
selectedRunnable = undefined
}
tabs = tabs.filter((t) => t.id !== id)
if (wasActive) {
// Fall back to the previous tab (force, in case it's Preview in split).
@@ -617,7 +653,7 @@
.slice()
.reverse()
.find((t) => t.id !== PREVIEW_TAB_ID)
if (lastUserTab) activeTabId = lastUserTab.id
if (lastUserTab) activateTab(lastUserTab.id)
}
splitWithPreview = true
}
@@ -768,6 +804,13 @@
)
}
// `wmill.ts` is generated inside the iframe from `populateRunnables`' dts: the
// sidebar lists it and the iframe can open it, but it is never a key of `files`.
// Every "does this document exist?" test has to allow for that.
function isOpenableDocument(path: string) {
return path === WMILL_TS_PATH || (files ?? {})[path] !== undefined
}
function populateFiles() {
if (files) {
suppressSetActiveDocument = true
@@ -776,10 +819,13 @@
suppressSetActiveDocument = false
suppressTimer = undefined
}, 500)
const doc = untrack(() => selectedDocument)
if (doc) {
const doc = untrack(() => iframeDocument)
if (doc !== undefined && isOpenableDocument(doc)) {
setFilesAndSelectInIframe(files, doc)
} else {
// Deleted or renamed away since we last told the iframe to open it;
// asking for a path that no longer exists errors in VS Code.
iframeDocument = undefined
setFilesInIframe(files)
}
}
@@ -798,6 +844,7 @@
}
function setFilesAndSelectInIframe(newFiles: Record<string, string>, pathToSelect: string) {
iframeDocument = pathToSelect
const files = Object.fromEntries(
Object.entries(newFiles).filter(([path, _]) => !path.endsWith('/'))
)
@@ -919,8 +966,8 @@
files = {}
}
files[path] = content
selectedDocument = path
// Use combined setFilesAndSelect to avoid race condition
// Combined setFilesAndSelect avoids a race, so let it do the telling.
select({ kind: 'file', path }, { notifyIframe: false })
setFilesAndSelectInIframe(files, path)
return lint()
},
@@ -988,7 +1035,7 @@
populateRunnables()
// Switch UI to show this runnable so Monaco can analyze it
selectedRunnable = key
select({ kind: 'runnable', key })
// Wait 2 seconds for Monaco to analyze the code
await new Promise((resolve) => setTimeout(resolve, 1000))
@@ -1134,8 +1181,13 @@
}
})
})
// Write these through `select` only.
let selectedRunnable: string | undefined = $state(undefined)
let selectedDocument: string | undefined = $state(undefined)
// The document the UI Builder iframe has open. Tracks the selection while a
// file is selected, but outlives switching to a runnable or Preview so a
// reload reopens what the user was editing.
let iframeDocument: string | undefined = $state(undefined)
let inspectorElement: InspectorElementInfo | undefined = $state(undefined)
let codeSelection: AppCodeSelectionElement | undefined = $state(undefined)
@@ -1292,24 +1344,18 @@
} else if (e.data.type === 'setActiveDocument') {
if (suppressSetActiveDocument) return
// Normalize Windows-style path separators to Linux-style
selectedDocument = e.data.path?.replace(/\\/g, '/')
// If VS Code switched to a file we don't have a tab for (e.g. via
// the file explorer's reveal-in-editor, or our own auto-open of
// the main app file at boot), backfill a tab.
if (selectedDocument) {
const id = fileTabId(selectedDocument)
if (!tabs.some((t) => t.id === id)) {
ensureFileTab(selectedDocument)
// Don't auto-activate — the user's tab choice wins.
// But if no file tab is currently active, fall in line.
// Skip this auto-activation in single-view-with-preview
// mode (the caller seeded `defaultSplitWithPreview=false`
// because Preview is the intended starting tab); the
// iframe's first setActiveDocument shouldn't fight that.
if (splitWithPreview && activeTabKind === 'preview' && tabs.length === 2) {
activateTab(id)
}
}
const activePath: string | undefined = e.data.path?.replace(/\\/g, '/')
if (!activePath) return
iframeDocument = activePath
// Follow VS Code's document only while the iframe is the visible surface
// (behind a runnable tab it must not steal the selection), plus at boot
// in split mode where no file tab exists yet. `notifyIframe: false`
// keeps us from echoing the move back at it.
const bootIntoFirstFile = splitWithPreview && activeTabKind === 'preview' && tabs.length === 1
if (activeTabKind === 'file' || bootIntoFirstFile) {
select({ kind: 'file', path: activePath }, { notifyIframe: false })
} else {
ensureFileTab(activePath)
}
} else if (e.data.type === 'editorSelection') {
// Handle code selection from the iframe editor
@@ -1988,11 +2034,13 @@
previewIframe?.contentWindow?.postMessage({ type: 'inspectorClear' }, '*')
}
function handleSelectFile(path: string) {
// Adding the tab activates it; activateTab posts the selectFile message
// to the UI Builder iframe and clears any selected runnable.
const id = ensureFileTab(path)
activateTab(id)
// Folders aren't selectable in the tree, so this only ever gets files — except
// for '' when the last file is deleted, where the tab cleanup picks the
// fallback selection instead. The trailing-slash guard keeps that true if a
// future `FileExplorer` caller feeds folder paths back in.
function handleSelectPath(path: string) {
if (!path || path.endsWith('/')) return
select({ kind: 'file', path })
}
// Track previous values for change detection
@@ -2011,19 +2059,6 @@
}
})
// Mirror sidebar runnable selection into the tab system. When the user
// picks a runnable from the sidebar, `selectedRunnable` flips via
// `bind:selectedRunnable`; ensure a tab for it exists and is active.
$effect(() => {
const key = selectedRunnable
if (!key) return
const id = runnableTabId(key)
untrack(() => {
if (!tabs.some((t) => t.id === id)) ensureRunnableTab(key)
if (activeTabId !== id) activeTabId = id
})
})
// Open a default file on mount (boots the iframe in split mode and gives
// the user something to edit on the left). When the caller seeded
// `defaultSplitWithPreview=false` we instead want the Preview tab as the
@@ -2034,21 +2069,21 @@
if (!splitWithPreview) return
if (tabs.length === 1) {
const def = pickDefaultFile(files)
if (def) activateTab(ensureFileTab(def))
if (def) select({ kind: 'file', path: def })
}
})
// Drop tabs whose file/runnable no longer exists.
// Drop tabs whose file/runnable no longer exists. Enumerate the keys: reading
// only the `$state` proxy misses a `delete runnables[id]`, which is exactly
// how a runnable disappears from the sidebar.
$effect(() => {
void files
void runnables
void Object.keys(files ?? {})
void Object.keys(runnables ?? {})
untrack(() => {
const filesSet = files ?? {}
const runnablesSet = runnables ?? {}
const stale = tabs.filter((t) => {
if (t.id.startsWith(FILE_PREFIX)) {
const fp = t.id.slice(FILE_PREFIX.length)
return filesSet[fp] === undefined
return !isOpenableDocument(t.id.slice(FILE_PREFIX.length))
}
if (t.id.startsWith(RUNNABLE_PREFIX)) {
const k = t.id.slice(RUNNABLE_PREFIX.length)
@@ -2104,10 +2139,10 @@
summary = entry.summary
data = structuredClone($state.snapshot(entry.data))
// If there's a selected document that exists in the new files, use the combined message
if (selectedDocument && entry.files[selectedDocument] !== undefined) {
// If the open document survives into the new files, use the combined message
if (iframeDocument && isOpenableDocument(iframeDocument)) {
// Use combined setFilesAndSelect message to avoid race condition
setFilesAndSelectInIframe(entry.files, selectedDocument)
setFilesAndSelectInIframe(entry.files, iframeDocument)
} else {
// Otherwise just set files normally
setFilesInIframe(entry.files)
@@ -2312,9 +2347,11 @@
setFilesInIframe(newFiles ?? {})
}
}
onSelectFile={handleSelectFile}
bind:selectedRunnable
bind:selectedDocument
onSelectPath={handleSelectPath}
onSelectRunnable={(key) => select({ kind: 'runnable', key })}
onDeleteRunnable={deleteRunnable}
{selectedRunnable}
{selectedDocument}
dataTableRefs={dataTableRefsObjects}
onDataTableRefsChange={(newRefs) => {
data.tables = newRefs.map(formatDataTableRef)
@@ -13,12 +13,16 @@
import { sendUserToast } from '$lib/toast'
interface Props {
/** Read-only; the editor switches selection through `onSelect`. */
selectedRunnable: string | undefined
runnables: Record<string, Runnable>
onSelect?: (id: string) => void
/** The editor deletes and moves the selection off it in one tick. Required:
* the row's Delete does nothing without it. */
onDelete: (id: string) => void
}
let { selectedRunnable = $bindable(), runnables, onSelect }: Props = $props()
let { selectedRunnable, runnables, onSelect, onDelete }: Props = $props()
let editingId: string | undefined = $state(undefined)
@@ -54,7 +58,6 @@
delete runnables[oldId]
if (selectedRunnable === oldId) {
selectedRunnable = newId
onSelect?.(newId)
}
editingId = undefined
@@ -86,7 +89,6 @@
type: 'inline'
}
selectedRunnable = nid
onSelect?.(nid)
}
</script>
@@ -125,16 +127,8 @@
{runnable}
isSelected={selectedRunnable === id}
isEditing={editingId === id}
onSelect={() => {
selectedRunnable = id
onSelect?.(id)
}}
onDelete={() => {
delete runnables[id]
if (selectedRunnable === id) {
selectedRunnable = undefined
}
}}
onSelect={() => onSelect?.(id)}
onDelete={() => onDelete(id)}
onRename={(newId) => renameRunnable(id, newId)}
onRequestEdit={() => (editingId = id)}
onCancelEdit={() => (editingId = undefined)}
@@ -3,6 +3,7 @@
SUBTLE_PANEL_TITLE
} from '../apps/editor/settingsPanel/common/PanelSection.svelte'
import type { Runnable } from '../apps/inputType'
import { WMILL_TS_PATH } from './utils'
import RawAppInlineScriptPanelList from './RawAppInlineScriptPanelList.svelte'
import FileExplorer from '../FileExplorer.svelte'
import { Plus, File, Folder, Camera } from 'lucide-svelte'
@@ -18,10 +19,14 @@
interface Props {
runnables: Record<string, Runnable>
/** Read-only; the editor switches selection through `onSelectRunnable`. */
selectedRunnable: string | undefined
files: Record<string, string>
modules?: Modules
onSelectFile?: (path: string) => void
onSelectRunnable?: (key: string) => void
onDeleteRunnable: (key: string) => void
onSelectPath?: (path: string) => void
/** Read-only; the editor switches selection through `onSelectPath`. */
selectedDocument: string | undefined
historyManager?: RawAppHistoryManager
historySelectedId?: number | undefined
@@ -39,11 +44,13 @@
let {
runnables,
selectedRunnable = $bindable(),
selectedRunnable,
files = $bindable({}),
modules,
onSelectFile,
selectedDocument = $bindable(),
onSelectRunnable,
onDeleteRunnable,
onSelectPath,
selectedDocument,
historyManager,
historySelectedId,
onHistorySelect,
@@ -79,13 +86,6 @@
}
let fileExplorer: FileExplorer | undefined = $state()
function handleSelectPath(path: string) {
selectedDocument = path
if (!path.endsWith('/')) {
onSelectFile?.(path)
}
}
</script>
<PanelSection
@@ -98,8 +98,8 @@
{#snippet action()}
<div class="flex gap-1">
<Button
onClick={() => fileExplorer?.handleAddRootFile()}
title="Add file to root"
onClick={() => fileExplorer?.handleAddFileBesideSelection()}
title="New file beside the selected one"
unifiedSize="xs"
variant="subtle"
btnClasses="px-1 gap-0.5"
@@ -108,8 +108,8 @@
<File size={12} />
</Button>
<Button
onClick={() => fileExplorer?.handleAddRootFolder()}
title="Add folder to root"
onClick={() => fileExplorer?.handleAddFolderBesideSelection()}
title="New folder beside the selected file"
unifiedSize="xs"
variant="subtle"
btnClasses="px-1 gap-0.5"
@@ -132,8 +132,8 @@
bind:this={fileExplorer}
bind:files
selectedPath={selectedDocument}
onSelectPath={handleSelectPath}
extraNodes={[{ name: 'wmill.ts', path: '/wmill.ts', isFolder: false }]}
{onSelectPath}
extraNodes={[{ name: 'wmill.ts', path: WMILL_TS_PATH, isFolder: false }]}
hideHeader
/>
</PanelSection>
@@ -142,11 +142,10 @@
<div class="py-4"></div>
<RawAppInlineScriptPanelList
bind:selectedRunnable
{selectedRunnable}
{runnables}
onSelect={() => {
selectedDocument = undefined
}}
onSelect={onSelectRunnable}
onDelete={onDeleteRunnable}
/>
<div class="py-4"></div>
@@ -233,6 +233,9 @@ function hiddenRunnableToTsType(runnable: Runnable) {
}
}
/** Shown in the file tree, generated by `genWmillTs` — never a key of `files`. */
export const WMILL_TS_PATH = '/wmill.ts'
export function genWmillTs(runnables: Record<string, Runnable>) {
return `// THIS FILE IS READ-ONLY
// AND GENERATED AUTOMATICALLY FROM YOUR RUNNABLES