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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<number>` 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 `<html>`, 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.
Expand Down
39 changes: 28 additions & 11 deletions docs/plugins/code-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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' },
})
```

Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 4 additions & 3 deletions packages/devframe/src/client/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
DevframeRpcServerFunctions,
DevframeSettings,
RpcSharedStateGetOptions,
ScopedRpcFn,
ScopedServerFunctions,
ScopedSharedStates,
} from 'devframe/types'
Expand Down Expand Up @@ -43,21 +44,21 @@ export interface DevframeScopedClientRpc<NS extends string = string> {

/** Call a server RPC function. Bare names resolve within this namespace. */
call: {
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>>>
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>): Promise<Awaited<ReturnType<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>>>
<T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>
(method: string, ...args: any[]): Promise<any>
}

/** Call a server RPC event (fire-and-forget). */
callEvent: {
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>): void
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>): void
<T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): void
(method: string, ...args: any[]): void
}

/** Call an optional server RPC function (no error if unregistered). */
callOptional: {
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>> | undefined>
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>): Promise<Awaited<ReturnType<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>> | undefined>
<T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>> | undefined>
(method: string, ...args: any[]): Promise<any>
}
Expand Down
14 changes: 12 additions & 2 deletions packages/devframe/src/types/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` (`<NS>:<T>`). Indexing the plain registry behind a `keyof`
* guard keeps TypeScript from rejecting a generic index into the
* key-remapped `Scoped*Functions<NS>` maps (TS2536) once a plugin augments
* the registry, while preserving each name's argument/return typing.
*/
export type ScopedRpcFn<Registry, NS extends string, T extends string>
= `${NS}:${T}` extends keyof Registry ? Extract<Registry[`${NS}:${T}`], AnyRpcFn> : AnyRpcFn

/**
* Augmentable registry mapping a scope namespace to the shape of its
* persisted settings. Tools augment it so `ctx.scope('my-plugin')`
Expand Down Expand Up @@ -157,7 +167,7 @@ export interface DevframeScopedNodeRpc<NS extends string = string> {
* (containing `:`) to call another scope's function.
*/
call: {
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>>>
<T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>): Promise<Awaited<ReturnType<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>>>
<T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>
(method: string, ...args: any[]): Promise<any>
}
Expand All @@ -167,7 +177,7 @@ export interface DevframeScopedNodeRpc<NS extends string = string> {
* names are resolved within this namespace.
*/
broadcast: {
<T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<Extract<ScopedClientFunctions<NS>[T], AnyRpcFn>>>): Promise<void>
<T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<ScopedRpcFn<DevframeRpcClientFunctions, NS, T>>>): Promise<void>
(options: ScopedBroadcastOptions<string, any[]>): Promise<void>
}

Expand Down
27 changes: 16 additions & 11 deletions plugins/code-server/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -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
14 changes: 10 additions & 4 deletions plugins/code-server/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -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 `<html>`, and the canvas takes the semantic
Expand All @@ -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: '<story />' }
}

const preview: Preview = {
parameters: {
layout: 'fullscreen',
controls: { expanded: true },
controls: {
expanded: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
globalTypes: {
theme: {
Expand Down
Loading
Loading