This commit is contained in:
seb
2026-10-12 14:30:26 +02:00
parent 627868eb2e
commit 15dce3c1cf
4 changed files with 231 additions and 35 deletions

View File

@@ -31,10 +31,11 @@ import { vfs as vfsApi } from '../../api';
export default class VfsAppletRunner extends PureComponent {
constructor(props) {
super(props);
const { inputSchema, outputColumns } = parseAppletSchemas(props.content || '');
const { inputSchema, outputColumns, description } = parseAppletSchemas(props.content || '');
this.state = {
inputSchema,
outputColumns,
description: description || props.file?.description || '',
inputValues: buildAppletDefaults(inputSchema),
isRunning: false,
runError: null,
@@ -50,10 +51,11 @@ export default class VfsAppletRunner extends PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.content !== this.props.content || prevProps.fileName !== this.props.fileName) {
const { inputSchema, outputColumns } = parseAppletSchemas(this.props.content || '');
const { inputSchema, outputColumns, description } = parseAppletSchemas(this.props.content || '');
this.setState({
inputSchema,
outputColumns,
description: description || this.props.file?.description || '',
inputValues: buildAppletDefaults(inputSchema),
isRunning: false,
runError: null,
@@ -122,6 +124,7 @@ export default class VfsAppletRunner extends PureComponent {
stdout: res?.stdout || '',
durationMs: res?.durationMs ?? null,
truncated: Boolean(res?.truncated),
description: res?.description || this.state.description,
});
} catch (err) {
this.setState({
@@ -236,6 +239,7 @@ export default class VfsAppletRunner extends PureComponent {
const {
inputSchema,
outputColumns,
description: stateDescription,
inputValues,
isRunning,
runError,
@@ -245,6 +249,7 @@ export default class VfsAppletRunner extends PureComponent {
durationMs,
truncated,
} = this.state;
const description = stateDescription || file?.description;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
@@ -268,14 +273,27 @@ export default class VfsAppletRunner extends PureComponent {
<ArrowBackRoundedIcon fontSize="small" />
</IconButton>
<VfsFileIcon fileName={fileName} mime={file?.mime} fontSize="small" />
<Typography
variant="body2"
fontWeight={700}
noWrap
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
>
{fileName}
</Typography>
<Box sx={{ minWidth: 0 }}>
<Typography
variant="body2"
fontWeight={700}
noWrap
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
>
{fileName}
</Typography>
{description && (
<Typography
variant="caption"
color="text.secondary"
noWrap
title={description}
sx={{ display: 'block', fontSize: '0.72rem', lineHeight: 1.2 }}
>
{description}
</Typography>
)}
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexShrink: 0 }}>

View File

@@ -55,7 +55,11 @@ function buildFileTree(files, searchQuery = '') {
const q = searchQuery ? searchQuery.trim().toLowerCase() : '';
const filtered = (files || []).filter((f) => {
if (!q) return true;
return f.name.toLowerCase().includes(q) || (f.mime && f.mime.toLowerCase().includes(q));
return (
f.name.toLowerCase().includes(q) ||
(f.mime && f.mime.toLowerCase().includes(q)) ||
(f.description && f.description.toLowerCase().includes(q))
);
});
const root = {
@@ -457,25 +461,65 @@ export default class VfsFileExplorer extends PureComponent {
/>
<Box sx={{ minWidth: 0, flexGrow: 1, pr: 1 }}>
<Typography
variant="body2"
fontWeight={isSelected ? 600 : 500}
noWrap
sx={{ fontFamily: 'monospace', fontSize: '0.82rem' }}
>
{file.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 0.15 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
{formatVfsSize(file.fileData.size)}
</Typography>
{file.fileData.updated_at && (
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem' }}>
{formatVfsDate(file.fileData.updated_at)}
{file.fileData?.description ? (
<>
{/* Line 1: Description */}
<Typography
variant="body2"
fontWeight={isSelected ? 600 : 500}
noWrap
title={file.fileData.description}
sx={{ fontSize: '0.82rem', lineHeight: 1.3 }}
>
{file.fileData.description}
</Typography>
)}
</Box>
{/* Line 2: size, date, filename */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.15, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem', flexShrink: 0 }}>
{formatVfsSize(file.fileData.size)}
</Typography>
{file.fileData.updated_at && (
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem', flexShrink: 0 }}>
{formatVfsDate(file.fileData.updated_at)}
</Typography>
)}
<Typography
variant="caption"
color="text.secondary"
noWrap
title={file.name}
sx={{ fontFamily: 'monospace', fontSize: '0.68rem', minWidth: 0 }}
>
{file.name}
</Typography>
</Box>
</>
) : (
<>
{/* Line 1: filename */}
<Typography
variant="body2"
fontWeight={isSelected ? 600 : 500}
noWrap
sx={{ fontFamily: 'monospace', fontSize: '0.82rem' }}
>
{file.name}
</Typography>
{/* Line 2: size, date */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 0.15 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
{formatVfsSize(file.fileData.size)}
</Typography>
{file.fileData.updated_at && (
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem' }}>
{formatVfsDate(file.fileData.updated_at)}
</Typography>
)}
</Box>
</>
)}
</Box>
{/* Quick action buttons on hover / select */}

View File

@@ -208,15 +208,38 @@ export function parseCsvRows(text, separator = APPLET_CSV_SEPARATOR) {
return rows;
}
/**
* Parse the applet description header.
*
* Accepts either a quoted string (e.g. `"Lorem ipsum"` or `'Lorem ipsum'`) or bare text.
* Strips surrounding quotes if present and trims whitespace.
*
* @param {string} header Raw text after the `// @applet-description ` marker.
* @returns {string}
*/
export function parseAppletDescription(header) {
const text = String(header || '').trim();
if (!text) return '';
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
try {
const parsed = JSON.parse(text);
if (typeof parsed === 'string' && parsed.trim()) return parsed.trim();
} catch {
const unquoted = text.slice(1, -1).trim();
if (unquoted) return unquoted;
}
}
return text;
}
/**
* Parse applet metadata from the file header.
*
* Mirrors the backend's `readApplet()`: the input JSON Schema must be on line 1 and the typed
* CSV output columns on line 2. Parsing the same two lines keeps the UI in lockstep with what
* the sandbox will actually accept.
* Mirrors the backend's `readApplet()`: the input JSON Schema must be on line 1, the typed
* CSV output columns on line 2, and the applet description on line 3.
*
* @param {string} content Applet file source.
* @returns {{ inputSchema: object|null, outputColumns: Array<{name: string, type: string}>|null }}
* @returns {{ inputSchema: object|null, outputColumns: Array<{name: string, type: string}>|null, description: string }}
*/
export function parseAppletSchemas(content) {
const lines = String(content || '').replace(/^\uFEFF/, '').split(/\r?\n/);
@@ -238,7 +261,15 @@ export function parseAppletSchemas(content) {
outputColumns = parseTypedCsvColumns(outputMatch[1]);
}
return { inputSchema, outputColumns };
let description = '';
const descMatch =
lines[2]?.match(/^\/\/ @applet-description (.+)$/) ||
lines.slice(0, 10).map((l) => l.match(/^\/\/ @applet-description (.+)$/)).find(Boolean);
if (descMatch) {
description = parseAppletDescription(descMatch[1]);
}
return { inputSchema, outputColumns, description };
}
/**