Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .agents/skills/testing-docs-site/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ messages, reload, and confirm the loaded file carries the intended rule before
diagnosing a CSS fix as ineffective. Do not change repository configuration solely
to make a preview work.

### The in-browser engines

The landing diagram and the CLI page's REPL load generated assets: `docs/assets/sysml-engine.wasm.gz`,
`sysml-repl.wasm.gz`, `wasm_exec.js` and `repl-examples/`. They are gitignored; if any is missing,
run `make docs-engine-assets` before building the site.

The REPL walkthroughs' inputs and expected output strings live in `docs/assets/repl-walkthroughs.json`.
Run each step through its button and check only newly appended terminal output. Wait for the input
queue to drain and `is-busy` to clear before sending the next command.

To check the SMT refusal, declare a valid constraint first, for example
`constraint def Positive { in x : ScalarValues::Integer; x > 0 }`, then run `%check Positive`. Expect
the message that a WebAssembly build cannot start external processes.

Completion considers every matching name, standard library included. `%inst` has several candidates;
`%instanti` has one. For a shared-prefix test, pick names that no library name shares.

At mobile width, navigate through the header menu. `label[for="__drawer"]` also matches a hidden
overlay, so scope automation to `header label[for="__drawer"]`. Compare the document's width with the
viewport's; a code textarea's own horizontal scrolling is not page overflow.

### Devin Secrets Needed

None for the public local site and public outbound link checks.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

# In-browser engine assets (make docs-engine-assets); built, never vendored
/docs/assets/sysml-engine.wasm*
/docs/assets/sysml-repl.wasm*
/docs/assets/repl-examples/
/docs/assets/wasm_exec.js

# Julia package environments resolve locally; the pinned deps are in Project.toml.
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,12 @@ docs-engine-assets: ## Build the in-browser engine assets into docs/assets
@mkdir -p docs/assets
GOOS=js GOARCH=wasm $(GO_BUILD) -o docs/assets/sysml-engine.wasm ./cmd/sysml-engine
gzip -9f docs/assets/sysml-engine.wasm
GOOS=js GOARCH=wasm $(GO_BUILD) -tags sysml_prod -o docs/assets/sysml-repl.wasm ./cmd/sysml
gzip -9f docs/assets/sysml-repl.wasm
@mkdir -p docs/assets/repl-examples
@cp examples/runtime-showcase/*.sysml docs/assets/repl-examples/
@cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" docs/assets/
@echo "✓ Built docs/assets/sysml-engine.wasm.gz + wasm_exec.js"
@echo "✓ Built docs/assets/sysml-engine.wasm.gz, sysml-repl.wasm.gz, repl-examples/ + wasm_exec.js"

docs-serve: docs-install ## Serve the documentation site with live reload
$(PYTHON) -m mkdocs serve --strict
Expand Down
12 changes: 12 additions & 0 deletions changes/unreleased/browser-repl.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- **The CLI page runs the real `sysml` REPL in the browser, with guided walkthroughs.** The
page's toy shell over `sysml-engine` is replaced by `cmd/sysml` itself, built for js/wasm
with `-tags sysml_prod` (about 12 MB gzipped, downloaded when started), on an in-memory
filesystem holding the runtime-showcase models. Seven walkthroughs type their commands into
the prompt: units, the Saturn V mass rollup, the delta-v analysis and sweep, failing
requirements, stepping an action, a state machine on a clock and a user-written one. Up and
Down recall earlier entries, kept across visits, and Tab completes through the REPL's own
completer. `TestBrowserREPLWalkthroughs` runs every walkthrough through the page's host.
- **The landing-page diagram can be edited.** "Edit the model" opens the diagram's SysML
source; the engine re-parses and re-instantiates it as you type and redraws the parts and
interfaces, a parse error is shown with its line and column while the last good diagram
stays up, and "Run the model" executes the edited model.
34 changes: 34 additions & 0 deletions cmd/sysml/host_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//go:build js

package main

import (
"encoding/json"
"syscall/js"

"github.com/Open-MBEE/OpenSysML/internal/frontend/repl"
)

// hostInterruptLine is the line a page sends to interrupt the REPL (Ctrl-C),
// discarding an unfinished declaration.
const hostInterruptLine = "\x03"

// exposeCompletion lets a page complete the line being typed:
// globalThis.sysmlReplComplete(textBeforeCursor) answers {candidates, prefix} as JSON.
func exposeCompletion(sess *repl.Session) {
js.Global().Set("sysmlReplComplete", js.FuncOf(func(_ js.Value, args []js.Value) any {
if len(args) == 0 || args[0].Type() != js.TypeString {
return "{}"
}
line := args[0].String()
c := sess.Complete(line, len(line))
out, err := json.Marshal(struct {
Candidates []string `json:"candidates"`
Prefix string `json:"prefix"`
}{c.Candidates, c.Prefix})
if err != nil {
return "{}"
}
return string(out)
}))
}
11 changes: 11 additions & 0 deletions cmd/sysml/host_wasip1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build wasip1

package main

import "github.com/Open-MBEE/OpenSysML/internal/frontend/repl"

// hostInterruptLine is empty: WASI input is a plain stream with no interrupt.
const hostInterruptLine = ""

// exposeCompletion has no host to expose completion to under WASI.
func exposeCompletion(*repl.Session) {}
22 changes: 14 additions & 8 deletions cmd/sysml/prompt_plain.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ import (

// newLineInput opens the prompt's line reader over standard input. A WebAssembly
// host has no line editor to hand: lines arrive one at a time from whatever the
// host wired up, without history, completion or an interrupt to catch. The session
// is passed for the signature it has on every build and is not consulted. The
// returned function closes the reader, which is nothing to do over standard input.
func newLineInput(_ *repl.Session) (repl.LineReader, func() error, error) {
return &plainReader{in: bufio.NewReader(os.Stdin), out: os.Stdout}, func() error { return nil }, nil
// host wired up. A browser page gets completion through the session and may send
// hostInterruptLine for Ctrl-C. The returned function closes the reader, which is
// nothing to do over standard input.
func newLineInput(sess *repl.Session) (repl.LineReader, func() error, error) {
exposeCompletion(sess)
return &plainReader{in: bufio.NewReader(os.Stdin), out: os.Stdout, interrupt: hostInterruptLine}, func() error { return nil }, nil
}

// plainReader yields the lines it is read from, writing each prompt before it waits.
type plainReader struct {
in *bufio.Reader
out io.Writer
in *bufio.Reader
out io.Writer
interrupt string // a line that reads as repl.ErrInterrupt, when nonempty
}

// ReadLine writes the prompt and reads the next line, without its line ending, and
Expand All @@ -43,7 +45,11 @@ func (r *plainReader) ReadLine(prompt string) (string, error) {
return "", io.EOF
}
}
return strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r"), nil
line = strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r")
if r.interrupt != "" && line == r.interrupt {
return "", repl.ErrInterrupt
}
return line, nil
}

// isTerminal reports whether the file descriptor is a terminal: no WebAssembly host
Expand Down
Loading
Loading