diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4a476..522bde3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.5.2 - Unreleased - Add `dgbuild bundle [export-name]` - builds the web page (`out/web/` + zip) for a `dialog.json` export configuration headlessly, so a GitHub Action can publish a release +- Add `dgbuild new-skein [name]` / `dgbuild open-skein [name]` - create or open a skein and drive its full interactive browser UI with no VS Code; auto-opens a browser (`--no-open` to skip), with an in-UI Quit button that prompts to save when there are unsaved changes ## 0.5.1 - 28 Aug 2026 diff --git a/README.md b/README.md index 640e369..6081065 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ A status bar item on the left also shows the current session (or lets you start - **`dgbuild test`** - runs the project's unit tests (`dgdebug --unit-test`) and exits non-zero on any failure, *or if `dialog.json` declares no `test` sources at all* (nothing to run is treated as a failure, not a silent pass). `--no-debug` excludes debug sources (included by default, matching **Dialog IDE: Run Tests**); extra arguments after the options are passed through to `dgdebug`. - **`dgbuild run-skein [names...]`** - replays one or more saved skeins (default: `default`, matching `default.skein`) against a fresh `dgdebug` process each, exits non-zero if any knot's live response no longer matches its blessed response across any of them, and prints a `valid/new/error` count summary per skein plus a `total` line when running more than one, e.g. `default: 200/0/1 (valid/new/error)`. Errored knots are printed above the summary. Add `-v/--verbose` to see the underlying `dgdebug` process commands/lifecycle logging (suppressed by default - busy otherwise, especially with multiple skeins). +- **`dgbuild new-skein [name]`** / **`dgbuild open-skein [name]`** - create a new skein (default `default`), or open an existing one, and run its full interactive browser UI headlessly - the same Skein panel the extension shows, served on `http://localhost:`. Prints the URL and opens your browser (`--no-open` to skip); the in-UI **Quit** button (or Ctrl+C) stops the server, prompting to save first if there are unsaved changes. `new-skein` takes `--seed `; `open-skein` replays every branch on load to pick up source edits. Both take `--port ` (default: an OS-assigned free port), `--theme `, `-p/--project` and `-v/--verbose`. `dgdebug` only for now. Tracing opens in a second browser tab. - **`dgbuild sources`** - prints the project's expanded source file list (`-d/--debug`, `-t/--test` to include those categories, `-T/--target ` to filter by target suffix, `-1/--single-line` for a colon-joined line instead of one path per line). - **`dgbuild bundle [export-name]`** - builds the web page (`out/web/` plus a zip at `out/-.zip`) for one of `dialog.json`'s named export configurations, the headless equivalent of **Dialog IDE: Export Web Page...** - so a GitHub Action can publish a release. Pass the configuration name, or omit it when exactly one is defined. Needs `dialogc`, `dgdebug` and `aambundle`. Add `-v/--verbose` for the underlying `dgdebug` lifecycle logging. diff --git a/docs/modules/ROOT/pages/command-line-dgbuild.adoc b/docs/modules/ROOT/pages/command-line-dgbuild.adoc index 998c94b..511b0ec 100644 --- a/docs/modules/ROOT/pages/command-line-dgbuild.adoc +++ b/docs/modules/ROOT/pages/command-line-dgbuild.adoc @@ -2,9 +2,10 @@ :navtitle: Command-Line Testing with dgbuild `dgbuild` runs the same project operations the extension does — unit tests, skein replay, source -listing, web-page bundling — from a terminal, with no editor involved. Its purpose is scripting and -continuous integration: gating a merge on a green test run, or building and publishing a release -automatically. +listing, web-page bundling, and the full interactive Skein UI — from a terminal, with no editor +involved. Its purpose is scripting and continuous integration — gating a merge on a green test run, +or building and publishing a release automatically — plus editing a skein on a machine that has no +VS Code. == What it is, and installing it @@ -78,6 +79,39 @@ $ dgbuild run-skein combat parser endings This is the command-line equivalent of the panel's *Replay All*. +== dgbuild new-skein and open-skein + +These run the *full interactive Skein UI* — the same transcript, navigation graph, trace panel and +keyboard shortcuts the extension's Skein panel provides — served as a local web page, with no editor +involved. `dgbuild` starts an HTTP server on `localhost`, prints its URL, and opens your default +browser at it (pass `--no-open` to just print the URL, e.g. on a headless box). + +`new-skein [name]` creates a new skein file (default `default.skein`) and refuses to overwrite an +existing one. `open-skein [name]` opens an existing one, refuses a missing file, and replays every +branch on load so live source edits are picked up — exactly as the extension does when it opens a +skein. Only `dgdebug` skeins are supported for now. + +[source,console] +---- +dgbuild new-skein +dgbuild new-skein combat --seed 42 +dgbuild open-skein --port 8600 --theme dark +---- + +Options: `--seed ` (`new-skein` only; default a random seed), `--port ` (default an +OS-assigned free port), `--theme `, `--no-open`, `-p` / `--project`, and `-v` / +`--verbose`. + +*Stopping it.* The navigation bar carries a *Quit* button. If the skein has unsaved changes it opens +a confirmation dialog offering *Save and Quit*, *Quit Without Saving*, or *Cancel*; a clean skein +quits immediately. Either way the browser tab is left showing a "you may close this window" screen +while `dgbuild` shuts the server and the `dgdebug` process down and exits. Pressing kbd:[Ctrl+C] in +the terminal also shuts down cleanly, but cannot save — it warns if there are unsaved changes. + +*Tracing.* Choosing *Trace* on a knot opens the trace in a second browser tab (the extension shows +it in a docked panel instead). Hovering a trace row still previews the source; clicking it does +nothing here, since there is no editor to open. + == dgbuild sources Prints the project's fully expanded source list — exactly the files the compiler would receive. diff --git a/media/js/main.js b/media/js/main.js index b8b7817..976d305 100644 --- a/media/js/main.js +++ b/media/js/main.js @@ -561,6 +561,142 @@ window.sk = { document.getElementById('insert-parent-modal')?.remove(); }, + // --------------------------------------------------------------------------- + // Standalone (`dgbuild new-skein`/`open-skein`) mode: served to a plain browser, not a VS Code + // webview. `data-standalone="true"` on (render.ts's renderPage / traceRender.ts) is the + // signal. Adds the Trace-in-a-second-tab behaviour and the Quit / shutdown flow that the + // extension handles natively. + + isStandalone() { + return document.documentElement.dataset.standalone === 'true'; + }, + + // Opens (or refocuses) the Trace page in a second browser tab. The named target means every + // Trace... (knot menu or ⌥T) reuses the one tab rather than piling up new ones; the /trace page + // live-updates over its own /trace/events SSE stream. + openTrace() { + const theme = document.documentElement.dataset.theme || 'light'; + window.open('/trace?theme=' + encodeURIComponent(theme), 'skein-trace'); + }, + + // Called first thing from the knot menu's Trace item and the ⌥T accelerator, synchronously in + // the click/keydown so window.open counts as a user gesture and isn't popup-blocked. A no-op + // outside standalone mode (the extension shows trace in a docked panel instead). It must live + // in the button's own data-on:click, not a bubbled document listener: the menu popover's + // data-on:click__stop (knot-menu.ts) stops the event before it reaches document. + maybeOpenTrace() { + if (this.isStandalone()) this.openTrace(); + }, + + // The navbar Quit button POSTs /actions/quit; if the skein is dirty the server answers by + // broadcasting this instead of shutting down. Three choices, modelled on showLabelModal's shell. + showQuitModal() { + this.hideQuitModal(); + closeAllDropdowns(); + + const overlay = document.createElement('div'); + overlay.id = 'quit-modal'; + overlay.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-grayscale'; + + const panel = document.createElement('div'); + panel.className = 'bg-base-100 rounded-lg shadow-xl max-w-full min-w-md mx-4'; + panel.setAttribute('role', 'dialog'); + panel.setAttribute('aria-modal', 'true'); + panel.setAttribute('tabindex', '-1'); + + const header = document.createElement('div'); + header.className = 'px-6 py-4 border-b border-base-200'; + const heading = document.createElement('h3'); + heading.className = 'text-lg font-medium text-base-content'; + heading.textContent = 'Unsaved Changes'; + header.appendChild(heading); + + const body = document.createElement('div'); + body.className = 'px-6 py-4'; + + const errorEl = document.createElement('div'); + errorEl.className = 'alert alert-error text-sm mb-3 hidden'; + errorEl.setAttribute('role', 'alert'); + errorEl.setAttribute('aria-live', 'assertive'); + body.appendChild(errorEl); + + const prompt = document.createElement('p'); + prompt.className = 'text-sm text-base-content mb-4'; + prompt.textContent = 'You have unsaved changes. What would you like to do?'; + body.appendChild(prompt); + + const buttonCol = document.createElement('div'); + buttonCol.className = 'flex flex-col gap-2'; + + const quitVia = async (query, failMsg) => { + errorEl.classList.add('hidden'); + let res; + try { + res = await fetch('/actions/quit' + query, { method: 'POST' }); + } catch { + errorEl.textContent = 'Failed to reach the server.'; + errorEl.classList.remove('hidden'); + return; + } + if (res.status === 204) { + // The server broadcasts sk.showShutdownScreen() over SSE; nothing more to do here. + return; + } + errorEl.textContent = failMsg; + errorEl.classList.remove('hidden'); + }; + + const saveQuitBtn = document.createElement('button'); + saveQuitBtn.type = 'button'; + saveQuitBtn.className = 'btn btn-primary'; + saveQuitBtn.textContent = 'Save and Quit'; + saveQuitBtn.addEventListener('click', () => quitVia('?save=1', 'Failed to save - not quitting.')); + + const forceQuitBtn = document.createElement('button'); + forceQuitBtn.type = 'button'; + forceQuitBtn.className = 'btn btn-warning'; + forceQuitBtn.textContent = 'Quit Without Saving'; + forceQuitBtn.addEventListener('click', () => quitVia('?force=1', 'Failed to quit.')); + + const cancelBtn = document.createElement('button'); + cancelBtn.type = 'button'; + cancelBtn.className = 'btn btn-neutral'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.addEventListener('click', () => this.hideQuitModal()); + + buttonCol.append(saveQuitBtn, forceQuitBtn, cancelBtn); + body.appendChild(buttonCol); + + panel.append(header, body); + overlay.appendChild(panel); + document.body.appendChild(overlay); + + panel.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault(); + this.hideQuitModal(); + } + }); + + panel.focus(); + }, + + hideQuitModal() { + document.getElementById('quit-modal')?.remove(); + }, + + // The server broadcasts this over both /events and /trace/events just before it tears the + // process down (POST /actions/quit). Browsers won't let a page close a user/OS-opened tab, so - + // like dialog-tool's #skein-shutdown screen - we just tell the user they can close it, and stop + // the now-pointless SSE reconnect churn against the port that's about to disappear. + showShutdownScreen() { + try { window.stop(); } catch (e) { /* not supported everywhere; harmless */ } + document.body.innerHTML = + '
' + + '

Skein Shutdown

' + + '

You may close this window now.

'; + }, + // --------------------------------------------------------------------------- // Tree/graph pane: SVG connector lines + drag-to-pan. // Ported from dialog-tool's own main.js (initTreeGraph/drawTreeArrows) - same element ids @@ -940,6 +1076,16 @@ document.addEventListener('click', (evt) => { if (!evt.target.closest('#skein-search-box')) dismissSearch(); }); +// Standalone mode only: warn before an accidental tab close drops unsaved work. renderNavbar +// stamps data-dirty on