Add app-console runme helpers for run-all and clear-outputs#81
Add app-console runme helpers for run-all and clear-outputs#81hamelsmu wants to merge 15 commits intorunmedev:mainfrom
Conversation
- Add MarkdownCell component with edit/render mode toggle - Double-click rendered markdown to enter edit mode - Press Escape or click away to render markdown - Fix Monaco editor layout issue causing horizontal expansion - Remove window.innerWidth fallback in adjustHeight() - Add contain: inline-size to prevent feedback loop - Add min-w-0/max-w-full CSS constraints throughout - Add tests for MarkdownCell component Amp-Thread-ID: https://ampcode.com/threads/T-019c2c77-9f58-726f-9c8d-21d4e1b6aff1 Co-authored-by: Amp <amp@ampcode.com>
- Fix memo comment to clarify React.memo semantics (return true = skip re-render) - Add useEffect to enforce 'empty cells must be in edit mode' invariant - Handles external changes (undo/redo, sync) that clear cell value - react-components version also resets on cell identity change - Add role='button' for accessibility on rendered markdown view - Guard keyboard handler to only trigger on container, not nested elements - Prevents Enter/Space from breaking links inside rendered markdown Amp-Thread-ID: https://ampcode.com/threads/T-019c2c77-9f58-726f-9c8d-21d4e1b6aff1 Co-authored-by: Amp <amp@ampcode.com>
…unmedev#55) Implements ContentsNotebookStore using the Go backend ContentsService via ConnectRPC JSON over HTTP POST. This provides a fully automatable storage backend that works in CI/headless environments without requiring browser user gestures. Features: - Full NotebookStore implementation (list, load, save, create, rename, getMetadata) - SHA256-based conflict detection with fork-on-conflict strategy - Markdown file support via injectable MarkdownConverter interface - IGNORED_DIRECTORIES and hidden file filtering in list() - ConflictResult return type moved to shared NotebookStore interface - UTF-8 safe base64 encoding/decoding for non-ASCII content - ContentsStoreContext + provider for React integration - Wired into App.tsx NotebookStoreInitializer - 56 comprehensive tests covering all operations URI scheme: contents://<host:port>/file|dir/<encodedPath> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…4 in create() Remove leaked FilesystemNotebookStore import/field/setter from AppState.ts that references non-existent fs.ts on this branch (build breaker). Also use utf8ToBase64() consistently in create() instead of raw btoa(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion test infrastructure - Fix ContentsNotebookStore.rename() to preserve markdown extensions - Add ContentsRpcError typed error class for robust conflict detection (HTTP 409) - Remove GoogleDrivePickerButton from Explorer header per UX review - Expand App Console commands: explorer.mountDrive(), openPicker(), removeFolder(), help() - Use resolved agent endpoint instead of hardcoded VITE_DEFAULT_AGENT_ENDPOINT - Filter dot-directories in ContentsNotebookStore.list() - Fix base64 encoding with 32KB chunked processing for large payloads - Add storeResolver.ts for URI-based routing to correct store - Add integration test notebooks (.runme.md) for CLI and browser testing - Add agent-browser test script with auth bypass pre-flight check - Fix .env.development port mismatch (9988 -> 9977) - Add 20 new unit tests (62 total in contents.test.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused isContentsUri import in NotebookContext.tsx (noUnusedLocals) - Use buildContentsUri helper in AppConsole instead of manual regex parsing - Include refId in MarkdownCell memo comparator to prevent stale rendered state - Introduce ResolvedStore type alias to remove unsafe cast in storeResolver - Fix documentation paths in browser test script and README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hamel Husain <hamel.husain@gmail.com>
Restore utf8ToBase64() chunked encoding helper that was lost during rebase. Document the known unsafe cast in storeResolver.ts (Copilot review feedback - proper fix requires LocalNotebooks to implement full NotebookStore interface). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
App.tsx calls appState.setFilesystemStore() but the method did not exist on AppState, causing a runtime TypeError that resulted in a blank screen. Amp-Thread-ID: https://ampcode.com/threads/T-019c3511-3c46-7022-871c-8cda3ec09592 Co-authored-by: Amp <amp@ampcode.com>
- test-smoke.sh: browser-based smoke test that checks for JS errors, verifies core UI elements render, and detects blank screen issues - typecheck script: runs tsc against tsconfig.app.json (not wired into build yet due to pre-existing type errors) These would have caught the setFilesystemStore TypeError (c4b4d48). Amp-Thread-ID: https://ampcode.com/threads/T-019c3511-3c46-7022-871c-8cda3ec09592 Co-authored-by: Amp <amp@ampcode.com>
…ck testing) Amp-Thread-ID: https://ampcode.com/threads/T-019c3511-3c46-7022-871c-8cda3ec09592 Co-authored-by: Amp <amp@ampcode.com>
Signed-off-by: Hamel Husain <hamel.husain@gmail.com> Amp-Thread-ID: https://ampcode.com/threads/T-019c3550-58d7-71cb-8e0f-dffaeb9ca1e0 Co-authored-by: Amp <amp@ampcode.com>
Signed-off-by: Hamel Husain <hamel.husain@gmail.com> Amp-Thread-ID: https://ampcode.com/threads/T-019c3550-58d7-71cb-8e0f-dffaeb9ca1e0 Co-authored-by: Amp <amp@ampcode.com>
Shows a red toast at the top of the viewport when a user tries to run a code cell but the Runme backend server is unreachable. The toast fires in two scenarios: when no runner/endpoint is configured, and when the WebSocket connection to the backend fails. New files: - app/src/lib/toast.ts (global event emitter) - app/src/components/Toast.tsx (GlobalToast UI component) Closes runmedev#71 Amp-Thread-ID: https://ampcode.com/threads/T-019c54ff-b8db-746a-88b9-3595e525ea3d Co-authored-by: Amp <amp@ampcode.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c87f2aade
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isFsUri(uri) && fsStore) { | ||
| return fsStore; | ||
| } | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
| return localStore as unknown as NotebookStore | null; |
There was a problem hiding this comment.
Return null when specialized store is unavailable
resolveStore falls back to localStore for fs:///contents:// URIs whenever the dedicated store is not initialized yet, which makes callers (for example NotebookContext loaders) invoke LocalNotebooks methods with non-local:// URIs and fail at runtime; this can surface as failed notebook loads and current-doc reset during startup races. For non-local schemes, the resolver should return null until the matching store is ready instead of silently routing to the local store.
Useful? React with 👍 / 👎.
| help: () => { | ||
| return [ | ||
| "Available namespaces:", | ||
| " runme - Notebook helpers (run all, clear outputs)", | ||
| " explorer - Manage workspace folders and notebooks", |
There was a problem hiding this comment.
Print help text to the console output
This help implementation only returns a string, but JSKernel.run does not print expression return values, so entering help() (the prompt’s suggested command) shows no help text to the user. The new top-level help path should write through sendStdout (like runme.help) so command discovery actually works in the app console.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR expands the app’s developer-facing console and notebook UX by introducing a new runme helper namespace in the App Console (run all cells / clear outputs), plus supporting changes to notebook storage routing (fs:// vs contents://), markdown cell rendering, and test/fixture coverage for UI + large payload scenarios.
Changes:
- Add
runme.*App Console helpers (getCurrentNotebook,runAll,clearOutputs,help) and preserve a globalhelp()inJSKernel. - Improve notebook handling: store resolution by URI scheme, conflict-result type plumbing, cell
refIdnormalization for JSON fixtures, debounced auto-save, and base64 chunking helper. - Add UI/testing support: empty-state quick-start snippets, global toast, browser smoke/integration scripts, and several fixture notebooks (including large-payload).
Reviewed changes
Copilot reviewed 39 out of 41 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds lock entries for new markdown-related dependencies (e.g., remark-gfm). |
| packages/react-components/src/components/index.tsx | Re-exports MarkdownCell. |
| packages/react-components/src/components/Actions/tests/MarkdownCell.test.tsx | Adds unit tests for the react-components MarkdownCell. |
| packages/react-components/src/components/Actions/MarkdownCell.tsx | Introduces Jupyter-style in-place markdown rendering for react-components. |
| packages/react-components/package.json | Adds remark-gfm dependency. |
| app/vite.config.mts | Ignores notebook fixture writes to avoid dev full-page reload loops. |
| app/test/fixtures/notebooks/ui-test.runme.md | Adds UI/UX test notebook fixture. |
| app/test/fixtures/notebooks/subdirectory/nested-notebook.json | Adds nested fixture to test explorer folder navigation. |
| app/test/fixtures/notebooks/large-payload-test.runme.md | Adds >100KB payload fixture for large payload/base64 testing. |
| app/test/fixtures/notebooks/hello-world.json | Adds simple JSON notebook fixture. |
| app/test/fixtures/notebooks/cell-types.json | Adds multi-language + markdown features JSON fixture. |
| app/test/fixtures/notebooks/cell-types-test.runme.md | Adds multi-language .runme.md fixture. |
| app/test/fixtures/notebooks/basic-test.runme.md | Adds basic .runme.md fixture. |
| app/test/browser/test-smoke.sh | Adds startup/blank-screen smoke test using agent-browser. |
| app/test/browser/test-notebook-ui.sh | Adds notebook UI integration test using fixtures + screenshots. |
| app/test/browser/test-app-console-commands.sh | Adds App Console integration test for explorer commands. |
| app/test/browser/README.md | Documents browser integration test setup and fixtures. |
| app/src/storage/storeResolver.ts | Adds URI-scheme routing to pick the correct NotebookStore. |
| app/src/storage/notebook.ts | Changes NotebookStore.save() to return ConflictResult. |
| app/src/storage/fs.ts | Ensures loaded JSON notebooks have stable refIds for cells. |
| app/src/storage/drive.ts | Updates Drive store save() to return ConflictResult. |
| app/src/storage/contents.ts | Adds chunked UTF-8→base64 helper and uses it for create-write path. |
| app/src/lib/toast.ts | Adds lightweight global toast event bus. |
| app/src/lib/runtime/runmeConsole.ts | Implements runme.* console helper API (run-all / clear-outputs). |
| app/src/lib/runtime/runmeConsole.test.ts | Adds unit tests for runmeConsole behavior. |
| app/src/lib/runtime/jsKernel.ts | Preserves a custom global help() if provided; otherwise uses default. |
| app/src/lib/runtime/AppState.ts | Reorders localNotebooks field and setter placement. |
| app/src/lib/notebookData.ts | Adds store abstraction, debounced persistence, and backend-error toasts. |
| app/src/lib/aisreClient.ts | Adds parser-service deserialize helper. |
| app/src/index.css | Adds selection styling for quick-start code snippet. |
| app/src/contexts/NotebookContext.tsx | Uses resolveStore and avoids persisting on initial loads. |
| app/src/contexts/CurrentDocContext.tsx | Allows direct contents:// and fs:// doc resolution from URL. |
| app/src/components/Workspace/WorkspaceExplorer.tsx | Improves tree row layout/wrapping; avoids store mirroring for fs/contents docs. |
| app/src/components/Toast.tsx | Adds global toast UI component. |
| app/src/components/AppConsole/AppConsole.tsx | Adds runme + expanded explorer helpers; captures output for tests; updates console banner/help. |
| app/src/components/Actions/MarkdownCell.tsx | Updates app markdown cell for accessibility + edit/render switching. |
| app/src/components/Actions/Editor.tsx | Switches Monaco theme to vs-dark; avoids bad width fallbacks; adjusts wrapper styling. |
| app/src/components/Actions/Actions.tsx | Updates markdown/code cell chrome, adds empty-state quick-start console snippets. |
| app/src/App.tsx | Wires global toast; passes agent endpoint to initializer for Contents store. |
| app/package.json | Adds typecheck script. |
| app/.env.development | Updates default agent/runner endpoint ports to 9977. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
app/src/storage/fs.ts:387
FilesystemNotebookStoreimplementsNotebookStore, but itssave()method still returnsPromise<void>and throws on conflict. With the updatedNotebookStore.save(): Promise<ConflictResult>, this store should return aConflictResult(and ideally not throw for the conflict case) so callers can handle conflicts consistently across stores.
const notebook = fromJsonString(parser_pb.NotebookSchema, text, {
ignoreUnknownFields: true,
});
ensureCellRefIds(notebook);
return notebook;
}
async save(uri: string, notebook: parser_pb.Notebook): Promise<void> {
const parsed = parseFsUri(uri);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Result returned by save() when a conflict is detected. | ||
| * Stores that don't support conflict detection return `{ conflicted: false }`. | ||
| */ | ||
| export interface ConflictResult { | ||
| conflicted: boolean; | ||
| conflictFileName?: string; | ||
| } | ||
|
|
||
| export interface NotebookStore { | ||
| save(uri: string, notebook: parser_pb.Notebook): Promise<void>; | ||
| save(uri: string, notebook: parser_pb.Notebook): Promise<ConflictResult>; | ||
| load(uri: string): Promise<parser_pb.Notebook>; |
There was a problem hiding this comment.
NotebookStore.save() now returns Promise<ConflictResult>, but there are still implementations (e.g. FilesystemNotebookStore and ContentsNotebookStore) returning Promise<void>, which will cause TypeScript errors and/or inconsistent conflict handling. Update all NotebookStore implementers so their save() signature matches and they return { conflicted: false } on success and { conflicted: true, ... } (instead of throwing) when a conflict is detected.
| let binary = ""; | ||
| for (let i = 0; i < bytes.length; i += CHUNK) { | ||
| const chunk = bytes.subarray(i, i + CHUNK); | ||
| binary += String.fromCharCode(...chunk); |
There was a problem hiding this comment.
The new utf8ToBase64() helper is added to avoid stack/size issues, but ContentsNotebookStore.save() still uses btoa(json) (see below in the file), so saving large notebooks can still hit the same failure mode. Consider switching save() to use utf8ToBase64() as well, and update save() to return ConflictResult to match the updated NotebookStore interface.
| binary += String.fromCharCode(...chunk); | |
| for (let j = 0; j < chunk.length; j++) { | |
| binary += String.fromCharCode(chunk[j]); | |
| } |
| return data; | ||
| }, | ||
| [emit, notebookStore], | ||
| [emit, notebookStore, contentsStore], |
There was a problem hiding this comment.
ensureNotebook uses fsStore when calling resolveStore(...), but fsStore is missing from the hook dependency array. This can leave NotebookData instances bound to a stale store if the filesystem store initializes after the callback is memoized. Add fsStore to the dependency list (and any other referenced store values).
| [emit, notebookStore, contentsStore], | |
| [emit, notebookStore, fsStore, contentsStore], |
| // Load any notebooks that were open last session once a store is available. | ||
| useEffect(() => { | ||
| if (!notebookStore) { | ||
| if (!notebookStore && !contentsStore) { | ||
| return; | ||
| } | ||
| const loadExisting = async () => { | ||
| for (const item of openNotebooks) { | ||
| const entry = storeRef.current.get(item.uri); | ||
| if (entry?.loaded) { | ||
| continue; | ||
| } | ||
| if (!entry) { | ||
| // We persisted an open notebook URI without a corresponding model in memory. | ||
| // Skip here; the model should be created elsewhere before loading. | ||
| continue; | ||
| } | ||
| const store = resolveStore(item.uri, notebookStore, fsStore, contentsStore); | ||
| if (!store) { | ||
| continue; | ||
| } | ||
| try { | ||
| const [metadata, notebook] = await Promise.all([ | ||
| notebookStore.getMetadata(item.uri), | ||
| notebookStore.load(item.uri), | ||
| store.getMetadata(item.uri), | ||
| store.load(item.uri), | ||
| ]); | ||
| entry.data.loadNotebook(notebook); | ||
| entry.data.loadNotebook(notebook, { persist: false }); | ||
| entry.loaded = true; | ||
| } catch (error) { | ||
| console.error("Failed to load open notebook", item.uri, error); | ||
| } | ||
| } | ||
| }; | ||
| void loadExisting(); | ||
| }, [notebookStore, openNotebooks]); | ||
| }, [notebookStore, contentsStore, openNotebooks]); |
There was a problem hiding this comment.
This effect calls resolveStore(item.uri, notebookStore, fsStore, contentsStore) but fsStore is not included in the dependency array. If fsStore becomes available after the effect has run, open fs:// notebooks may never load (or may load via the wrong store). Include fsStore (and any other referenced store values) in the deps.
| <Text | ||
| size="2" | ||
| className="mt-1 p-2 font-bold text-gray-400 font-mono" | ||
| data-testid="sequence-label" | ||
| > | ||
| [{sequenceLabel}] | ||
| </Text> |
There was a problem hiding this comment.
sequenceLabel returns a single space when no sequence is set, but the UI now always renders it inside brackets ([{sequenceLabel}]), which produces a visible "[ ]" placeholder for most cells. Either only render the label when a real sequence exists, or have sequenceLabel return an empty string and skip the brackets when blank.
Summary
runmenamespace in the App Console with:runme.getCurrentNotebook()runme.runAll([notebookOrUri])runme.clearOutputs([notebookOrUri])runme.help()help()inJSKernelso App Console namespace help remains visibleTesting
cd web/app && npx vitest run src/lib/runtime/runmeConsole.test.tscd web && pnpm run buildcd web/app && bash test/browser/test-smoke.sh 5173agent-browser:runme.help()runme.runAll("local://...")and confirmed console summary outputrunme.clearOutputs("local://...")and confirmed console summary outputNotes
runme.getCurrentNotebook()can returnnullwhen no active doc is resolved;runme.runAll/clearOutputsaccept an explicit notebook URI for this case.Closes #46