@solidrt/cli 0.0.53 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -1
- package/agents/debugging.md +31 -8
- package/dist/console.srtapp +3881 -3525
- package/dist/demos/3d/the-third-dimension/the-third-dimension.srt.js +540 -397
- package/dist/demos/components/gallery/gallery.srt.js +922 -646
- package/dist/server.js +23 -25
- package/package.json +7 -7
- package/src/android/docs.md +5 -2
- package/src/android/main.ts +135 -86
- package/src/client/docs.md +3 -1
- package/src/client/main.ts +6 -9
- package/src/init/main.ts +1 -1
- package/src/init/scaffold/AGENTS.md +4 -2
- package/src/init/scaffold/package.json +6 -6
- package/src/mcp/main.ts +13 -7
- package/src/server/control.ts +9 -2
- package/src/server/docs.md +2 -0
- package/src/server/main.ts +6 -12
- package/src/types/control.d.ts +5 -0
- /package/src/{init → lib}/prompt.ts +0 -0
package/src/mcp/main.ts
CHANGED
|
@@ -192,7 +192,7 @@ let TOOLS: {
|
|
|
192
192
|
name: "list_clients",
|
|
193
193
|
annotations: READ_ONLY,
|
|
194
194
|
description:
|
|
195
|
-
"List the app clients connected to the SolidRT dev server, and what the server serves. Server fields: `generation` (identity of this server run; client ids, node ids and log cursors are only valid within one, so if it changed since your last call, re-fetch them), `key` and `mode` (the project root, or the single file, this server serves - check it is the app you intend to drive before acting), `entry` (the app source file it rebuilds; `load` moves it), `projectDir` (null for a file served on its own), `userInputMuted` (see mute_user_input) and `watchPaused` (see pause_watch). Per client: `id` (pass it as `client` to the other tools), `platform`, `version` (the runtime's git describe; a -dirty suffix means it was built from uncommitted engine changes), `profile` (debug/release), `capabilities` (the capability names compiled into that runtime), `queries` (the dev-tool query kinds that runtime answers: clock, input, snapshot, tree, ...; a list without \"input\" predates send_input, one without \"clock\" predates set_time_scale/step_frames, an empty list predates the advertisement itself - check it before planning a verification strategy), `stats` (whether its overlay is drawn, see set_stats_overlay), `timeScale` (its clock as it last answered set_time_scale/step_frames: 0 paused, 1 real time; back to 1 on every reload), and what the client knows about itself: `clientDir` (its storage tree on its own machine, `<data-root>/client<N>` for a dev client), `pid`, `execPath` (the runtime binary), `host` (hostname), `os` and `kernel` (the OS as a person names it, e.g. \"Android 15 on Pixel 9 Pro\", and the kernel version), `videoDriver` (SDL's: wayland, x11, android, ...) and `gpu` (vendor, renderer, version as GL reports them) - each null on a runtime that predates it or has no such fact. Use version/profile to check whether a connected binary contains a given engine change before debugging against it.",
|
|
195
|
+
"List the app clients connected to the SolidRT dev server, and what the server serves. Server fields: `generation` (identity of this server run; client ids, node ids and log cursors are only valid within one, so if it changed since your last call, re-fetch them), `key` and `mode` (the project root, or the single file, this server serves - check it is the app you intend to drive before acting), `entry` (the app source file it rebuilds; `load` moves it), `projectDir` (null for a file served on its own), `userInputMuted` (see mute_user_input) and `watchPaused` (see pause_watch). Per client: `id` (pass it as `client` to the other tools), `platform`, `version` (the runtime's git describe; a -dirty suffix means it was built from uncommitted engine changes), `profile` (debug/release), `capabilities` (the capability names compiled into that runtime), `queries` (the dev-tool query kinds that runtime answers: clock, input, snapshot, tree, ...; a list without \"input\" predates send_input, one without \"clock\" predates set_time_scale/step_frames, an empty list predates the advertisement itself - check it before planning a verification strategy), `stats` (whether its overlay is drawn, see set_stats_overlay), `timeScale` (its clock as it last answered set_time_scale/step_frames: 0 paused, 1 real time; back to 1 on every reload), and what the client knows about itself: `clientDir` (its storage tree on its own machine, `<data-root>/client<N>` for a dev client), `pid`, `execPath` (the runtime binary), `host` (hostname), `os` and `kernel` (the OS as a person names it, e.g. \"Android 15 on Pixel 9 Pro\", and the kernel version), `videoDriver` (SDL's: wayland, x11, android, ...), `refreshRate` (the display's nominal refresh rate in Hz as SDL reported it at connect, what `onFrame`'s `rate` argument carries; null when the client connected before its window existed, a reconnect fills it in) and `gpu` (vendor, renderer, version as GL reports them) - each null on a runtime that predates it or has no such fact. Use version/profile to check whether a connected binary contains a given engine change before debugging against it.",
|
|
196
196
|
inputSchema: {},
|
|
197
197
|
},
|
|
198
198
|
{
|
|
@@ -232,7 +232,7 @@ let TOOLS: {
|
|
|
232
232
|
name: "get_stats",
|
|
233
233
|
annotations: READ_ONLY,
|
|
234
234
|
description:
|
|
235
|
-
"Performance statistics from a running app client. Start with `window`: a summary of the frames
|
|
235
|
+
"Performance statistics from a running app client. Start with `window`: a summary of the frames that changed the picture in the last window_ms (default 5000, max 10000): tree rebuilds, plus GPU content changes presented without one (a layer write, a shader param, an upload: a sprite or shader app's every frame, where the critical path is the render handler alone) - frames, p50Ms/p95Ms/maxMs of the JS-thread critical path per frame (render handler + layout + postLayout + paint + hover), slowFrames (frames over the refresh period, periodMs), and `worst`, the single most expensive frame with its ageMs, phase breakdown (jsMs/layoutMs/postLayoutMs/paintMs/hoverMs) and that frame's own layout activity (paraShapes, measureCalls, dirtiedNodes, cacheGets/cacheHits, nodesPainted). This is where jank shows: the smoothed figures below average a one-frame hitch away, the window keeps it. Typical flow: send_input a burst (typing, a drag), then get_stats - `frames: 0` means nothing changed the picture in the window (idle app), which is different from all-fast. The window also carries rates for the GPU counters when it spans 2+ frames: fenceTimeoutsPerSec, gpuPassesPerFrame (per presented frame), gpuPassIssueMsPerFrame, gpuPassExecMsPerFrame, gpuFrameExecMsPerFrame, rasterCmdMsPerSec - read these instead of differencing the cumulatives yourself. timeMs (client monotonic clock) and frame (present index) stamp the payload so two samples can be differenced. Then the smoothed figures: fps, CPU%, memory, smoothed JS/layout/paint/hover frame times (ms), setProperty writes per frame, demand-gate reuse/skip counts per second, and live texture count. Layout-activity counters cover the last full rebuild, raw: nodes (live node count, mounted AND detached), mountedNodes/orphanNodes (live at query time: nodes reachable from the root vs not - orphans growing at a stable tree shape mean an unmount leak; absent when no engine is running), measureCalls (text measures; mostly cache hits, cheap), paraShapes (paragraphs actually shaped, i.e. words the shared word cache did not have; the expensive signal - high layoutMs with near-zero paraShapes means the cost is not text shaping), wordHits (words answered from the shared word cache; hits high and paraShapes near zero on a text change means only the changed words were reshaped), dirtiedNodes (layout caches cleared by property writes since the previous rebuild; how much of the tree a write burst invalidated), cacheGets/cacheHits (layout-cache lookups during the rebuild; a hit on a container skips its whole subtree, so a healthy incremental rebuild shows a near-100% hit rate - a low rate at scale means the layout cache is being defeated), nodesPainted (nodes the latest frame's paint walk entered, 0 when that frame reused the display list - the last rebuild's count is in `window.worst`; mountedNodes minus this is what viewport culling skipped - a long scroller should paint a near-constant number of nodes however long its content). GPU-side health, read live at query time (absent when no engine is running): rasterQueue (raster commands sent but not yet executed at the instant of the query, including the one executing; the frame command blocks on vsync in it, so 1 while frames flow is normal - it is a backlog signal only when it climbs across queries while fps drops; a persistently high idle reading has been seen once on a Windows client and is unexplained, so do not conclude from this field alone), idleTicks (cumulative idle frame signals emitted while the GPU had nothing queued; idleTicks racing while rasterQueue sits nonzero would mean the idle-tick gate is broken), fenceTimeouts (cumulative present-fence waits that expired instead of signaling - each one is a frame where the GPU was over budget for 100ms+ and one-frame-in-flight pacing was lost; zero on a healthy machine, climbing means the GPU is the bottleneck right now), gpuPasses/gpuPassIssueMs/gpuPassExecMs (cumulative shader/pipeline target renders on the raster thread, the wall time the raster thread spent issuing them, and the GPU-side time executing them, all in whole ms - diff two queries to get a rate; passes racing far ahead of frames means redundant target re-renders, the failure mode where fps and frameMs look healthy while the raster thread drowns; issue and exec are different clocks: a pass with a heavy fragment shader is cheap to issue and expensive to execute, so a busy GPU with a small issue figure is normal, and gpuPassExecMs is the number to compare against the refresh period. gpuPassExecMs comes from GL timer queries and lags the pass by a frame or two; it is absent, not 0, when the client's context has none), gpuFrameExecMs (cumulative GPU-side time executing the window draw of each presented frame - the display list plus any window shader, excluding the pass flush and the present - from the same timer queries, same absence rule; gpuFrameExecMsPerFrame in the window is the number to hold against periodMs: near or above it, the GPU is the bottleneck and fenceTimeouts follow, while a healthy jsMs says nothing about it), rasterCmdMs (cumulative wall time in whole ms the raster thread spent executing non-frame commands - texture uploads, readbacks, offscreen rasterizations, shader compiles, param writes and the target re-renders they trigger; the work frameMs never sees, so rasterCmdMs growing much faster than frames are presented means the raster thread is drowning in side work even if every counter above looks calm).",
|
|
236
236
|
inputSchema: {
|
|
237
237
|
window_ms: z
|
|
238
238
|
.number()
|
|
@@ -321,9 +321,10 @@ let TOOLS: {
|
|
|
321
321
|
name: "get_gpu_resources",
|
|
322
322
|
annotations: READ_ONLY,
|
|
323
323
|
description:
|
|
324
|
-
"Inventory of a running app client's GPU resources: textures (id, size, whether a shader renders into it), vertex buffers (id, byteLength), and shader/pipeline targets (output textureId, kind, bufferId, topology, drawCount plus firstVertex/instanceCount when off their 0/1 defaults, depth, attribute layout, bound sampler texture ids, current uniform values - the most recent writes, which the next frame or readback draws with - plus passes/issueMs/execMs, cumulative per-target render count, raster-thread issue time and GPU-side execution time in whole ms: when get_stats shows gpuPasses or gpuPassExecMs running hot, these attribute the cost to the specific target). Use it when the render tree is just a <texture> leaf and the interesting state lives behind it; follow up with get_texture or get_buffer to see contents. Pass `label` to keep only the resources created with exactly that debug label (the create's `label` option) - the stable way to find a target again after a reload, since ids change.",
|
|
324
|
+
"Inventory of a running app client's GPU resources: textures (id, size, format, whether a shader renders into it, and its declared sampler - filter/wrap/mipmap/anisotropy - the first thing to check when a map looks soft or aliased), vertex buffers (id, byteLength), and shader/pipeline targets (output textureId, kind, bufferId, topology, drawCount plus firstVertex/instanceCount when off their 0/1 defaults, depth, attribute layout, bound sampler texture ids, current uniform values - the most recent writes, which the next frame or readback draws with - plus passes/issueMs/execMs, cumulative per-target render count, raster-thread issue time and GPU-side execution time in whole ms: when get_stats shows gpuPasses or gpuPassExecMs running hot, these attribute the cost to the specific target). Use it when the render tree is just a <texture> leaf and the interesting state lives behind it; follow up with get_texture or get_buffer to see contents. Pass `label` to keep only the resources created with exactly that debug label (the create's `label` option) - the stable way to find a target again after a reload, since ids change. In a draw target's entry list, uniforms wider than a vec4 (matrices) are elided to their length (\"[16]\") so a model's hundred entries stay readable; pass `draw` (an entry id from that list, with `label` to pin the target, since entry ids are per target) to get that one entry's params in full.",
|
|
325
325
|
inputSchema: {
|
|
326
326
|
label: z.string().describe("Keep only resources whose create label equals this").optional(),
|
|
327
|
+
draw: z.number().int().describe("Draw entry id whose params are reported in full (pair with label)").optional(),
|
|
327
328
|
client: CLIENT_ARG,
|
|
328
329
|
},
|
|
329
330
|
},
|
|
@@ -415,14 +416,14 @@ let TOOLS: {
|
|
|
415
416
|
name: "pause_watch",
|
|
416
417
|
annotations: SETS_STATE,
|
|
417
418
|
description:
|
|
418
|
-
"Pause the dev server's reload-on-save until resume_watch, so your half-finished saves are not pushed to the user's screens while you edit; your explicit reload still is. Call it before an edit burst; when the edits are done, reload, then resume_watch. Changes saved while paused are not replayed on resume: reload is what pushes them. The pause lifts on resume_watch, when the dev server goes away, or when this bridge exits. ALWAYS resume when you are done: while paused, the human's own saves reach nothing.",
|
|
419
|
+
"Pause the dev server's reload-on-save until resume_watch, so your half-finished saves are not pushed to the user's screens while you edit; your explicit reload still is. Call it before an edit burst; when the edits are done, reload, then resume_watch. Changes saved while paused are not replayed on resume: reload is what pushes them. The pause lifts on resume_watch, when the dev server goes away, or when this bridge exits. ALWAYS resume when you are done: while paused, the human's own saves reach nothing. Returns `{ ok, watchPaused }`.",
|
|
419
420
|
inputSchema: {},
|
|
420
421
|
},
|
|
421
422
|
{
|
|
422
423
|
name: "resume_watch",
|
|
423
424
|
annotations: SETS_STATE,
|
|
424
425
|
description:
|
|
425
|
-
"Lift the pause set by pause_watch: saves push again, the human's included. Call it after your reload, and always before you stop working.",
|
|
426
|
+
"Lift the pause set by pause_watch: saves push again, the human's included. Call it after your reload, and always before you stop working. Returns `{ ok, watchPaused }`.",
|
|
426
427
|
inputSchema: {},
|
|
427
428
|
},
|
|
428
429
|
{
|
|
@@ -529,8 +530,12 @@ async function callTool(name: string, args: any): Promise<ControlResult> {
|
|
|
529
530
|
case "resume_watch": {
|
|
530
531
|
let paused = name === "pause_watch"
|
|
531
532
|
let result = await control(`/watch?active=${!paused}`, "POST")
|
|
532
|
-
if (result.ok)
|
|
533
|
-
|
|
533
|
+
if (!result.ok) return result
|
|
534
|
+
watchPaused = paused
|
|
535
|
+
// The control API answers in its uniform `active` (whether it watches);
|
|
536
|
+
// name the state as list_clients does, so an `active: false` cannot be
|
|
537
|
+
// read as "the pause is not active".
|
|
538
|
+
return { ...result, body: { ok: true, watchPaused } }
|
|
534
539
|
}
|
|
535
540
|
case "get_snapshot": {
|
|
536
541
|
if (typeof args?.nodeId !== "number") return { ok: false, message: "get_snapshot requires a numeric nodeId" }
|
|
@@ -565,6 +570,7 @@ async function callTool(name: string, args: any): Promise<ControlResult> {
|
|
|
565
570
|
case "get_gpu_resources": {
|
|
566
571
|
let params = new URLSearchParams()
|
|
567
572
|
if (typeof args?.label === "string") params.set("label", args.label)
|
|
573
|
+
if (typeof args?.draw === "number") params.set("draw", String(args.draw))
|
|
568
574
|
if (typeof args?.client === "number") params.set("client", String(args.client))
|
|
569
575
|
let qs = params.toString()
|
|
570
576
|
return control(`/gpu${qs ? `?${qs}` : ""}`)
|
package/src/server/control.ts
CHANGED
|
@@ -81,6 +81,7 @@ export function clientList(withAddress = false): (ClientEntry & { address?: stri
|
|
|
81
81
|
os: info.os,
|
|
82
82
|
kernel: info.kernel,
|
|
83
83
|
videoDriver: info.videoDriver,
|
|
84
|
+
refreshRate: info.refreshRate,
|
|
84
85
|
gpu: info.gpu,
|
|
85
86
|
...(withAddress ? { address: ws.remoteAddr ?? null } : {}),
|
|
86
87
|
}))
|
|
@@ -410,9 +411,15 @@ export async function handleControl(req: Request, path: string, query: Map<strin
|
|
|
410
411
|
return handleQuery(query, "snapshot", extra)
|
|
411
412
|
}
|
|
412
413
|
case "/__control__/gpu": {
|
|
413
|
-
// ?label=<text> keeps only resources created with exactly that label
|
|
414
|
+
// ?label=<text> keeps only resources created with exactly that label;
|
|
415
|
+
// ?draw=<id> reports that draw entry's params in full (matrix-valued
|
|
416
|
+
// params are elided everywhere else).
|
|
417
|
+
let extra: Record<string, unknown> = {}
|
|
414
418
|
let label = query.get("label")
|
|
415
|
-
|
|
419
|
+
if (label !== undefined) extra.label = label
|
|
420
|
+
let draw = parseInt(query.get("draw") ?? "", 10)
|
|
421
|
+
if (Number.isFinite(draw)) extra.draw = draw
|
|
422
|
+
return handleQuery(query, "gpu", extra)
|
|
416
423
|
}
|
|
417
424
|
case "/__control__/debug": {
|
|
418
425
|
// GET lists the app's registered debug commands; POST calls one, with
|
package/src/server/docs.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`run` is the everyday command: it starts the dev server and a local client
|
|
4
4
|
window together, and it is what `bun run dev` calls in a scaffolded project.
|
|
5
|
+
The server outlives the client: closing (or killing) a wedged client keeps
|
|
6
|
+
the server up, and `srt client` reattaches a new one.
|
|
5
7
|
|
|
6
8
|
{{ usage run }}
|
|
7
9
|
|
package/src/server/main.ts
CHANGED
|
@@ -248,6 +248,7 @@ function onOpen(ws: ServerWebSocket) {
|
|
|
248
248
|
os: null,
|
|
249
249
|
kernel: null,
|
|
250
250
|
videoDriver: null,
|
|
251
|
+
refreshRate: null,
|
|
251
252
|
gpu: null,
|
|
252
253
|
})
|
|
253
254
|
console.log(`[cli] Client connected ${ws.remoteAddr ?? "unknown"}`)
|
|
@@ -263,10 +264,6 @@ function onClose(ws: ServerWebSocket) {
|
|
|
263
264
|
let info = state.clients.get(ws)
|
|
264
265
|
state.clients.delete(ws)
|
|
265
266
|
console.log(`[cli] Client disconnected: ${info?.platform ?? "unknown"}`)
|
|
266
|
-
// `srt run` lives as long as its clients: once the local client is gone,
|
|
267
|
-
// the last remote disconnect ends the server. `srt server` runs until
|
|
268
|
-
// stopped.
|
|
269
|
-
if (config.client && localClientExited && state.clients.size === 0) shutdown()
|
|
270
267
|
}
|
|
271
268
|
|
|
272
269
|
function onMessage(ws: ServerWebSocket, msg: string | Uint8Array) {
|
|
@@ -290,6 +287,7 @@ function onMessage(ws: ServerWebSocket, msg: string | Uint8Array) {
|
|
|
290
287
|
os: text(data.os),
|
|
291
288
|
kernel: text(data.kernel),
|
|
292
289
|
videoDriver: text(data.videoDriver),
|
|
290
|
+
refreshRate: typeof data.refreshRate === "number" ? data.refreshRate : null,
|
|
293
291
|
gpu:
|
|
294
292
|
data.gpu && typeof data.gpu === "object"
|
|
295
293
|
? { vendor: text(data.gpu.vendor) ?? "", renderer: text(data.gpu.renderer) ?? "", version: text(data.gpu.version) ?? "" }
|
|
@@ -399,7 +397,6 @@ let keepalive = setInterval(() => {
|
|
|
399
397
|
let shuttingDown = false
|
|
400
398
|
let stopRepl = () => {}
|
|
401
399
|
let localClient: Child | null = null
|
|
402
|
-
let localClientExited = false
|
|
403
400
|
let signalOffs = ["SIGINT", "SIGTERM"].map((signal) =>
|
|
404
401
|
onSignal(signal, () => {
|
|
405
402
|
shutdown()
|
|
@@ -463,14 +460,11 @@ if (config.client) {
|
|
|
463
460
|
localClient = child
|
|
464
461
|
pump(child.stdout, (line) => console.log(line))
|
|
465
462
|
pump(child.stderr, (line) => console.error(line))
|
|
463
|
+
// The server outlives its client: a wedged or crashed client is restarted
|
|
464
|
+
// with `srt client` (it reattaches by cwd) without losing the server, its
|
|
465
|
+
// bundle, the watcher or the MCP session. The server stops on quit/signal.
|
|
466
466
|
child.status().then(() => {
|
|
467
467
|
localClient = null
|
|
468
|
-
|
|
469
|
-
if (shuttingDown) return
|
|
470
|
-
if (state.clients.size === 0) {
|
|
471
|
-
shutdown()
|
|
472
|
-
} else {
|
|
473
|
-
console.log(`[cli] Local client exited, ${state.clients.size} remote client(s) still connected`)
|
|
474
|
-
}
|
|
468
|
+
if (!shuttingDown) console.log("[cli] Local client exited; the server keeps running (srt client reattaches)")
|
|
475
469
|
})
|
|
476
470
|
}
|
package/src/types/control.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export type ClientEntry = {
|
|
|
35
35
|
kernel: string | null
|
|
36
36
|
/** The SDL video driver ("wayland", "x11", "android", "offscreen", ...). */
|
|
37
37
|
videoDriver: string | null
|
|
38
|
+
/** The display's nominal refresh rate in Hz as SDL reported it when the
|
|
39
|
+
* client connected (what `onFrame`'s `rate` argument carries); null on a
|
|
40
|
+
* runtime that predates it, or on a client that connected before its
|
|
41
|
+
* window existed (a reconnect fills it in). */
|
|
42
|
+
refreshRate: number | null
|
|
38
43
|
/** The GPU strings as GL reports them; null on a client that connected
|
|
39
44
|
* before its GL context existed (a reconnect fills it in). */
|
|
40
45
|
gpu: GpuInfo | null
|
|
File without changes
|