Skip to content

Commit 73d9da8

Browse files
committed
web/shell: the engine, the runtime and the terminal (round 1, each independently verified)
LevelCode Shell stage 1 — a JavaScript bash over vscode.workspace.fs, for the visitor's terminal and the agent's run_command; no programs, no network. engine/ just-bash 3.6.0 vendored unmodified (sha256 pinned) and patched in memory at build time (anchor-checked); a copy-on-write overlay over a read-only RPC; compat wrappers; limits; the worker entry. 535 golden cases recorded from GNU bash 5.1 / coreutils 8.32. src/ runtime (engine worker lifecycle, read-only fs RPC, sessions, the agent's ChildLike, commit), commit (the visitor's terminal), info, sanitize, extension. src/terminal/ Pseudoterminal: input parser, line editor (checked against real xterm.js), completeness, output allow-list filter, history, prompt, agent tab, shell integration. Known defects found by the verifiers are fixed in the commits that follow.
1 parent 817f3cb commit 73d9da8

108 files changed

Lines changed: 36539 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎scripts/build-web.mjs‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ hashTree(fingerprint, aiDir, { skip: ['test', 'node_modules', 'scripts', '.DS_St
7676
hashTree(fingerprint, path.join(WEB, 'ai-extension'), { skip: ['assets.generated.js', 'node_modules', '.DS_Store'] });
7777
hashTree(fingerprint, path.join(WEB, 'workspace'), { skip: ['node_modules', '.DS_Store'] });
7878
hashTree(fingerprint, path.join(WEB, 'theme'), { skip: ['.DS_Store'] });
79+
hashTree(fingerprint, path.join(WEB, 'shell'), { skip: ['node_modules', '.DS_Store'] });
7980
hashTree(fingerprint, path.join(WEB, 'lib'), { skip: ['.DS_Store'] });
8081
// The declarative built-ins are staged from the checkout's extensions/ (grammars, language configurations, the desktop's
8182
// themes — whose manifest this build rewrites): a changed grammar is a changed build, and so is the fallback copy of
@@ -84,7 +85,7 @@ hashTree(fingerprint, path.join(WEB, 'lib'), { skip: ['.DS_Store'] });
8485
const baked = fs.existsSync(path.join(staticDir, 'extensions')) ? fs.readdirSync(path.join(staticDir, 'extensions')) : [];
8586
const source = path.join(vscodeDir, 'extensions');
8687
if (fs.existsSync(source)) {
87-
for (const name of declarativeExtensionNames(source, [...baked, 'levelcode-ai', 'levelcode-web'])) {
88+
for (const name of declarativeExtensionNames(source, [...baked, 'levelcode-ai', 'levelcode-web', 'levelcode-shell'])) {
8889
fingerprint.update('\0ext\0' + name);
8990
hashTree(fingerprint, path.join(source, name), { skip: [...COPY_SKIP, '.DS_Store'] });
9091
}
@@ -114,13 +115,17 @@ log('bundling levelcode-ai for the web worker extension host');
114115
const r = spawnSync(process.execPath, [path.join(WEB, 'ai-extension', 'build.mjs'), '--out', extOut, '--vscode', vscodeDir, '--minify'], { stdio: 'inherit' });
115116
if (r.status !== 0) { die('the levelcode-ai browser build failed'); }
116117

118+
log('bundling levelcode-shell (the shell extension and its engine worker)');
119+
const rShell = spawnSync(process.execPath, [path.join(WEB, 'shell', 'build.mjs'), '--out', extOut, '--vscode', vscodeDir, '--minify'], { stdio: 'inherit' });
120+
if (rShell.status !== 0) { die('the levelcode-shell build failed'); }
121+
117122
log('staging the scratch workspace and the declarative built-ins');
118123
const scratchOut = path.join(extOut, 'levelcode-web');
119124
copyExtension(path.join(WEB, 'workspace'), scratchOut);
120125
fs.writeFileSync(path.join(scratchOut, 'package.nls.json'), '{}\n');
121126
const extensionsSource = path.join(vscodeDir, 'extensions');
122127
if (!fs.existsSync(extensionsSource)) { die('no extensions/ in ' + vscodeDir + ' to take the declarative built-ins from'); }
123-
const declarative = stageDeclarativeExtensions(extensionsSource, extOut, { exclude: [...baked, 'levelcode-ai', 'levelcode-web'] });
128+
const declarative = stageDeclarativeExtensions(extensionsSource, extOut, { exclude: [...baked, 'levelcode-ai', 'levelcode-web', 'levelcode-shell'] });
124129
// A checkout that has not had LevelCode's extensions copied in still builds; the themes are then absent.
125130
if (!declarative.includes('levelcode-themes')) {
126131
const themes = path.join(REPO, 'extensions', 'levelcode-themes');
@@ -133,8 +138,8 @@ copyExtension(path.join(WEB, 'theme', 'levelcode-web-theme'), themeOut);
133138
fs.writeFileSync(path.join(themeOut, 'package.nls.json'), '{}\n');
134139
const desktopThemes = path.join(extOut, 'levelcode-themes', 'package.json');
135140
if (fs.existsSync(desktopThemes)) { withoutConfigurationDefaults(desktopThemes); }
136-
const extensions = ['levelcode-ai', 'levelcode-web', 'levelcode-web-theme', ...declarative.sort()];
137-
log(`${extensions.length} extensions: levelcode-ai, levelcode-web, levelcode-web-theme and ${declarative.length} declarative (${declarative.slice(0, 6).join(', ')}, ...)`);
141+
const extensions = ['levelcode-ai', 'levelcode-web', 'levelcode-web-theme', 'levelcode-shell', ...declarative.sort()];
142+
log(`${extensions.length} extensions: levelcode-ai, levelcode-web, levelcode-web-theme, levelcode-shell and ${declarative.length} declarative (${declarative.slice(0, 6).join(', ')}, ...)`);
138143

139144
log('writing the page');
140145
// main.js and boot.css are addressed by the build id like everything else; the page names them.
@@ -190,7 +195,8 @@ JSON.parse(m[1]);
190195
if (/\{\{[A-Z]+\}\}/.test(html)) { die('index.html still has an unfilled placeholder'); }
191196
for (const must of [`${prefix}/main.js`, `${prefix}/boot.css`, `${prefix}/static/out/nls.messages.js`, `${prefix}/extensions/levelcode-ai/extension.web.js`,
192197
`${prefix}/extensions/levelcode-ai/package.json`, `${prefix}/extensions/levelcode-web/extension.js`,
193-
`${prefix}/chrome.css`, `${prefix}/extensions/levelcode-web-theme/package.json`]) {
198+
`${prefix}/chrome.css`, `${prefix}/extensions/levelcode-web-theme/package.json`,
199+
`${prefix}/extensions/levelcode-shell/package.json`, `${prefix}/extensions/levelcode-shell/extension.js`, `${prefix}/extensions/levelcode-shell/engine.worker.js`]) {
194200
if (!fs.existsSync(path.join(outDir, must))) { die('missing ' + must); }
195201
}
196202
log(`done: ${outDir} ${mb(sizeOf(outDir))} (client ${mb(sizeOf(path.join(root, 'static')))}, extensions ${mb(sizeOf(extOut))})`);

‎web/lib/config.mjs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ export function webConfig(o = {}) {
6161
// position zero too), which washes out line one. The editor starts below it.
6262
'editor.padding.top': 24,
6363
// Light or dark follows the visitor's system; the dark one is where it starts.
64+
// The terminal (LevelCode Shell): a scrollback that holds a screenful of output the agent printed, no file links
65+
// the workbench cannot resolve in a virtual workspace, only web links that may be followed, no toast when a
66+
// command's status is not 0, and no kitty keyboard protocol (the shell does not speak it).
67+
'terminal.integrated.scrollback': 10000,
68+
'terminal.integrated.enableFileLinks': 'off',
69+
'terminal.integrated.allowedLinkSchemes': ['http', 'https', 'mailto'],
70+
'terminal.integrated.showExitAlert': false,
71+
'terminal.integrated.enableKittyKeyboardProtocol': false,
6472
'window.autoDetectColorScheme': true,
6573
'workbench.colorTheme': 'LevelCode Web Dark',
6674
'workbench.preferredDarkColorTheme': 'LevelCode Web Dark',

‎web/serve.mjs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,14 @@ const server = http.createServer((req, res) => {
6262
}
6363
if (pathname.startsWith('/static/')) { file = within(STATIC_DIR, pathname.slice('/static/'.length)); }
6464
else if (pathname.startsWith('/extensions/')) { file = within(EXT_DIR, pathname.slice('/extensions/'.length)); }
65-
else { file = within(WEB_DIR, pathname.slice(1)); }
66-
if (!file || file.includes(`${path.sep}lib${path.sep}`) || file.endsWith('serve.mjs')) { res.writeHead(404).end('not found'); return; }
65+
else {
66+
file = within(WEB_DIR, pathname.slice(1));
67+
// The page's own files are served; the server's and the build's are not. (This used to be a test for any path
68+
// containing /lib/, which also hid node_modules/@xterm/xterm/lib/xterm.js: the terminal never started here.)
69+
const own = file && path.relative(WEB_DIR, file).split(path.sep)[0];
70+
if (own === 'lib' || own === 'test' || own === 'shell' || (file && file.endsWith('serve.mjs'))) { file = null; }
71+
}
72+
if (!file) { res.writeHead(404).end('not found'); return; }
6773
}
6874
if (!file) { res.writeHead(404).end('not found'); return; }
6975

0 commit comments

Comments
 (0)