diff --git a/AGENTS.md b/AGENTS.md index cc3f5612..c97e4011 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,12 +42,12 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co ### Design system -All five built-in plugins — and every example under `examples/` — share one design system, [`@antfu/design`](https://github.com/antfu/design), so they look and feel like one product across frameworks (Git is React/Next, terminals is Svelte, code-server is vanilla DOM, inspect is Vue, a11y is Solid, the examples are Preact/Next/vanilla). It's a dev dependency consumed at build time: its UnoCSS preset and shipped styles drive every surface, and its Vue components are the canonical reference every framework matches. There is no shared internal design package — each app wires the preset itself and owns its own component ports. +All five built-in plugins — and every example under `examples/` — share one design system, [`@antfu/design`](https://github.com/antfu/design), so they look and feel like one product across frameworks (Git is React/Next, terminals is Svelte, code-server is Vue, inspect is Vue, a11y is Solid, the examples are Preact/Next/vanilla). It's a dev dependency consumed at build time: its UnoCSS preset and shipped styles drive every surface, and its Vue components are the canonical reference every framework matches. There is no shared internal design package — each app wires the preset itself and owns its own component ports. - **Respect the skills.** This design system is built to the `antfu` and `antfu-design` skills (UnoCSS-first, class-based semantic tokens, dual light/dark, anti-slop) — load and follow them when building or changing any UI here. The surfaces deliberately echo the upstream devtools they descend from; reference their UI/UX when in doubt: [`antfu/node-modules-inspector`](https://github.com/antfu/node-modules-inspector), [`antfu/vite-plugin-inspect`](https://github.com/antfu/vite-plugin-inspect), [`eslint/config-inspector`](https://github.com/eslint/config-inspector), and [`vitejs/devtools` → `packages/rolldown`](https://github.com/vitejs/devtools/tree/main/packages/rolldown). - **One preset, wired per app.** Each consumer's `uno.config.ts` composes the same stack: `presetAnthonyDesign({ primary })` (from `@antfu/design/unocss`, tuned to devframe's sage green) + `presetWind4()` + `presetIcons()` (Phosphor) + `presetWebFonts()` (DM Sans / DM Mono) + `transformerDirectives()` + `transformerVariantGroup()`, plus the named `z-*` layers the nav/overlay surfaces reference (`z-nav`, `z-dropdown`, `z-tooltip`, `z-toast`, `z-modal-*`, `z-drawer-*`) — `presetAnthonyDesign` blocks plain `z-` so every layer is named. Keep the block identical across apps so the surfaces stay consistent. - **Tokens are semantic shortcuts.** Build UI from `@antfu/design`'s class vocabulary — surfaces `bg-base` / `bg-secondary` / `bg-active`, text `color-base` / `color-muted` / `color-faint` / `color-active`, `border-base`, `op-fade` / `op-mute` — never a hardcoded palette. Import `@antfu/design/styles.css` (or cherry-pick `@antfu/design/styles/base.css` + `scrollbar.css`) once per page; dark mode is the `.dark` class on ``, flipped from the OS preference in the SPA entry. -- **Vue uses the components directly; other frameworks port them.** The Vue surface (inspect) imports components straight from `@antfu/design/components/*` (`ActionButton`, `ActionIconButton`, `DisplayBadge`, `LayoutTabs`, `LayoutToolbar`, `LayoutCard`, …). Every non-Vue surface ports the components it needs into its own framework — React in git and the Next examples, Svelte in terminals, Solid in a11y, Preact in the Preact examples, vanilla DOM helpers in code-server and the Vite hub — mirroring the upstream component's markup, classes and behavior so it renders identically. Port on demand: recreate only what a surface uses, and keep each port faithful to its `@antfu/design` source. +- **Vue uses the components directly; other frameworks port them.** The Vue surface (inspect) imports components straight from `@antfu/design/components/*` (`ActionButton`, `ActionIconButton`, `DisplayBadge`, `LayoutTabs`, `LayoutToolbar`, `LayoutCard`, …). Every non-Vue surface ports the components it needs into its own framework — React in git and the Next examples, Svelte in terminals, Solid in a11y, Preact in the Preact examples, vanilla DOM helpers in the Vite hub — mirroring the upstream component's markup, classes and behavior so it renders identically. Port on demand: recreate only what a surface uses, and keep each port faithful to its `@antfu/design` source. - **One nav, three buttons, one tab selector — strictly.** Every surface opens with the same top bar — a `LayoutToolbar`-style row led by a brand block (a primary-tinted `i-ph:*` icon + the product name). Buttons come in exactly three forms: a **text button** (`ActionButton` → `btn-action` / `btn-primary`), a **bordered icon button** (`ActionIconButton` → `btn-icon-square`), and a **borderless icon button** (round `btn-icon`). Multi-view tools (inspect, git) switch views with the one shared segmented selector (`LayoutTabs` `variant="segment"`: a `bg-secondary` track with `data-[state=active]:bg-base` triggers). Don't invent bespoke nav bars, button shapes, or tab styles. - **Icons** come from the shared Phosphor set (`i-ph:*`, duotone preferred) via `presetIcons` — use them everywhere instead of per-consumer icon libraries or bespoke SVG. - **A surface keeping its own component CSS** (inspect, a11y) sources every color from `@antfu/design`'s semantic shortcuts via `--at-apply` (expanded by `transformerDirectives`) rather than hardcoding a palette, so it tracks the shared theme and the `.dark` class. diff --git a/docs/plugins/code-server.md b/docs/plugins/code-server.md index 7c67191c..b2366f4c 100644 --- a/docs/plugins/code-server.md +++ b/docs/plugins/code-server.md @@ -4,17 +4,30 @@ outline: deep # Code Server -Run [code-server](https://github.com/coder/code-server) (VS Code in the browser) as a devframe panel. The plugin detects a local install, launches it on demand, and embeds the editor in an auto-authenticated iframe. Its launcher UI is plain **vanilla TypeScript** — a state-driven view with no UI framework at all, which makes the framework-neutral point from the opposite end. +Run VS Code in the browser as a devframe panel. The plugin detects a local editor binary, launches it on demand, and embeds the editor in an auto-authenticated iframe. Its launcher is a small **Vue** SPA built on the shared `@antfu/design` system, matching the rest of the devframe surfaces. -Package: `@devframes/plugin-code-server` · framework: **Vanilla TypeScript** +Package: `@devframes/plugin-code-server` · framework: **Vue** ## What it does -- **Detection** — on startup it runs `code-server --version`. When the binary is missing, the launcher renders install instructions instead of a launch button. -- **Launch** — the launcher's button starts code-server as a managed child process bound to a free port, scoped to the workspace, and probes readiness via its `/healthz` endpoint. -- **Auto-auth** — the plugin generates a random token, sets code-server's `HASHED_PASSWORD` to its SHA-256, and hands the matching session cookie back to the already-authorized devframe client, so the editor opens already signed in. +- **Detection** — on startup it probes the resolved binary with `--version`. When none is found, the launcher renders install instructions instead of a launch button. +- **Launch** — the launcher's button starts the editor as a managed child process bound to a free port, scoped to the workspace, and probes readiness before embedding it. +- **Auto-auth** — the plugin generates fresh auth material per launch and hands it to the already-authorized devframe client, so the editor opens already signed in. `code-server` uses a session cookie; `code serve-web` uses a connection token on the URL. -The editor iframe points at code-server's own origin, so its WebSocket traffic flows directly without a reverse proxy. +The editor iframe points at the server's own origin, so its WebSocket traffic flows directly without a reverse proxy. + +## Backends + +`mode: 'local'` (the default) embeds a server running on this machine. Pick the backend with `backend`, or leave it unset to auto-detect — the plugin prefers `code-server` on `PATH`, then falls back to the `code` CLI's `serve-web`. + +| `backend` | Binary | Auth handoff | +|-----------|--------|--------------| +| `'code-server'` | Coder's [`code-server`](https://github.com/coder/code-server) | password + session cookie | +| `'ms-code-serve-web'` | Microsoft's [`code serve-web`](https://code.visualstudio.com/docs/remote/vscode-server) | connection token (`?tkn=`) | + +## Tunnel mode + +`mode: 'tunnel'` runs Microsoft's `code tunnel` and embeds the hosted `vscode.dev` editor. The first launch surfaces a device-login prompt (a verification URL and a one-time code) in the launcher; authentication is handled by `vscode.dev`. Tunnel mode always uses the `code` CLI. ## Standalone @@ -42,8 +55,12 @@ export default defineConfig({ import { createCodeServerDevframe } from '@devframes/plugin-code-server' export default createCodeServerDevframe({ - // bin: 'code-server', // binary to detect / launch (default: PATH) - // serverPort: 8080, // force a port (default: free port near 8080) + // backend: 'code-server', // 'code-server' | 'ms-code-serve-web' (default: auto-detect) + // mode: 'local', // 'local' | 'tunnel' + // serverPort: 8080, // force a port (default: free port near 8080) + // startOnBoot: true, // launch during setup instead of on demand + // reuseExistingServer: true, // adopt a server already answering on the port + // tunnel: { name: 'my-box' }, }) ``` @@ -53,12 +70,12 @@ All functions are namespaced `devframes:plugin:code-server:*`: | Function | Type | Purpose | |----------|------|---------| -| `detect` | `query` | Re-probe for the binary; returns `{ installed, version, bin }`. | -| `status` | `query` | Current status plus the auth cookie when running. | +| `detect` | `query` | Re-probe for the binary; returns `{ installed, version, bin, backend, mode }`. | +| `status` | `query` | Current status plus the connect descriptor when running. | | `start` | `action` | Launch and wait for readiness. | | `stop` | `action` | Stop the process. | -Status (minus the auth cookie) is mirrored into the `…:state` shared state for reactive UIs. +Status (minus the connect descriptor) is mirrored into the `…:state` shared state for reactive UIs. ## Source diff --git a/docs/plugins/index.md b/docs/plugins/index.md index fb9be4ff..c21a45c5 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -15,11 +15,11 @@ Each plugin is built with a **different UI framework**. That is deliberate: devf | [Accessibility Inspector](./a11y) | Solid | Run axe-core against a host app, list WCAG violations, and highlight the offending element in the page on hover. | | [Git](./git) | React (Next.js) | A repository dashboard — status, a commit graph, branches, and diffs, with optional staging and committing. | | [Terminals](./terminals) | Svelte | Stream read-only command output and run fully interactive PTY shells in the browser. | -| [Code Server](./code-server) | Vanilla TypeScript | Launch code-server (VS Code in the browser) on demand and embed it in an auto-authenticated iframe. | +| [Code Server](./code-server) | Vue | Launch VS Code in the browser (code-server, `code serve-web`, or a tunnel) on demand and embed it in an auto-authenticated iframe. | ## One client, any framework -The collection spans Vue, Solid, React, Svelte, and framework-free TypeScript, yet every plugin shares the same node-side surface: register RPC functions, publish shared state, and connect from the browser with `connectDevframe`. Whatever renders the UI — a reactive framework or a handful of DOM calls — talks to the backend through the same protocol. +The collection spans Vue, Solid, React, and Svelte, yet every plugin shares the same node-side surface: register RPC functions, publish shared state, and connect from the browser with `connectDevframe`. Whatever renders the UI, it talks to the backend through the same protocol — the framework-free Vite hub example drives it with a handful of DOM calls. This is the framework-agnostic promise in practice. The browser bundle is the author's to choose; devframe handles the transport, the data model, the adapters, and the agent surface underneath. diff --git a/packages/devframe/src/client/scope.ts b/packages/devframe/src/client/scope.ts index 3bb7b640..8ca420c3 100644 --- a/packages/devframe/src/client/scope.ts +++ b/packages/devframe/src/client/scope.ts @@ -3,6 +3,7 @@ import type { DevframeRpcServerFunctions, DevframeSettings, RpcSharedStateGetOptions, + ScopedRpcFn, ScopedServerFunctions, ScopedSharedStates, } from 'devframe/types' @@ -43,21 +44,21 @@ export interface DevframeScopedClientRpc { /** Call a server RPC function. Bare names resolve within this namespace. */ call: { - & string>(method: T, ...args: Parameters[T], AnyRpcFn>>): Promise[T], AnyRpcFn>>>> + & string>(method: T, ...args: Parameters>): Promise>>> (method: T, ...args: Parameters>): Promise>>> (method: string, ...args: any[]): Promise } /** Call a server RPC event (fire-and-forget). */ callEvent: { - & string>(method: T, ...args: Parameters[T], AnyRpcFn>>): void + & string>(method: T, ...args: Parameters>): void (method: T, ...args: Parameters>): void (method: string, ...args: any[]): void } /** Call an optional server RPC function (no error if unregistered). */ callOptional: { - & string>(method: T, ...args: Parameters[T], AnyRpcFn>>): Promise[T], AnyRpcFn>>> | undefined> + & string>(method: T, ...args: Parameters>): Promise>> | undefined> (method: T, ...args: Parameters>): Promise>> | undefined> (method: string, ...args: any[]): Promise } diff --git a/packages/devframe/src/types/scope.ts b/packages/devframe/src/types/scope.ts index d7218f7d..238875c3 100644 --- a/packages/devframe/src/types/scope.ts +++ b/packages/devframe/src/types/scope.ts @@ -17,6 +17,16 @@ import type { DevframeViewHost } from './views' // work with even when a registry entry's type can't be proven callable. type AnyRpcFn = (...args: any[]) => any +/** + * Resolve a scoped bare name `T` back to its fully-qualified entry in + * `Registry` (`:`). Indexing the plain registry behind a `keyof` + * guard keeps TypeScript from rejecting a generic index into the + * key-remapped `Scoped*Functions` maps (TS2536) once a plugin augments + * the registry, while preserving each name's argument/return typing. + */ +export type ScopedRpcFn + = `${NS}:${T}` extends keyof Registry ? Extract : AnyRpcFn + /** * Augmentable registry mapping a scope namespace to the shape of its * persisted settings. Tools augment it so `ctx.scope('my-plugin')` @@ -157,7 +167,7 @@ export interface DevframeScopedNodeRpc { * (containing `:`) to call another scope's function. */ call: { - & string>(method: T, ...args: Parameters[T], AnyRpcFn>>): Promise[T], AnyRpcFn>>>> + & string>(method: T, ...args: Parameters>): Promise>>> (method: T, ...args: Parameters>): Promise>>> (method: string, ...args: any[]): Promise } @@ -167,7 +177,7 @@ export interface DevframeScopedNodeRpc { * names are resolved within this namespace. */ broadcast: { - & string>(options: ScopedBroadcastOptions[T], AnyRpcFn>>>): Promise + & string>(options: ScopedBroadcastOptions>>): Promise (options: ScopedBroadcastOptions): Promise } diff --git a/plugins/code-server/.storybook/main.ts b/plugins/code-server/.storybook/main.ts index 504078dc..cecaf8c1 100644 --- a/plugins/code-server/.storybook/main.ts +++ b/plugins/code-server/.storybook/main.ts @@ -1,20 +1,25 @@ -import type { StorybookConfig } from '@storybook/html-vite' +import type { StorybookConfig } from '@storybook/vue3-vite' +import vue from '@vitejs/plugin-vue' import UnoCSS from 'unocss/vite' +import { mergeConfig } from 'vite' +import { alias } from '../../../alias' const config: StorybookConfig = { - stories: ['../src/**/*.stories.@(ts|tsx)'], + stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: ['@storybook/addon-docs'], framework: { - name: '@storybook/html-vite', + name: '@storybook/vue3-vite', options: {}, }, - viteFinal(viteConfig) { - viteConfig.plugins ??= [] - viteConfig.plugins.push(UnoCSS()) - // Dev tool reached from arbitrary hostnames (LAN IPs, tunnels, tailnets), - // e.g. when iframed by the storybook-hub example: accept any Host header. - viteConfig.server = { ...viteConfig.server, allowedHosts: true } - return viteConfig + docs: {}, + async viteFinal(config) { + return mergeConfig(config, { + resolve: { alias }, + plugins: [vue(), UnoCSS()], + // Dev tool reached from arbitrary hostnames (LAN IPs, tunnels, + // tailnets), e.g. when iframed by the storybook-hub example. + server: { allowedHosts: true }, + }) }, } - export default config diff --git a/plugins/code-server/.storybook/preview.ts b/plugins/code-server/.storybook/preview.ts index 37011dfb..bab616f2 100644 --- a/plugins/code-server/.storybook/preview.ts +++ b/plugins/code-server/.storybook/preview.ts @@ -1,7 +1,7 @@ -import type { Decorator, Preview } from '@storybook/html-vite' +import type { Decorator, Preview } from '@storybook/vue3-vite' import 'virtual:uno.css' import '@antfu/design/styles.css' -import '../src/client/style.css' +import '../src/spa/style.css' // Drive the shared `@antfu/design` tokens off the toolbar theme toggle: dark mode // is the `.dark` class on ``, and the canvas takes the semantic @@ -13,13 +13,19 @@ function applyTheme(theme: string): void { const withTheme: Decorator = (story, context) => { applyTheme(context.globals.theme ?? 'dark') - return story(context) + return { components: { story }, template: '' } } const preview: Preview = { parameters: { layout: 'fullscreen', - controls: { expanded: true }, + controls: { + expanded: true, + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, }, globalTypes: { theme: { diff --git a/plugins/code-server/README.md b/plugins/code-server/README.md index 7630fab8..02be9c29 100644 --- a/plugins/code-server/README.md +++ b/plugins/code-server/README.md @@ -4,28 +4,37 @@ > This plugin is experimental and may change without a major version bump until > it stabilizes. -Run [code-server](https://github.com/coder/code-server) (VS Code in the -browser) as a devframe panel. The plugin detects a local `code-server` -install, launches it on demand, and embeds the editor in an -auto-authenticated `